There are two modules in fp-ts:
As the Code Conventions chapter says
However usually it means Transformer like in “monad transformers” (e.g. OptionT, EitherT, ReaderT, StateT)
So what is a Transformer? And how do I know what import to use?
Examples are welcome.
CodePudding user response:
Let’s look at what Option
and OptionT
are.
export type Option<A> = None | Some<A>
/** @deprecated */
export interface OptionT<M, A> extends HKT<M, Option<A>> {}
Option
is the base monad we are familiar with, which represents a Some<A>
or None
.
OptionT<M, A>
is identical to HKT<M, Option<A>>
. This interface is actually deprecated — I’ll explain this more later. You can still however use the helper functions in the OptionT
module without using the OptionT
type.
You can think of OptionT
like this (which sadly doesn’t compile due to TypeScript’s lack of higher kinded types):
type OptionT<M, A> = M<Option<A>>
OptionT
is an example of a monad transformer. A monad transformer is a way to combine multiple monads together into a new monad so we can use chain
(and other utility functions).
Example
Let’s look at a (slightly contrived) example.
import * as Console from 'fp-ts/Console'
import * as IO from 'fp-ts/IO'
import * as O from 'fp-ts/Option'
import {pipe} from 'fp-ts/function'
type IOOption<A> = IO.IO<O.Option<A>> // our new monad
// You could think of IOOption<A> as OptionT<IO, A>
const getNumber: IOOption<number> = pipe(
Console.log('getting number'),
IO.map(() => O.some(42))
)
/** Returns None when the number is not divisible by 2. */
const half = (number: number): IOOption<number> =>
pipe(
Console.log('halving number'),
IO.map(() => (number % 2 ? O.none : O.some(number / 2)))
)
How do we put half
and getNumber
together? Initially, you might think of doing something like this:
const bad: IOOption<number> = pipe(getNumber, IO.chain(O.chain(half)))
// ~~~~
// Argument of type '(number: number) => IOOption<number>' is not
// assignable to parameter of type '(a: number) => Option<unknown>'.
However, this doesn’t work. O.chain
accepts a function that returns an Option
, not an IOOption
. Additionally, IO.chain
accepts a function that returns an IO
, but O.chain
returns a function that returns an Option
. (For more information you could read up on how monads don’t compose.)
The correct way to do this is:
const good = pipe(
getNumber,
IO.chain(O.fold(
// If we get a None, return IO None
() => IO.of(O.none),
// If we get a Some(number), return half(number)
half
))
)
This seems fine for this small example, but you can imagine this getting a little unwieldy when you chain more and more functions together:
pipe(
foo,
IO.chain(O.fold(() => IO.of(O.none), bar)),
IO.chain(O.fold(() => IO.of(O.none), baz)),
IO.chain(O.fold(() => IO.of(O.none), qux))
)
Fortunately, we have the OptionT
module. Using the chain
from this module, we can remove the IO.chain(O.fold(() => IO.of(O.none), ...))
boilerplate:
import * as OT from 'fp-ts/lib/OptionT'
const better = pipe(getNumber, OT.chain(IO.Monad)(half))
const ioOptionChain = OT.chain(IO.Monad)
const alsoBetter = pipe(getNumber, ioOptionChain(half))
The type of OT.chain
is this:
// chain :: Monad m => (a -> m (Option b)) -> m (Option a) -> m (Option b)
export declare function chain<M>(
M: Monad<M>
): <A, B>(f: (a: A) => HKT<M, Option<B>>) => (ma: HKT<M, Option<A>>) => HKT<M, Option<B>>
// a bunch of other overloads omitted for brevity
You may have noticed that the approach we used with good
can be generalised to any monad M
:
M.chain(O.fold(() => M.of(O.none), fn))
This is why OT.chain
accepts a M: Monad<M>
: a monad instance such as IO.Monad
, Task.Monad
, Either.Monad
, etc.
Do I use Option
or OptionT
?
Use Option
if you’re only dealing with just Option
. Use OptionT
if the Option
is wrapped in some other type like IO
, Task
, Array
, Either
, etc.
Why OptionT
is deprecated
Recall that OptionT<M, A>
is equivalent to HKT<M, Option<A>>
. While you’ll see HKT
pop up in overloads for some utility functions, you probably won’t use it in your own types. For example, our IOOption<A>
isn’t HKT<'IO', A>
and is instead equivalent to Kind<'IO', Option<A>>
.
However, Kind
only works for monads with 1 type parameter. For more type parameters, there’s Kind2
(e.g. for Either
), Kind3
(e.g. for ReaderEither
), and Kind4
(e.g. for StateReaderTaskEither
).
This is why there’s so many overloads for OT.chain
and other similar functions:
export declare function chain<M extends URIS4>(M: Monad4<M>): <A, S, R, E, B>(f: (a: A) => Kind4<M, S, R, E, Option<B>>) => (ma: Kind4<M, S, R, E, Option<A>>) => Kind4<M, S, R, E, Option<B>>
export declare function chain<M extends URIS3>(M: Monad3<M>): <A, R, E, B>(f: (a: A) => Kind3<M, R, E, Option<B>>) => (ma: Kind3<M, R, E, Option<A>>) => Kind3<M, R, E, Option<B>>
export declare function chain<M extends URIS3, E>(M: Monad3C<M, E>): <A, R, B>(f: (a: A) => Kind3<M, R, E, Option<B>>) => (ma: Kind3<M, R, E, Option<A>>) => Kind3<M, R, E, Option<B>>
export declare function chain<M extends URIS2>(M: Monad2<M>): <A, E, B>(f: (a: A) => Kind2<M, E, Option<B>>) => (ma: Kind2<M, E, Option<A>>) => Kind2<M, E, Option<B>>
export declare function chain<M extends URIS2, E>(M: Monad2C<M, E>): <A, B>(f: (a: A) => Kind2<M, E, Option<B>>) => (ma: Kind2<M, E, Option<A>>) => Kind2<M, E, Option<B>>
export declare function chain<M extends URIS>(M: Monad1<M>): <A, B>(f: (a: A) => Kind<M, Option<B>>) => (ma: Kind<M, Option<A>>) => Kind<M, Option<B>>
export declare function chain<M>(M: Monad<M>): <A, B>(f: (a: A) => HKT<M, Option<B>>) => (ma: HKT<M, Option<A>>) => HKT<M, Option<B>>
The overload we actually used in OT.chain(IO.Monad)
is the one with Kind
in it (second last one).
This is why it doesn’t really make that much sense to use the OptionT
type when it can only be used for the last overload with HKT
.
If you would like to learn more about monad transformers:
Even though they’re about Haskell, the same principles apply to fp-ts.
CodePudding user response:
A Monad Transformer is a concept from functional programming in general. I'll defer to the Wikipedia definition because it seems pretty precise:
In functional programming, a monad transformer is a type constructor which takes a monad as an argument and returns a monad as a result.
Monad transformers can be used to compose features encapsulated by monads – such as state, exception handling, and I/O – in a modular way. Typically, a monad transformer is created by generalising an existing monad; applying the resulting monad transformer to the identity monad yields a monad which is equivalent to the original monad (ignoring any necessary boxing and unboxing).
-- Wikipedia
To help illustrate why this might be a nice set of utilities to create, let's look at the match
definition from the OptionT
module.
// It is overloaded a few time to support Functors with multiple type parameters
// but this is the simplest
export declare function match<F>(
F: Functor<F>
): <B, A>(onNone: () => B, onSome: (a: A) => B) => (ma: HKT<F, Option<A>>) => HKT<F, B>
You would use this function if you have a Functor
and want to be able to handle the case where that functor holds an Option
value just a single time.
For example, if your Functor
were an Array
then you could use match
like
import * as A from 'fp-ts/Array';
import * as O from 'fp-ts/Option';
import { match } from 'fp-ts/OptionT';
import { pipe } from 'fp-ts/function';
const defaultTo0 = match(
A.Functor,
)(
() => 0,
(a: number) => a,
);
const sum = pipe(
[O.some(1), O.some(2), O.none, O.some(3)],
defaultTo0,
A.reduce(0, (acc, c) => acc c),
);
console.log(sum); // 6
In this case, we use the match
function from the option transformer module to create a helper that converts Array<Option<number>>
into Array<number>
. Monad Transformers are pretty broad subject so it's hard to say, "this is exactly when you use one" in more definite terms than what the Wikipedia article is saying. Hopefully this example provides a small example of how it might make managing multiple monadic layers a bit less tedious?