3

Is there a way to lift a simple function, like this

fn add(a:i32, b:i32) -> i32 {a+b}

to operate on Option (or any other monadic type), similar to how one would use Applicative in Haskell

I'm aware of this solution:

pub fn add(a: Option<i32>, b: Option<i32>) -> Option<i32> {
  Some(a? + b?)
}

but that requires me to actually write a separate function, that is tightly coupled with Option, whereas what I want is to be able to lift an arbitrary function to an arbitrary monadic type, or some other way to re-use functions operating on simple types with arguments and return values of monadic types

// something like this
let func = Option::lift2(add) //example, not working code                         

I'm obviously thinking Haskell, maybe there's more idiomatic way of doing this in Rust

smitop
  • 4,770
  • 2
  • 20
  • 53
dark_ruby
  • 7,646
  • 7
  • 32
  • 57
  • 5
    Rust doesn't have higher ranked types, so you are unlikely to find something satisfactory that is generic over monads or applicatives. This type of thing can be a fun exercise but idiomatic Rust is _very_ different from idiomatic Haskell - there just isn't the same level of expressive abstraction available. – Peter Hall Aug 05 '20 at 08:28

1 Answers1

4

You could start out with this:

fn lift<A, B, C>(f: impl Fn(A, B)->C) -> impl Fn(Option<A>, Option<B>)->Option<C> {
    move |oa, ob| {
        match (oa, ob) {
            (Some(a), Some(b)) => Some(f(a,b)),
            _ => None,
        }
    }
}

Or, to make it shorter:

fn lift<A, B, C>(f: impl Fn(A, B)->C) -> impl Fn(Option<A>, Option<B>)->Option<C> {
    move |a, b| Some(f(a?, b?))
}

Beware that you would possibly need similar things for FnMut and FnOnce, too.

Moreover, the above is still bound to Option, and not generic over monadic things in general, which is afaik quite cumbersome to simulate in Rust (if possible at all).

phimuemue
  • 34,669
  • 9
  • 84
  • 115
  • How come something like this isn't included into `Option` module? – dark_ruby Aug 05 '20 at 08:23
  • @dark_ruby Probably because idiomatic Rust relies a lot less heavily on function combinators in general. These operations make a lot more sense in a world where you can define and use them in a highly abstract way. – Peter Hall Aug 05 '20 at 08:33
  • 4
    The function body can be simplified to `move |a, b| Some(f(a?, b?))`, which is analogous to what the OP already suggested in the question. – Sven Marnach Aug 05 '20 at 09:00
  • 1
    Isn't [`Option::zip_with`](https://doc.rust-lang.org/stable/std/option/enum.Option.html#method.zip_with) more or less that? – rodrigo Aug 05 '20 at 10:46
  • @rodrigo It doesn't return a new function. You could use it in the function body to define the inner closure: `move |a, b| a.zip_with(b, f)`, but in my opinion it would only obfuscate what the function is doing, and it's not more concise. Moreover, `zip_with()` is unstable. – Sven Marnach Aug 05 '20 at 18:55