Natalie Perret
Natalie Perret

Reputation: 9057

What those F# functions are supposed to do?

Learning F# these days, I've noticed that in some libraries like this one or that one there are some similar functions which seem to be common in F# but can't really decipher them, what are they doing, what are they for?

let ap x f =
    match f, x with
    | Ok f        , Ok x    -> Ok (f x)
    | Error e     , _       -> Error e
    | _           , Error e -> Error e
let inline (<*>) f x = ap x f
let inline (<!>) f x = Result.map f x
let inline lift2 f a b = f <!> a <*> b

Even aggregating comments with them does not really help in my understanding:

/// Sequential application
/// If the wrapped function is a success and the given result is a success the function is applied on the value. 
/// Otherwise the exisiting error messages are propagated.
let ap x f =
match f,x with
    | Ok f        , Ok x    -> Ok (f x)
    | Error e     , _       -> Error e
    | _           , Error e -> Error e

/// Sequential application
/// If the wrapped function is a success and the given result is a success the function is applied on the value. 
/// Otherwise the exisiting error messages are propagated.
let inline (<*>) f x = ap x f

/// Infix map, lifts a function into a Result and applies it on the given result.
let inline (<!>) f x = Result.map f x

/// Promote a function to a monad/applicative, scanning the monadic/applicative arguments from left to right.
let inline lift2 f a b = f <!> a <*> b

I don't even see an example of how they could be used, not sure also why inline has been used.

If there is somebody who could hint about how useful those functions are, I would greatly appreciate.

Upvotes: 1

Views: 195

Answers (2)

Tarmil
Tarmil

Reputation: 11372

These are called "applicative functors" (sometimes just "applicatives"). Their purpose is to combine data from multiple Something<'T> using a function. Basically, "lifting" a function of type 'Arg1 -> 'Arg2 -> ... -> 'Result into a function of type Something<'Arg1> -> Something<'Arg2> -> ... -> Something<'Result>.

For example, given the standard Result type:

type Result<'T, 'Err> = Ok of 'T | Error of 'Err

you may have several Result values that you want to combine together. For example, say you have a form with inputs firstName, lastName and age. You also have a result type Person:

type Person = { firstName: string; lastName: string; age: int }

// string -> string -> int -> Person
let makePerson firstName lastName age =
    { firstName = firstName; lastName = lastName; age = age }

The values coming from your actual form may have type Result<string, InputError> or Result<int, InputError>, which can be Error if eg. the user hasn't entered a value.

type InputError =
    | FieldMissing of fieldName: string
    // Other error cases...

You want to combine them into a Result<Person, InputError>, which is Ok if all inputs are Ok, or Error if any input is Error. Using the applicative, you can do it like this:

// Result<string, InputError> -> Result<string, InputError> -> Result<int, InputError> -> Result<Person, InputError>
let makePersonResult firstName lastName age =
    makePerson <!> firstName <*> lastName <*> age

// Example uses:

makePersonResult (Ok "John") (Ok "Doe") (Ok 42)
// --> Ok { firstName = "John"; lastName = "Doe"; age = 42 }
makePersonResult (Error (FieldMissing "firstName")) (Ok "Doe") (Ok 42)
// --> Error (FieldMissing "firstName")

A similar concept can be applied to many other types than Result, which is why it was given a name. For example, an applicative on Async<'T> could run all the argument Asyncs in parallel, and when they're finished, combine their results into an Async<'Result>. Another example, an applicative on 'T list would be equivalent to the standard library's List.map2 or List.map3 but generalizable to any number of argument lists.

Side note: if you look up "applicative functor", most of the results you'll find will be in Haskell, where the map operator, usually written <!> in F#, is written <$> instead.

Upvotes: 5

dumetrulo
dumetrulo

Reputation: 1991

Scott Wlaschin's F# for fun and profit (https://fsharpforfunandprofit.com) has a series Map and Bind and Apply, Oh my! (https://fsharpforfunandprofit.com/posts/elevated-world-7) which should be able to shed more light on this. Regarding your particular question:

  • <!> is the map operator which applies a function f and a parameter x to elements of the data structure you are mapping over, or in other words, lifts the function into the realm of the data structure, in this case the Result type.
  • <*> is the ap (apply) operator which unpacks a function wrapped inside a elevated value into a lifted function.
  • lift2 is basically the map operator for a two-parameter function.

Please have a look at the blog, it really helps!

Upvotes: 5

Related Questions