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.