Home > Back-end >  What is the difference between Option and OptionT?
What is the difference between Option and OptionT?

Time:05-29

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.

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?

  • Related