tidyverse / magrittr

Improve the readability of R code with the pipe
https://magrittr.tidyverse.org
Other
957 stars 156 forks source link

Should magrittr handle monadic binding? #75

Open lionel- opened 9 years ago

lionel- commented 9 years ago

e.g. this experiment with monadic error handling: https://gist.github.com/lionel-/a9aee3edb45a60a6e393

lionel- commented 9 years ago

The changes to magrittr are in the following commits: https://github.com/lionel-/magrittr/commits/monads

It evaluates piped functions to check if they have class "monadic". If they do, the bind() generic is called in the relevant parts of the fseq sequence.

smbache commented 9 years ago

I'll look forward to looking at it—looks interesting. Not sure my initial enthusiasm for this is shared by my coauthor (maybe with good reason; I know little about the theory here).. In any case I'm sure we will be wiser soon :)

smbache commented 9 years ago

And I'm quite curious to see how you thought about going about it. ;) but will most likely not have time before after Easter. :(

lionel- commented 9 years ago

and I'm quite curious to see what you guys think about it because I'm not sure what I did makes perfect sense ;)

hadley commented 9 years ago

I'd want to see an example with a monad not related to errors, because I think in R, it's better to deal with errors using the condition system.

lionel- commented 9 years ago

The idea I think is not to replace R's condition system but rather to complement it. Ultimately, when it's time to process the errors, we'd use stop(), warning(), etc. The presentation that Stefan tweeted about was kind of convincing that there are advantages to propagate errors monadically. In particular, the argument of having all exceptions defined at the same place was compelling to me.

Other than that, I'm still struggling to figure out what monads are useful for! The Reader monad looks interesting and if I manage to use it in my work I'll add an example to the gist.

smbache commented 9 years ago

I can't run it; I get a stack overflow error...

Still not really sure what I'd want in general

lionel- commented 9 years ago

weird, not sure why you would get such an error.

I noticed I used map() instead of lapply(), this is now fixed. Probably not the cause of your error though.

smbache commented 9 years ago

I noticed that one, loading purrrr got me past that..

vapniks commented 9 years ago

I think this is a very good idea. Anything that moves us in the Haskell direction is very welcome by me.

tonyfischetti commented 9 years ago

That demo is super cool! I think that because of R's dynamic (and weak) type system, most non-error monads are of more limited use, though. For example, 'Either' is unnecessary because you can just return different types from an R function. Certainly the IO monad is necessary because any old function can perform side-effect IO. Writer monad-like-functionality might be pretty cool, but R could always mutate the global state of something that represents a running log.

lionel- commented 9 years ago

I was also wondering about how useful monads really are without strong typing. But it seems they are used in Clojure so they should be useful in R too.

Ultimately, I think we would need someone experimented in monadic programming to implement a library demonstrating how to program with monads in R.

jcheng5 commented 6 years ago

@hadley and I have had conversations about introducing a monadic bind operator for R. The main usages I had in mind were for reactives (from Shiny) and promises. Promises already have the %...>% operator, my thinking was that a monadic operator would have similar semantics but be generic. I was also imagining a dedicated operator instead of overloading %>%.

Reactive expression example w/o bind:

rand <- reactive({ invalidateLater(1000); runif(1, max=100) })
rand_int <- reactive({ rand() %>% round })

With bind (%$>%):

rand <- reactive({ invalidateLater(1000); runif(1, max=100) })
rand_int <- rand %$>% round

Quite different than what you were discussing here I think. In fact I don't even think it's really monadic, just functor-y?

lionel- commented 6 years ago

In fact I don't even think it's really monadic, just functor-y?

I think so. Are functors sufficient for promise chaining?

My feeling is that an operator would be too much syntax for fmap. Functor application is already provided by purrr::modify() so that could be:

rand %>%
  modify(round) %>%
  modify(`*`, 10) %>%
  modify(~ . + 42)

# Or equivalently
chain <- . %>% round() %>% { . * 10 + 42 }
rand %>% modify(chain)

This would have the advantage of reusing existing syntax.

jcheng5 commented 6 years ago

Are functors sufficient for promise chaining?

I mean, technically, I don't think so. The steps in the promise chain need to be able to return objects that are either promises, or not promises, so promises need to join in addition to fmap. And I'm not sure what promises do is even a traditional join, as they can flatten zero or more levels deep, where my understanding is that join does exactly one level. It might be that promises::then is just its own weird thing; would it be inappropriate for it to be used to implement modify.promise?

OTOH I wasn't thinking that chains of reactive expressions would do any flattening at all, so just fmap/modify would do the right thing.

jrosell commented 6 months ago

I'd want to see an example with a monad not related to errors, because I think in R, it's better to deal with errors using the condition system.

@hadley in Haskell there are some examples.

import Control.Monad (when)
import System.Directory (doesFileExist, removeFile)

removeIfExists :: FilePath -> IO ()
removeIfExists filePath =
    doesFileExist filePath >>= \fileExists ->
        when fileExists (removeFile filePath)

Now, one could write something like this in R:

remove_if_exists <- \(file_path){
    f <- purrr::compose(
        .dir = "forward",
        \(file_path) file.exists(file_path),
        \(file_exists) {
            if(file_exists) return(file.remove(file_path))
            return(file_exists)
        }
    )
    f(file_path)
}
file.create("blank.txt")
remove_if_exists("blank.txt")

I think there are cases where what simply a pipable purrr::compose could help:

remove_if_exists <-
    purrr::compose(\(x) {
        list(name = x, return = file.exists(x))
    }, .dir = "forward") %>%
    purrr::compose(\(x) {
        if(x$return) x$return <- file.remove(x$name)
        x
    }, .dir = "forward")

file.create("blank.txt")
remove_if_exists("blank.txt")

Another option, here:

when <- \(x, f) ifelse(x, f, x)
remove_if_exists <- \(file_path) {
    file.exists(file_path)  %>% (\(file_exists) when(file_exists, file.remove(file_path)))
}

file.create("blank.txt")
remove_if_exists("blank.txt")

Bonus: I think it's worth to mention https://github.com/b-rodrigues/chronicler by @b-rodrigues