7
\$\begingroup\$

To practice writing code in the functional programming style, I wrote a program to plot two-dimensional lattice random walks. I'd appreciate any feedback about how to improve its "functionaliness".

possibleSteps <- list(c(-1,0), c(0,-1), c(1,0), c(0,1))

step <- function(x) {
        return(unlist(sample(possibleSteps, 1)))
}

takeRandomWalk <- function(nSteps) {
        coordPairs <- Reduce(`+`, lapply(1:nSteps, step), accumulate = T)
        x <- sapply(coordPairs, `[`, 1)
        y <- sapply(coordPairs, `[`, 2)
        return(list(x, y))
}

plotRandomWalk <- function(nSteps, margins) {
        walkObj <- takeRandomWalk(nSteps)
        plot(seq(-margins,margins), seq(-margins,margins),
             type = 'n', xlab = "", ylab = "")
        lines(walkObj[[1]], walkObj[[2]])
}

Call plotRandomWalk(10000, 80) for an example.

EDIT

I have now compiled a far schnazier version as a shiny app:

Check it out!

Thanks for the help!

\$\endgroup\$
4
\$\begingroup\$

Looks good. The code is functional with the exception of using return, which is not necessary as the last value will be returned anyway, so I'd simplify to the following and also in the interested of efficiency only compute the coords once and without seq, as : is shorter:

step <- function(x) {
        unlist(sample(possibleSteps, 1))
}

takeRandomWalk <- function(nSteps) {
        coordPairs <- Reduce(`+`, lapply(1:nSteps, step), accumulate = T)
        x <- sapply(coordPairs, `[`, 1)
        y <- sapply(coordPairs, `[`, 2)
        list(x, y)
}

plotRandomWalk <- function(nSteps, margins) {
        walkObj <- takeRandomWalk(nSteps)
        coords <- -margins:margins
        plot(coords, coords, type = 'n', xlab = "", ylab = "")
        lines(walkObj[[1]], walkObj[[2]])
}

Reduce with accumulate is pretty cool, I think this is the first time I've seen an accumulate parameter on a reduce-like function anywhere.

Now I think you know that, but usually R code like this would be written with less lists and more data frames or matrixes. That doesn't mean the code isn't functional: As long as it's using functions, not mutating things and using functional abstractions instead of imperative constructs it's fine.

So the following is probably more idiomatic and at the same time uses more efficient representations for the data:

possibleSteps <- matrix(c(-1, 0, 0, -1, 1, 0, 0, 1), ncol = 2, byrow = TRUE,
                        dimnames = list(NULL, c("X", "Y")))

Gives a matrix with named columns like so, which will be the representation throughout the rest of the code (which is good for consistency):

> possibleSteps
      X  Y
[1,] -1  0
[2,]  0 -1
[3,]  1  0
[4,]  0  1

The random walk will still be created by sample, except it's sampling indexes. cbind will create a new matrix from the two accumulated sums (cumsum):

takeRandomWalk <- function(nSteps) {
        indexes <- sample(1:dim(possibleSteps)[1], nSteps, TRUE)
        walk <- possibleSteps[indexes,]
        cbind(X = cumsum(walk[,1]), Y = cumsum(walk[,2]))
}

And finally plotRandomWalk is a bit simpler as we can just give lines the constructed coordinates matrix:

plotRandomWalk <- function(nSteps, margins) {
        coords <- -margins:margins
        plot(coords, coords, type = 'n', xlab = "", ylab = "")
        lines(takeRandomWalk(nSteps))
}
\$\endgroup\$
4
\$\begingroup\$

Since you seem focused on functional programming, the main suggestion I'll have is regarding the use of arguments. It is recommended functions use all their arguments (see how step does not use x) and more importantly, that they only use variables that are either passed as arguments or defined inside their body (see how step uses possibleSteps that is defined outside). Fixing for that and making other personal but minor changes, the final code would look like this:

takeRandomStep <- function(possibleSteps) sample(possibleSteps, 1)[[1]]

takeRandomWalk <- function(possibleSteps, nSteps) {
   randomWalk <- replicate(nSteps, takeRandomStep(possibleSteps))

   data.frame(X = cumsum(randomWalk[1, ]),
              Y = cumsum(randomWalk[2, ]))
}

plotRandomWalk <- function(possibleSteps, nSteps, margins = NULL) {
   walkObj <- takeRandomWalk(possibleSteps, nSteps)
   if (is.null(margins)) {
      maxVal  <- max(abs(unlist(walkObj)))
      margins <- ceiling(maxVal) + 1L
   }
   xylim <- c(-margins, margins)

   plot(walkObj, type = "l", xlim = xylim, ylim = xylim)
}

# test code
testSteps <- list(c(-1,0), c(0,-1), c(1,0), c(0,1))
plotRandomWalk(testSteps, 10000)

You will notice and hopefully appreciate:

  1. the use of replicate rather than looping over an unused argument
  2. the use of plot's xlim and ylim arguments rather than the trick you used
  3. making margins an optional argument
\$\endgroup\$
  • \$\begingroup\$ I had forgotten about replicate--thanks, that's super helpful! \$\endgroup\$ – Richard Border Mar 7 '15 at 23:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.