8
\$\begingroup\$

I was messing around online at work the other day when our phone systems went down. I came across the R programming language. Now I'm not guru with programming by any means but I like to mess around with different languages here and there, I believe it helps me to become a better programmer. So I decided that I would try to solve the fizzbuzz challenge in R for those of you that don't know what fizzbuzz is:

Write a program that prints the numbers from 1 to 100. But for multiples of three print "Fizz" instead of the number and for the multiples of five print "Buzz". For numbers which are multiples of both three and five print "FizzBuzz"

This is the first thing I've ever written in R and I would like to have some critique done on what I've accomplished so far:

fizzBuzz = function(range, x, y){
    for (i in seq(1, range, by=1)){
        if (i %% x == 0 & i %% y == 0){
            print('FizzBuzz')
        }
        else if (i %% y ==0){
            print('Buzz')
        }
        else if (i %% x == 0){
            print('Fizz')
        }
        else{
            print(i)
        }
    }
}
fizzBuzz(100, 3, 5)

Some key things I'd like to focus on, per norm feel free to critique everything;

  1. Is it a little bit over kill to put this all in a function, obviously I could do it simply without the function, but is this considered good practice in R to put your solutions inside of functions? I only ask because I looked around at peoples code and didn't see very many functions in it.
  2. Are there easier ways to create the range? I feel like seq(from, to, by=1) is a little harder to understand then range(from, to)
\$\endgroup\$
  • 1
    \$\begingroup\$ In ref to 1. I think functions are good solutions (especially for this kind of problem), and good stuff if you can write them. However many R users (including myself) come from no programming experience at all, and so we write things without writing our own functions if we can help it. My solution to the problem, unless I was aiming to write customer functions, would probably rely heavily on data.table package, meaning I wouldn't need to declare any custom functions. \$\endgroup\$ – DaveRGP Dec 8 '16 at 16:12
  • 1
    \$\begingroup\$ Additionally, in R, writing your own functions makes code more portable/reproducible because the functions can be held in a package: statmethods.net/interface/packages.html \$\endgroup\$ – DaveRGP Dec 8 '16 at 16:13
5
+50
\$\begingroup\$

To answer your initial questions:

  1. I can't speak for R good practices in general - but I like functions, and I'm still reusing many of my functions created in my first R script. They also work well with parallelization.
  2. You can use a range shorthand syntax such as 1:n to create a range from 1 through n. Alternatively seq(1, n) would suffice in your use case.

Moreover:

  1. Printing values generally isn't very useful. It doesn't work well with parallelization / vectorization. And in R studio, you can store values in variables and use them later; this wouldn't work with printing on the go.
  2. Loop based approaches, in my experience, are quite slow. Consider using lapply.
  3. I prefer <- over = just to remind me it's not C. (very highly subjective). When its not a top level assignment, e.g., named arguments, I use = due to the difference in scope.

Here's how I would write it. I've also included a parallel function, parallelization works quicker than lapply when range is sufficiently high. The code is supplemented with a mini benchmark, I've adjusted your for-loop code to be compatible, and inlined the integers 3 and 5 where appropriate.

# Run this once to install benchmark suite:
#install.packages(c("microbenchmark", "stringr"), dependencies = TRUE)
require(microbenchmark)
library(parallel)

# Setup parallelization particulars.
cores <- detectCores()
cluster <- makeCluster(cores)

gerardFizzBuzz <- function(i) {
    fizz <- i %% 3
    buzz <- i %% 5

    if (fizz == 0 & buzz == 0) {
      return('FizzBuzz')
    }
    else if (buzz == 0) {
      return('Buzz')
    }
    else if (fizz == 0) {
      return('Fizz')
    }

    return(i)
}

applyFizzBuzz <- function(range) {
    return(lapply(1:range, gerardFizzBuzz))
}

parallelFizzBuzz <- function(range) {
    return(parLapply(cluster, 1:range, gerardFizzBuzz))
}

vectorizedFizzBuzz <- function(range) {
    v <- Vectorize(gerardFizzBuzz)
    return(v(1:range))
}

papasmurfFizzBuzz <- function (range) {
    res <- seq(1, range)

    for (i in res){
      if (i %% 3 == 0 & i %% 5 == 0){
        res[i] <- 'FizzBuzz'
      }
      else if (i %% 5 ==0){
        res[i] <- 'Buzz'
      }
      else if (i %% 3 == 0){
        res[i] <- 'Fizz'
      }
      else{
        res[i] <- i
      }
    }

    return(res)
}

