4 ms·
> Therefore, to do such thing one has to mentally switch the models to something else and „convert“ the fallible function’s result into a „frozen“ representatio
by bcheung 6y ago
> Therefore, to do such thing one has to mentally switch the models to something else and „convert“ the fallible function’s result into a „frozen“ representation.
This is where the concept of lifting comes in.
Take for example a simple function that squares a number.
function sq(n) { return n * n }
Instead of taking something out of a representation (category in math speak), you can instead "lift" your function to be able to work _inside_ the representation.
Here's an example:
const nums = [1, 2, 3]
Now you could write a "for" loop, extract the value from the "array representation" to just the "int" type expected by your "sq" function, call the function, and the push the resultant value into a new array, but there's an easier way.
const squaredNums = nums.map(sq)
Or what about dealing with async values?
fetchNumAsync().then(sq)
Or what about if the value is optional?
maybeNum.map(sq)
Instead of converting from one representation to another back and forth over and over, "lift" your function to be able to work within that representation.
"map" and "then" do the work of lifting a function that doesn't understand that representation and makes it so a much more general function can still do meaningful work within that representation.
This may be a bit advanced, but there's also a special form of composition called "Kleisli composition" that lifts functions that straddle 2 different "representations".
Let's say you have some functions as follows:
processA(value: A) => Result<B,E>
processB(value: B) => Result<C,E>
processC(value: C) => Result<D,E>
How would you go from A -> B -> C -> D?
You could call the function and unwrap it each time and then pass it to the next function.
let b = processA(value)
if b.isErr() { /* abort early */ }
let c = processB(b.unwrap())
...
Or you could do something like:
pipe(processA, processB, processC)(value)
The pipe function in this context would be performing "Kleisli composition" and straddling the 2 different representations for you so you can remove that boilerplate.
In essence, for each function in the pipeline it would check if the returned value was an error, and if it was it would just return that error right away. Otherwise, it would unwrap the result and pass it to the next function.
If you understood that then you have the intuition behind a monad.
The only real difference between a "functor" and a "monad" is that the functor takes functions that return value, and a monad takes functions that return a wrapped value.
Functors lift functions that work within the same "representation".
Monads lift functions that work across 2 different "representations".