Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# Common infix operators (fmap, applicative, bind, etc) for multiple types

What I would like do is use infix fmap (which I have defined as <^>) to work for multiple types such as Option and Either (custom type).

Given:

   type Either<'a, 'b> = Left of 'a | Right of 'b 

In code I would like to be able to do:

   let fO (a : int option) = None
   let fE (a : Either<string,int>) = Left "dummy"

   let mO = Some 1
   let mE = Right 1

   let testO = f0 <^> m0
   let testE = fE <^> mE

Where (<^>) for each:

    let (<^>) f m = match m with | Some a -> Some <| f a | None -> None
    let (<^>) f m = match m with | Right a -> Right <| f a | Left a -> Left a

To get the Option <^> to work I have extended the module :

    namespace Microsoft.FSharp.Core
    [<AutoOpen>]
    module Option =
    let (<^>) f m = match m with | Some a -> Some <| f a | None -> None

    [<assembly:AutoOpen("Microsoft.FSharp.Core")>]
    do ()

And for Either:

    type Either<'a, 'b> = Left of 'a | Right of 'b with
       static member (<^>) (f,m) = match m with | Right a -> Right <| f a | Left a -> Left a

This almost works, however only one can be used at a time. An Either module can be also appended to FSharp.Core, but then equally you can only have one or the other.

I'm aware this can be completed with 2 custom types, say Either and Maybe (Haskell option), I would however like to stick with Option.

Any and all suggestions welcome.

like image 506
rbonallo Avatar asked Jan 30 '23 18:01

rbonallo


1 Answers

This isn't really something that's easy to represent in F#, the only way to do this is using statically resolved type parameters and it's generally not considered idiomatic.

Doing this is quite easy for new, custom types but retrofitting it into existing types is more complex. Supporting both is slightly harder again.

The way you can proceed is to create a helper type of a single case discriminated union with static methods hard-coded for existing types:

type Functor = Functor
    with 
    static member FMap (Functor, mapper : 'T -> 'U, opt : Option<'T>) : Option<'U> =
        Option.map mapper opt
    static member FMap (Functor, mapper : 'T -> 'U, ch : Choice<'T, _>) : Choice<'U, _> =
        match ch with
        |Choice1Of2 v -> Choice1Of2 (mapper v)
        |Choice2Of2 v -> Choice2Of2 v

Now you can use a function with statically resolved type paramters to choose the appropriate method based on type:

let inline fmap (f : ^c -> ^d ) (x : ^a) =
    ((^b or ^a) : (static member FMap : ^b * ( ^c -> ^d ) * ^a -> ^e ) (Functor, f, x))

Notice the ^b or ^a condition? That also gives us a method to insert this behaviour into custom types.

type Either<'a, 'b> = Left of 'a | Right of 'b with
    static member FMap (Functor, f, m) = 
        match m with | Right a -> Right <| f a | Left a -> Left a

For operator form, just define:

let inline (<^>) f x = fmap f x

You end up with functions defined:

val inline fmap :
  f:( ^c ->  ^d) -> x: ^a ->  ^e
    when (Functor or  ^a) : (static member FMap : Functor * ( ^c ->  ^d) *  ^a ->  ^e)
val inline ( <^> ) :
  f:( ^a ->  ^b) -> x: ^c ->  ^d
    when (Functor or  ^c) : (static member FMap : Functor * ( ^a ->  ^b) *  ^c ->  ^d)

Now you can do this type of thing with the <^> operator:

let x  = (fun x -> x + 1) <^> (Some 1)
let x'  = (fun x -> x + 1) <^> (None)
let z<'a> : Either<'a, _> = (fun x -> x + 2) <^> (Right 2)
let z'  = (fun x -> x + 2) <^> (Left 5)

Also you can take a look at F#+ for a more complete implentation of many of these standard functional abstractions.

like image 120
TheInnerLight Avatar answered Feb 28 '23 02:02

TheInnerLight