{loopurrr}
makes {purrr}
’s iterator functions more understandable
and easier to debug. In this initial version, {loopurrr}
consists only
of one main function: as_loop()
.
as_loop()
takes a function call to one of {purrr}
’s iterator
functions, such as purrr::map()
, and translates it into a regular
for
loop.
You might ask: “Why would anyone want to do this?!”
as_loop()
has at least three use cases:
- Learning and Teaching Functional Programming
- Debugging
- Accessing and Extending
{purrr}
Functions
The remainder of this readme will expand on the uses cases above, show how to get started, and give a brief outlook on the development roadmap.
Beginners, and especially users new to functional-style programming,
often have a hard time getting their head around R’s rather opaque
iterator functions, such as base R’s lapply()
or purrr::map()
. for
loops, on the other hand, are fairly well understood, even by users new
to R.
as_loop()
translates a function call to one of {purrr}
’s iterator
functions into a regular for
loop. Through this as_loop()
shows how
{purrr}
’s iterator functions work under the hood. After reading about
what iterator functions do (for example
here),
LearneRs can start playing around with calling as_loop()
on the
examples in the {purrr}
documentation. TeacheRs, on the other hand,
can use as_loop()
interactively when introducing the different types
of iterator functions in the {purrr}
package.
Finally, this package is not only for beginners and users new to R. When
writing this package I was fairly confident in my understanding of
{purrr}
’s iterator functions. Nevertheless, translating each of them
into a for
loop was quite revealing, especially with the more complex
functions, such as purrr::reduce()
(specifically when the direction is
set to "backward"
).
Once learneRs know what an iterator function does and how to use it, the
next hurdle to take is dealing with failure. Iterator functions
introduce an additional layer of complexity, because special knowledge
is required to debug non-running code (see also
here). By
translating an iterator function into a regular for
loop, as_loop()
can help with debugging. Usually a for
loop will run over an index,
for example i
. When executed in the global environment, useRs can
easily inspect the code in the console at index i
once the code throws
an error - without any special knowledge of how to use a debugger,
browser()
or purrr::safely()
.
Of course, useRs are still highly encouraged to learn how to use R’s and
{purrr}
’s debugging tools and functions. However, in data science
teams with different levels of programming knowledge, the possibility to
translate complex iterator functions to regular for
loops can help
mitigate temporary knowledge gaps.
After getting used to {purrr}
’s functions, they easily come to mind,
when dealing with iteration problems. However, sometimes the {purrr}
package is not available, for example in a production environment where
new packages cannot easily be installed, or when writing a package that
doesn’t depend on {purrr}
. Although base R equivalents exist for
{purrr}
’s major functions, there are functions like purrr::imap()
or
purrr::reduce2()
which are not available in base R and need to be
constructed. In those cases, as_loop()
provides a ready-to-use
alternative.
Further, by translating {purrr}
’s iterator functions into for
loops,
the underlying building blocks can easily be rearranged to create
functions that are not included in the {purrr}
package. For example,
by translating a call to purrr::imap()
and a call to purrr::map2()
we could easily build a for
loop that loops over two vectors and an
index, as if a function like imap2()
existed.
{loopurrr}
is not on CRAN yet. You can install the latest version from
GitHub with:
# install.packages("remotes")
remotes::install_github("TimTeaFan/loopurrr")
First, lets use get_supported_fns("as_loop")
to get a glimpse of which
iterator functions from the {purrr}
package are currently supported by
as_loop()
:
library(loopurrr)
get_supported_fns("as_loop")
#> $map
#> [1] "map" "map_at" "map_chr" "map_dbl" "map_df" "map_dfc" "map_dfr"
#> [8] "map_if" "map_int" "map_lgl" "map_raw"
#>
#> $imap
#> [1] "imap" "imap_chr" "imap_dbl" "imap_dfc" "imap_dfr" "imap_int" "imap_lgl"
#> [8] "imap_raw"
#>
#> $map
#> [1] "map2" "map2_chr" "map2_dbl" "map2_df" "map2_dfc" "map2_dfr" "map2_int"
#> [8] "map2_lgl" "map2_raw"
#>
#> $pmap
#> [1] "pmap" "pmap_chr" "pmap_dbl" "pmap_df" "pmap_dfc" "pmap_dfr" "pmap_int"
#> [8] "pmap_lgl" "pmap_raw"
#>
#> $lmap
#> [1] "lmap" "lmap_at"
#>
#> $modify
#> [1] "modify" "modify_at" "modify_if" "modify2" "imodify"
#>
#> $walk
#> [1] "iwalk" "pwalk" "walk" "walk2"
#>
#> $accumulate
#> [1] "accumulate" "accumulate2"
#>
#> $reduce
#> [1] "reduce" "reduce2"
Now lets take the first example of {loopurrr}
’s documentation and
start with translating a call to purrr::map()
. First, lets look at the
result:
x <- list(1, c(1:2), c(1:3))
x %>% purrr::map(sum)
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [1] 3
#>
#> [[3]]
#> [1] 6
Next, lets pipe the function call into as_loop()
.
x %>%
purrr::map(sum) %>%
as_loop()
Depending on the automatically detected output settings, the result will either be:
- directly inserted in the original R script or the console, given
that the code is run in RStudio and the
{rstudioapi}
package is installed, - copied to the clipboard, given that the above conditions are not
met and the
{clipr}
package is installed, - or if none of the conditions above are met, the result will just be printed to the console.
# --- convert: `map(x, sum)` as loop --- #
out <- vector("list", length = length(x))
for (i in seq_along(x)) {
out[[i]] <- sum(x[[i]])
}
# --- end loop --- #
To see the result we need to print out
:
out
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [1] 3
#>
#> [[3]]
#> [1] 6
For future versions of {loopurrr}
the following milestones are
planned:
- release
{loopurrr}
on CRAN - enable support for more iterator functions from
{purrr}
(e.g.cross()
etc.) - support base R’s
apply
family inas_loop()
- translate
{purrr}
’s iterators to base R equivalents withas_base()
(yet to be created)
If anyone is interested in collaborating on one or more of those milestones, any help is appreciated! Feel free to reach out to me, for example on Twitter @TimTeaFan.
The idea of this package is based on an experience I had at work. After
diving deeper into {purrr}
’s iterator functions I started refactoring
some old code by replacing loops with map
functions. To my surprise,
although the code was much cleaner now, not everybody liked it. For some
users it made things harder to understand. Learning once how map
functions work, was not enough to solve this, since things got more
complicated when the code was throwing errors. {loopurrr}
allows us to
write clean code and translate it to regular for
loops when needed.
Credit goes to the creators and maintainers of the amazing {purrr}
package! {loopurrr}
is just an add-on which would not exist without
it.
Further, credit goes to the
{gradethis}
package from
which I adapted this
code to make
as_loop()
work with piped expressions (function calls).
{gradethis}
license and copyrights apply!
I was further inpsired by Miles McBain’s
{datapasta}
’s different
output options. Looking at the code alone wasn’t enough, I also got help
on
StackOverflow
from user @Waldi to make the {rstudioapi} package work.
Finally, I adapted this answer on
StackOverflow to replace
the function arguments of the functions in map(.f = )
with the actual
objects that are being used.
This package does not promote for
loops over iterator functions.
Rather the opposite is true. I love the {purrr}
package and would be
happy if people would use it more.
Although this package contains tests with more than 1000 lines of code, there are definitely a number of edge cases which won’t work correctly. If you find one, I’d be happy if you file an issue here.