range <- 100000;

perf <- microbenchmark(applyFizzBuzz(range), vectorizedFizzBuzz(range), parallelFizzBuzz(range), papasmurfFizzBuzz(range), times=20)

# note the log scale.
boxplot(perf, names = c("lapply", "Vectorized", "parLapply", "forloop"))

stopCluster(cluster)

This is the result of running the above script on my machine (4 cores), with range <- 100000 and times=20. enter image description here

\$\endgroup\$
  • \$\begingroup\$ Some good stuff. Note that lapply is basically syntactic sugar for a for loop so it won't be faster. Rather than parallelization, the obvious faster implementation is to use vectorized operations on the whole 1:range vector. Would make a nice addition to your benchmarks. \$\endgroup\$ – flodel Nov 30 '16 at 2:25
  • \$\begingroup\$ lapply is quicker than a for loop in this anecdotal test case, quite possibly indicative of more than just syntactic sugar? e.g., it may run via the underlying implementation without having round trips to the R run time. I can't speak for this. Vectorize is a wrapper around mapply, I'll add it to the script for reference. \$\endgroup\$ – Gerard Nov 30 '16 at 10:41
  • \$\begingroup\$ @Gerard I really appreciate the answer, but 99% of what you said went way over my head at this point lol. However, the detail is wonderful and it's a great answer \$\endgroup\$ – papasmurf Nov 30 '16 at 12:07
  • 4
    \$\begingroup\$ I don't think you understand what I meant by vectorized. I mean to do all the operations in memory, using vectorized functions. Try i <- 1:range; fizz <- i %% 3 == 0; buzz <- i %% 5 == 0; out <- as.character(i); out[ fizz & !buzz] <- 'Fizz'; out[!fizz & buzz] <- 'Buzz'; out[ fizz & buzz] <- 'FizzBuzz'; out \$\endgroup\$ – flodel Dec 1 '16 at 3:49
  • 2
    \$\begingroup\$ Your lapply is faster than your for loop because your lapply code includes other implementation improvements (it stores fizz and buzz, it does not contain unnecessary data type conversions, etc.) See this for example (stackoverflow.com/q/2275896/1201032) if you need more convincing, but don't get carried away by the statement that lapply can be a little faster than for, it is marginal. I can provide a better for implementation that will get close to your lapply if you insist. \$\endgroup\$ – flodel Dec 1 '16 at 3:57
9
\$\begingroup\$

Just to add to @flodel's comments, here is a properly vectorized version. One can see this is quite a bit faster, outperforming the parallel option (on 7 cores) by a factor 5. This problem is a quite nice demonstration of why is is worth your time to think about vectorized code:

flodel_fizzbuzz <- function(range = 100, fizz = 3, buzz = 5) {
  s <- 1:range
  is.fizz <- s %% fizz == 0
  is.buzz <- s %% buzz == 0

  s[is.fizz] <- 'Fizz'
  s[is.buzz] <- 'Buzz'
  s[is.fizz & is.buzz] <- 'FizzBuzz'
  return(s)
}

Running the benchmarks from @Gerard's answer (note that vectorizedFizzBuzz isn't truly vectorized):

Unit: milliseconds
                      expr       min        lq      mean    median        uq       max neval  cld
      applyFizzBuzz(range) 296.52224 303.19205 315.76901 309.18860 320.01676 384.18786    20   c 
 vectorizedFizzBuzz(range) 341.41121 356.82580 374.53857 362.45300 372.81459 461.65169    20    d
   parallelFizzBuzz(range) 117.40058 128.91812 158.66095 153.87976 170.76776 288.99613    20  b  
  papasmurfFizzBuzz(range) 292.22177 301.48002 316.31151 307.31605 319.05426 393.53235    20   c 
    flodel_fizzbuzz(range)  27.98101  29.15677  29.93554  29.93097  30.84586  32.09304    20 a   
  1. I do think functions are a nice way to perform these tasks.

  2. The easiest way to create a range of subsequent integers is with :.

enter image description here

This tidyverse version does about equally well (with ~40 ms):

tidy_fizbuzz <- function(range = 100, fizz = 3, buzz = 5) {
  x <- 1:range
  dplyr::case_when(
    x %% (fizz * buzz) == 0 ~ "Fizz Buzz",
    x %% fizz == 0 ~ "Fizz",
    x %% buzz == 0 ~ "Buzz",
    TRUE ~ as.character(x)
  )
}
\$\endgroup\$

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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