Lets say we have the following simple function:
const even = (a: number) => {
if (a % 2 === 0) {
return a
}
throw new Error(`value: ${a} is even`);
}
as you can see if the value is even we return the number else we throw an error. The normal way of handling this would be to wrap it inside a try-catch and handle the error. But when you have multiple functions this becomes tedious and error-prone.
In the fp world instead of throwing errors all over the place, we convert them to data. This in typescript would look something like this:
type Try<A> = Success<A> | Failure
where Success is a container that wraps some value of type A and Failure is the alternative one that contains the error.
Let’s try to implement this structure! We will use a mix of OOP and FP, to create nicely chainable data structures. First, we will create an abstract class that will contain the types of our methods that our data structure will define
abstract class Try<A> {
abstract map<B>(this: Try<A>, fn: (a: A) => B): Try<B>
abstract chain<B>(this: Try<A>, fn: (a: A) => Try<B>): Try<B>;
}
We defined two abstract methods: map and chain. The map
function acts the same as it does in Array with one element. It takes the value and applies a function over it then "magically" it will be packed back into the array.
Basically:
[1].map(x => `${x}`).
In types:
Array<Int> => (fn: (a: Int) => string) => Array<string>
The chain
method is a bit more complicated. While the map just takes out the value, applies a function over it, and packs it back, with a chain it is your responsibility to pack it.
Our analog in an array of one element would be the following:
[1].flatMap(x => [`${x * 2}`]).
In types:
Array<Int> => (fn: (a: Int) => Array<string>) => Array<string>
You may ask why is this chain
method useful anyway? As its name suggests we will use it to chain together multiple functions.
Before we continue implementing Success we need to introduce the concept of laziness.
The function arguments in javascript are called by value. What does this mean?
For example:
function a(arg) {
console.log(“hi”)
}
function b(a, b) {
throws new Error(“Hello From the other side” + a + b)
}
a(b(1, 2))
What will happen here? Even if the argument of the function a
was not used we still executed the function b
and an error was thrown. How can we solve this? What if we would want to call it inside of a
?
Here our some helpers that solves our issue:
// pass as thunk
type $<A> = () => A
function $<A>(a: A): $<A>{
return () => a
}
// make function lazy
function $fn<A extends (...args: any) => any>(a: A): ((...p: Parameters<A>) => $<ReturnType<A>>) {
return (...p: [Parameters<A>]) => () => {
return a(...p)
}
}
Next let us create a Success that extends Try
class Success<A> extends Try<A> {
value: A
constructor(a: A) {
super()
this.value = a;
}
static of<A>(value: A): Try<A> { return new Success(value); }
map<B>(fn: (a: A) => B): Try<B> {
const val = this.value;
const x = $fn(fn)(val);
return mkTry<B>(x)
}
chain<B>(fn: (a: A) => Try<B>): Try<B> {
return fn(this.value);
}
}
And Failure:
class Failure<A> extends Try<A> {
value: Error;
constructor(e: Error) {
super();
this.value = e;
}
static of<A>(value: Error): Try<A> { return new Failure(value); }
map<B>(fn: (a: A) => B): Try<B> {
return Failure.of(this.value);
}
chain<B>(fn: (a: A) => Try<B>): Try<B> {
return Failure.of(this.value)
}
}
Lets make a helper function to make our life easier:
function mkTry<A>(value: $<A>): Try<A> {
try {
const a = value();
return Success.of(a)
} catch (error) {
return Failure.of(error as Error)
}
}
With this in place now we have a mechanism to safely chain and execute our code.
Example:
// // define a simple function that throws an error
const x = (a: number) => {
if (a % 2 === 0) {
return a
}
throw new Error(`value: ${a} is even`);
}
const div4 = (a: number) => {
if (a % 4 === 0) {
return a
}
throw new Error(`value: ${a} is not dividible by 4`);
}
// make it lazy
const lazyX = $fn(x)
const lazyDiv = $fn(div4)
// // wrap inside try, map chain
const a1 = Try.lift(12)
.chain(a => mkTry(lazyX(a)))
.map(b => (b * 2) - 4)
.chain(a => mkTry(lazyDiv(a)));
console.log(a1);
In this example we defined some functions that can throw an error, then we made them lazy and wrapped them with Try using mkTry data constructor.
Hope this helped a bit :) For any question leave a comment.
For full implementation see: https://gist.github.com/lupuszr/f73baa7ef559f77e6e847ba75ed97182
Top comments (0)