I can't tell others how to write their libraries so that I can understand them easier. That's a given. That's also nonsense. I can do this with code at work though (which I do all the time). What I can do is learn how to read these things better. So I can decipher how others write their code.
But if you read the docs or take a bunch of courses you might not come away with a better understanding. I think it's just fine if your use cases are regular imperative code. When things are curried and you can write a signature for a function as a type within a type that's when things get more difficult.
And honestly, that's where I like to live. So it's time to do a little learning.
I was having issues with complex TypeScript generics/types. I wanted to figure out what was going on with the following function signature. I'm not used to generics espcially complicated ones. I'm fully aware that there are even more complicated ones but this isn't a dick measuring contest. I'm learning, others are too.
export const map: <A, B>(f: (a: A) => B) => <E>(fa: Either<E, A>) => Either<E, B> = (f) => (fa) =>
isLeft(fa) ? fa : right(f(fa.right))
It's just a simple map()
function so why is it so difficult to understand? I couldn't parse out the types as to what they were supposed to be doing and how. I get it now tho. It's actually super simple but when starting out it is crazy hard to understand.
First of all I want to give the answer first. No need to wait on dessert I want to give that right away so I can explain the process.
results in (formatted)
exports.map = function (f) {
return function (fa) {
return exports.isLeft(fa)
? fa
: exports.right(f(fa.right))
}
}
Firstly what I did was gather everything up. I went to the source code and pulled everything out of the Either.ts
from fp-ts
and put it into it's own file JUST for the map()
function.
Which would be all this
type Either<E, A> = Left<E> | Right<A>
interface Left<E> {
readonly _tag: 'Left'
readonly left: E
}
interface Right<A> {
readonly _tag: 'Right'
readonly right: A
}
export const isLeft = <E, A>(ma: Either<E, A>): ma is Left<E> => ma._tag === 'Left'
export const right = <E = never, A = never>(a: A): Either<E, A> => ({ _tag: 'Right', right: a })
export const map: <A, B>(f: (a: A) => B) => <E>(fa: Either<E, A>) => Either<E, B> = (f) => (fa) =>
isLeft(fa) ? fa : right(f(fa.right))
I ran it through the compiler and got (some extra formatting from me)
"use strict";
exports.__esModule = true;
exports.map = exports.right = exports.isLeft = void 0;
exports.isLeft = function (ma) { return ma._tag === 'Left'; };
exports.right = function (a) { return ({ _tag: 'Right', right: a }); };
exports.map = function (f) {
return function (fa) {
return exports.isLeft(fa)
? fa
: exports.right(f(fa.right))
}
}
So the function is exactly what you'd expect. Return a function that map
s over a value. Although it returns either a Left
or Right
but that is for another discussion on FP which I don't want to have here (learn it, it's awesome and makes your code type safe. Please, for the sanity of good code please learn it and use it).
The generics is what makes this hard to read. It's a side effect of generics. I wish there were a way to make this better but there currently isn't. So let's learn.
What will make this easier to learn is to break it down.
export const map: <A, B>(f: (a: A) => B) => <E>(fa: Either<E, A>) => Either<E, B> = (f) => (fa) =>
isLeft(fa) ? fa : right(f(fa.right))
There are 3 parts (last part is an addendum):
export const map
Nothing extra needs to be said here I hope.
<A, B>(f: (a: A) => B) => <E>(fa: Either<E, A>) => Either<E, B>
This for me is where it got complicated. If it weren't for the output of the TypeScript code to show me what the function actually looked like then I'd have a really difficult time understanding how this worked. The output showed me that the function itself was just (f) => (fa) => 'some value'
.
So after comparing the original TypeScript code and the compiled JavaScript you can see where the division is. It's the =
sign. That's where the type of the function signature ends and the function declaration (function body) begins. I want to discuss that a bit further to help further explain it.
To do that I need to break it down even further. To break it down I will separate everything at each fat arrow (=>
).
There are 2 generics in this section A
, B
and one type.
This is a unary function where it takes one paramater that happens to be a function. To me this is where it gets super complicated as all the :
s and ()
s make it very difficult to read.
The type is (f: (a: A) => B)
. The parameter is f
with the type of function that takes an A
and returns a B
This is a unary function. It defines a generic E
where it takes a single paramater fa
of type Either<E, A>
This is the return type. It returns an Either
of error or B
.
(f) => (fa) => isLeft(fa) ? fa : right(f(fa.right))
A curried function that returns either a Left
or Right
depending on if the predicate fails or not.
In functional programming most functions aren't just unary
they are also curried (when multiple args) to allow for laziness of compulation (allows for setup, easier failures and recovery). So the final part of the function signature is a return value (a function) but when run it does the following
isLeft(fa) ? fa : right(f(fa.right))
I know this is more text but I feel like the signature itself is much easier to read.
type mapSig = <A, B>(f: (a: A) => B) => <E>(fa: Either<E, A>) => Either<E, B>
export const map: mapSig = (f) => (fa) => isLeft(fa) ? fa : right(f(fa.right))
I learned oodles. I hope this process was illuminating to you to do the same if you're stuck on a type signature that you can't wrap your head around at first.
- That spearating the type signature from the implementation makes it signficantly easier to understand (previous heading).
- Compiling it down to regular JS is super helpful
- Taking the code itself and breaking it down (reformatting, indentation et al) makes it easier.
- That the
=
just like regular currying is where you should start