F# Applicative - how I get my head around this kind of black magic spell

When I came the first time in contact with F#, a little more than a year ago, I work myself to some tutorial and code and stuff. And the first time I dicovered some kind of black magic spell, where some values, which are wrapped in a result-type or error-type and a function which uses the "normal" values and not the values wrapped in the result type, put together with some magic symbols and we get actually a result out of this. The Spell I mean is:

myfunction <!> resultValue1 <*> resultValue1 <*> resultValue2

I tried to get my head around this and I failed. It was to much to process as a beginner in F#. I think I used Scott's Blog Post to get it at this time.
https://fsharpforfunandprofit.com/posts/elevated-world/ But It didn't help me, because I was to new and maybe to stupid to get it.
I put my effort to learn in other stuff and forget about this topic.

Later I discovered the applicative stuff again (I think it was Scott's cheat sheet on gist and some other code example) and now I thought, maybe I get it now. I learned about Scott's ROP, Monads, computation expressions and all the awesome stuff, that F# has to offer.

So this blog post describe the way I get my head around this topic and how I understand, why this following Black Magic Spell actually works.

The Black Magic Spell:

myfunction <!> resultValue1 <*> resultValue1 <*> resultValue2

For someone who is new to the functional stuff, even if the developer is a senior in other programming paradigms, this line of code appears to be some kind of black magic.

On one side, we have our normal function like this:

let myfunction (a:int) (b:int) (c:int) =
    a + b + c

It doesn't take any kind of result-ish parameters. Like Ok 1 or Error "ups!", it uses the values, which are "hidden" in the Result itself. Here in this example 3 integer values.

On the other side, we have actually result-ish values:

let resultValue1 = Ok 2
let resultValue2 = Error "ups!"

Now we put this with some strange symbols together and we get actually our result, wrapped in Result-Datatype.

How?

To understand this, you need to know the concept of partial application. Partial application is the ability to put only a part of the arguments to a function and get a new function back. Also you need to know, how a map function works. In this case the Result.map function.

Quick recap:

// -------------------
// partial application
// -------------------
let add a b =
    a + b
    
// you get a new function with the missing second parameter b
let add10 = add 10  // int->int

add10 10 // result = 20


//----------------
// map function - Result.map
//----------------

let result1 = Ok 1
let result2 = Error "ups!"

let mappingFunction input =
    input + 10

// this inner Ok value of the Result is run thru the mapping function 
// and returns the result wrapped in an Ok
Result.map mappingFunction result // Result= Ok 11

As always Scott Wlashin has the reference material
partial application: https://fsharpforfunandprofit.com/posts/partial-application/

But let us continue...

the Symbols

to understand this "black magic", we need to know, what the symbols stand for.

  1. <!> is the map function or in this case, it stands for the Result.map
  2. <*>is the apply function. This function doesn't existis in the FSharp.Core library. This function we have to write on our own.
let (<!>) = Result.map
let (<*>) = apply

Okay, the Result.map function takes 2 parameters. The first one, is the mapping function with one parameter a' and the result b'. SO the signature is a' -> b'. The second parameter is the result, which we want to map. The type is Result<'a,error'>

With the ability to create our own operators, like <!> in F#, we actually move the parameters from the original Result.map function around this operator. On the left side of our operator, we have the mapping-function our first parameter and on the right side of our operator we have our input the second parameter.

let input = Ok 1
let mapping a = a + 10

let res1 = Result.map mapping input
// is the same as
let res2 = mapping <!> input

So what does this mean to the first part of our "black magic" expression from the beginning:

myfunction <!> resultValue1 <*> resultValue1 <*> resultValue2

where myfunction is actually:

let myfunction (a:int) (b:int) (c:int) =
    a + b + c

We did following:

// myfunnction <!> resultValue1
let firstPart = Result.map myfunction resultValue1

But stop, our myfunction does have 3 parameters and not one. Map function suppose to have 1 parameter as input, not 3 or more of them.

Here comes the partial application in place. We put a function with 3 paramters into a function with one and get as a result a function with 2 parameters left.
But first, let show how the map work by building a map function of our own:

let giveMeAMap (func:int -> 'b) input =
    let res = func input 
    res


giveMeAMap (fun i-> printfn "%i" i) 10 // returns a unit, and prints the 10 on the screen
giveMeAMap (fun i-> i + 100) 10 // returns 110 (it add 100 to our input)
giveMeAMap (fun i-> i.ToString()) 10 // return the string "10"

You see here our mapping function will be called with our input parameter. So what did we get, if we have a function with multiple parameters as a mapping function? Yes, we apply 1 parameter to this function, our input and get a function with the missing other parameters.

// a mapping function 
let mymap a b c = a + b + c // mapping function with 3 parameter
giveMeAMap mymap 10

// you get a function with following signature: int -> int -> int (2 input parameters)
// internally we are running our map function as following:
let res = mymap 10 // we use only one parameter

And now we have solved the first part of our magic spell from above:

myfunction <!> resultValue1 <*> resultValue1 <*> resultValue2

or more precisely

myfunction <!> resultValue1 ...

Let solve the first part:

let resultValue1 = Ok 10

let myfunction (a:int) (b:int) (c:int) =
    a + b + c
    
// Remember myfunction <!> resultValue1 is like:
let firstPart = Result.map myfunction resultValue1

we get:
Ok [some function with the signature int->int->int]
a function wrapped In a result-type

The FSI shows:
[<Struct>]
val firstStep : Result<(int -> int -> int),string> = Ok <fun:Invoke@2697-1>

Now to the second part of our black magic spell, the apply-function:

let apply func input =
    match func,input with
    | Ok f, Ok x -> Ok (func input)  // runs func with the parameter input
    | Error e, _ -> Error e  // return the error if func is already an error
    | _, Error e -> Error e // return the error if the input is an error

// like the map, we make out of the apply function an operator, 
// so the the first parameter, the wrapped function, 
// is on the left side and our second parameter the input is on the right side.
let (<*>) = apply

The apply-function takes 2 parameters. The first one is a function wrapped in a result-Type. The second one is a value also wrapped in a result-Type.
We check both parameters. If both are okay, we use the inner-function and call it with the value from our second parameters. A little bit, like our map function, but here the mapping-function is wrapped in the result-type.

Wait, a function wrapped in a result type. That is exactly the same thing our map function return in the first part of our "magic spell".

Okay, before we continue and to complete the apply-function, in case one of the parameters contains an error, the apply-function returns this error.

Now we have all to understand our black magic spell:

myfunction <!> resultValue1 <*> resultValue1 <*> resultValue2

Let's do it step by step now:

Step 1 (the map function):

let firstStep = myfunction <!> resultValue1
// or
let firstStep = Result.map myfunction resultValue1

we get, as mentions a function (int -> int -> int) wrapped in a result (OK)

Step 2 (the first appearence of our apply function):

let secondStep = myfunction <!> resultValue1 <*> resultValue1
// or
let secondStep = (myfunction <!> resultValue1) <*> resultValue1
// better
let secondStep = firstStep <*> resultValue1
// or, the same without the operator:
let secondStep = apply firstStep resultValue1

In this step our function, which came as a result from our first step, will be "unpacked" and called with the next value from the second parameter (our resultValue1). We make another partial application. Because our function from the firstStep has 2 parameters to work with and we apply only one of them. Like here:

// function with the signature: int -> int -> int
let nextFunc a b = 
    a + b

let nextFunc10 = nextFunc 10 // resturns a function int->int

The apply function here returns a new function with only one parameter left, wrapped in a result-type.

Step 3 (another apply - last step):

let thirdStep = myfunction <!> resultValue1 <*> resultValue1 <*> resultValue1
// or
let thirdStep = (myfunction <!> resultValue1 <*> resultValue1) <*> resultValue1
// better:
let thirdStep = secondStep <*> resultValue1
// or, the same without the operator:
let thirdStep = apply secondStep resultValue1

Here we doing the same, as in the step before. We unpack our function, which is return from the second step and which has one input parameter left, and we run it with the unpacked second parameter, our resultValue1.

And with this step our whole back magic spell from above is solved. We get actually our result wrapped in a result-type.

Here again the whole snippet, with all steps inside:

let apply f x =
    match f,x with
    | Ok f, Ok x -> Ok (f x)
    | Error e, _ -> Error e
    | _, Error e -> Error e


let (<!>) = Result.map
let (<*>) = apply

let myfunction (a:int) (b:int) (c:int) =
    a + b + c


let resultValue1 = Ok 2
let resultValue2 = Error "ups!"

// returns Error "ups!" because, one of the parameter is an error
let res1:Result<int,string> =
    myfunction <!> resultValue1 <*> resultValue1 <*> resultValue2

// return Ok 6, because 2 + 2 + 2 is 6 wrapped in an OK
let res2:Result<int,string> =
    myfunction <!> resultValue1 <*> resultValue1 <*> resultValue1


let firstStep:Result<int->int->int,string> =
    Result.map myfunction resultValue1

let secondStep:Result<int->int,string> =
    apply firstStep resultValue1

let thirdStep:Result<int,string> =
    apply secondStep resultValue2

I hope this explanation helps, it's the way I get my head around this applicative thing.

I know, maybe it is the same as Scott wrote in his blog post and likely he did a better job to describe this topic at all. But this is how I got my head around it later, without to check again the blog post from Scott.