Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the difference between Option and OptionT?

There are two modules in fp-ts:

  1. Option
  2. OptionT

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.

like image 797
Roman Mahotskyi Avatar asked Sep 02 '25 03:09

Roman Mahotskyi


1 Answers

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:

  • ‘Monad transformers’ on the Haskell wiki
  • The usage of monad transformers haskell

Even though they’re about Haskell, the same principles apply to fp-ts.

like image 64
cherryblossom Avatar answered Sep 04 '25 23:09

cherryblossom