Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

https://github.com/TimTeaFan/loopurrr

Translate purrr functions into regular for loops
https://github.com/TimTeaFan/loopurrr

functional-programming purrr r

Last synced: about 1 month ago
JSON representation

Translate purrr functions into regular for loops

Lists

README

        

---
output: github_document
---

```{r, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
fig.path = "man/figures/README-",
out.width = "100%"
)
```

# loopurrr

![Release status](https://img.shields.io/badge/status-first%20release-yellow)
[![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental)
[![R-CMD-check](https://github.com/TimTeaFan/loopurrr/workflows/R-CMD-check/badge.svg)](https://github.com/TimTeaFan/loopurrr/actions)
[![Codecov test coverage](https://codecov.io/gh/TimTeaFan/loopurrr/branch/main/graph/badge.svg)](https://codecov.io/gh/TimTeaFan/loopurrr?branch=main)
[![CodeFactor](https://www.codefactor.io/repository/github/timteafan/loopurrr/badge)](https://www.codefactor.io/repository/github/timteafan/loopurrr)
![CRAN status](https://img.shields.io/badge/CRAN-not%20published-red)
[](https://twitter.com/timteafan/status/1511034067344572422?s=21)

## Overview

loopurrr's logo a cat sleeping in the form of a circle

`{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:

1. Learning and Teaching Functional Programming
1. Debugging
1. 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.

## Motivation and Use Cases

#### 1. Learning and Teaching Functional Programming
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](https://r4ds.had.co.nz/iteration.html#the-map-functions)),
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"`).

#### 2. Debugging
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](https://r4ds.had.co.nz/iteration.html#dealing-with-failure)).
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.

#### 3. Accessing and Extending `{purrr}` Functions
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.

## Installation

`{loopurrr}` is not on CRAN yet. You can install the latest version from
[GitHub](https://github.com/TimTeaFan/loopurrr) with:

```{r, eval = FALSE}
# install.packages("remotes")
remotes::install_github("TimTeaFan/loopurrr")
```

## Getting Started

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()`:

```{r, comment = "#>", collapse = TRUE, echo = TRUE, eval = TRUE, warning = FALSE, message = FALSE}
library(loopurrr)
get_supported_fns("as_loop")
```

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:

```{r, eval = TRUE}
x <- list(1, c(1:2), c(1:3))
x %>% purrr::map(sum)
```

Next, lets pipe the function call into `as_loop()`.
```{r, eval = FALSE}
x %>%
purrr::map(sum) %>%
as_loop()
```

Depending on the automatically detected output settings, the result will either be:

1. directly *inserted in* the original R *script or the console*, given that the code is run in RStudio and the `{rstudioapi}` package is installed,
1. *copied to the clipboard*, given that the above conditions are not met and the `{clipr}` package is installed,
1. or if none of the conditions above are met, the result will just be *printed to the console*.

```{r, eval = TRUE}
# --- 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`:

```{r}
out
```

## Roadmap and Collaboration

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 in `as_loop()`
- translate `{purrr}`'s iterators to base R equivalents with `as_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.

## History

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.

## Acknowledgements

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}`](https://github.com/rstudio/gradethis/]) package from which I
adapted [this code](https://github.com/rstudio/gradethis/blob/main/R/unpipe.R) to make `as_loop()` work
with piped expressions (function calls).
[`{gradethis}`](https://github.com/rstudio/gradethis/blob/main/LICENSE.md) license and copyrights apply!

I was further inpsired by Miles McBain's [`{datapasta}`](https://github.com/MilesMcBain/datapasta)'s
different output options. Looking at the code alone wasn't enough, I also got help on [StackOverflow](https://stackoverflow.com/questions/70572072/how-to-use-indentation-with-rstudioapiinserttext)
from user @Waldi to make the {rstudioapi} package work.

Finally, I adapted [this answer on StackOverflow](ttps://stackoverflow.com/a/33850689/9349302) to
replace the function arguments of the functions in `map(.f = )` with the actual objects that are being used.

## Disclaimer

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](https://github.com/TimTeaFan/loopurrr/issues).