Reader

In FP-TS, a Reader is defined as follows:

interface Reader<R, A> {
  (r: R): A
}
Copy the code

That is, a function of type r -> a, where r can be seen as the environment required for the calculation, and A is the result of the calculation. It is often used for dependency injection. Let’s start with some code:

const f = (b: boolean) :string= > b ? 'true' : 'false';

const g = (n: number) :string= > f(n > 2);

const h = (s: string) :string= > g(s.length + 1)

console.log(h('abc')) // 'true'
Copy the code

These are three ordinary functions, nothing special about them. Now suppose we want f to be more international, such that when a user from China visits, f should return “yes” and “no”. So, we add a parameter to f:

interface Dependencies {
  i18n: {
    true: string.false: string,}}const f = (b: boolean.deps: Dependencies): string= > b ? deps.i18n.true : deps.i18n.false;
Copy the code

However, this causes the g compilation to fail because g does not pass deps after calling f. Thus, we must add a parameter to g:

const g = (n: number.deps: Dependencies): string= > f(n > 2, deps);
Copy the code

Again, this will cause the compilation of h to fail, and then we need to add a parameter to h as well:

const h = (s: string.deps: Dependencies): string= > g(s.length + 1, deps)
Copy the code

After that, we can finally pass deps to h and get the desired result:

console.log(h("356", { i18n: { true: "Yes".false: "No." "}}))Copy the code

This result is not satisfactory, g and H do not use the dePS passed in, but in order to use F, they must get the DEPS.

So is there a better solution? Let’s try to move the Dependencies parameter out as a separate parameter.

const f =
  (b: boolean) :((deps: Dependencies) => string) = >
  (deps) = >
    b ? deps.i18n.true : deps.i18n.false;

const g =
  (n: number) :((deps: Dependencies) => string) = > f(n > 2);

const h =
  (s: string) :((deps: Dependencies) => string) = > g(s.length + 1);
Copy the code

If (deps: Dependencies) => string
,>

const f =
  (b: boolean): Reader<Dependencies, string> = >(deps) = >
    b ? deps.i18n.true : deps.i18n.false;

const g = (n: number): Reader<Dependencies, string> => f(n > 2);

const h = (s: string): Reader<Dependencies, string> => g(s.length + 1);
Copy the code

Suppose we don’t want to hard code our lower bound of length (that is, 2 of n > 2) and instead inject the lower bound into G. First, we add an item to Dependencies:

interface Dependencies {
  i18n: {
    true: string.false: string}; lowerBound:number;
}
Copy the code

Later, we want to read lowerBound from Dependencies, but we don’t want to write code like this:

const g = (n: number): Reader<Dependencies, string> = >(deps) = > f(n > deps.lowerBound)(deps);
Copy the code

It’s gonna undo everything we’ve worked for. Reader’s ask method helps you read lowerBound from Dependencies without actually getting the deps:

const g = (n: number): Reader<Dependencies, string> => pipe(
  ask<Dependencies>(),
  chain(deps => f(n > deps.lowerBound))
)
Copy the code

The implementation of ask is as follows:

const ask: <R>() = > Reader<R, R> = () = > identity
Copy the code

G

First, ask () returns Reader

. The chain accepts the parameter type Dependencies -> Reader

, and the return type is indeed Reader

.
,>
,>
,>
,>

The most interesting thing about Reader is that, like Either, it is kind * -> * -> *, which means that we cannot create a Functor instance for Reader r, but only for Reader R. The corresponding map function type is:

map :: (a -> b) -> Reader r a -> Reader r b
Copy the code

Reader r -> a Reader r -> a

(a -> b) -> (r -> a) -> (r -> b)
Copy the code

For the compose function definition:

compose :: (b -> c) -> (a -> b) -> (a -> c)
compose g f = \x -> g(f(x))
Copy the code

You’ll see that the map of Reader r or (->) r is the composition.

Writer

Writer in FP-TS is defined as follows:

interface Writer<W, A> {
  (): [A, W]
}
Copy the code

That is, a function that returns a tuple. Suppose we have a function like this:

const isHeavy = (weight: number) = > weight > 80;
Copy the code

This function only returns a Boolean, but we want the return value to tell us something extra, so we modify the return value:

const isHeavy =
  (weight: number): W.Writer<string.boolean> = >() = >
    [weight > 80."Compare with 80kg."];
Copy the code

This way, we know more context than a dry Boolean value. But this also creates a problem, if we also have a function like this:

const getWeight =
  (user: User): W.Writer<string.number> = >() = >
    [user.weight, "Get your weight."];
Copy the code

The return value of getWeight cannot be passed directly to isHeavy because isHeavy requires a plain number, whereas getWeight returns a value with context attached. However, chain can be evaluated in context, so we’ll use it again. Fp-ts Writer does not provide a Monad instance directly. We need to create it ourselves using getMonad:

import * as W from "fp-ts/Writer";

const monad = W.getMonad(monoidString);
Copy the code

GetMonad requires a Monoid instance, because the first type Writer accepts is not necessarily string, but Array string, etc. The only limitation is that they need instances that can provide monoids. So what does this Monoid typeclass mean? You may not be familiar with the English name Monoid, but you may have heard of Monoid in Chinese.

Monoid, in the branch of mathematics known as abstract algebra, is an algebraic structure with associable binary operations and identity elements.

Simply put, monoid is a semigroup with identity element. In the context of programming:

interface Semigroup<A> {
  readonly concat: (x: A, y: A) = > A
}

interface Monoid<A> extends Semigroup<A> {
  readonly empty: A
}
Copy the code

In other words, we need to define A associative binary operation concat for A. For strings, this means that concat(concat(a, b), c) = concat(a, concat(b, c)) for any three strings a, B, or C. Operator + satisfies our requirements and is written as a function:

const concat: (a: string, b:string) = > string = (a, b) = > a + b;
Copy the code

The identity element means that we need to find a string e such that any string A has concat(e, a) = concat(a, e). E can select an empty string. So the triplet

forms a semi-unitary group. Other examples of semi-unitary groups are

, < Boolean, &&, true>, etc.
,>
,>

After passing getMonad, we get a Monad instance. We can then use chain to do a context-dependent operation:

// Use execute to get accumulated additional information
W.execute(chain(getWeight({ weight: 75 }), isHeavy)) // Take the weight. Compare with 80kg.

Use evaluate to get the result
W.evaluate(chain(getWeight({ weight: 81 }), isHeavy)) // true
Copy the code

State

In FP-TS, State is defined as follows:

interface State<S, A> {
  (s: S): [A, S]
}
Copy the code

Since a pure function cannot access external state, state needs to be passed in as an argument in functional programming, and the function then returns not only the result computed from state, but also the new state. An example of a pseudo random number generator (PRNG) : The State Monad.