fp-ts icon indicating copy to clipboard operation
fp-ts copied to clipboard

Alternative do notation implementation

Open apoberejnyi opened this issue 3 years ago • 6 comments

🚀 Feature proposal

There's a technique to emulate type-safe do notation using exceptions internally.

Short example:

// note: another lib for Either is used
function getFormattedName(): Either<string, string> {
  return doEither((run) => {
    // type of `getNamePrefix()` is Either<string, string>
    const prefix: string = run(getNamePrefix());
    const firstName = run(getFirstName());
    const lastName = run(getLastName());
    return Right(prefix + " " + firstName + " " + lastName);
  });
}

A summary with more examples and implementation is available in the following article: https://apoberejnyi.medium.com/do-notation-for-either-in-typescript-c207e5987b7a.

I hope that this idea might be useful for the project.

apoberejnyi avatar Jan 21 '21 19:01 apoberejnyi

I agree the current Do notation is verbose and unnatural (though is better than the fp-ts-contrib one). I really miss a Do notation in TypeScript, but still avoid using it because of the unnatural style (both as a function programmer and as a TypeScript programmer).

I really think we should find a solution that will allow us to break-free from the pipe (in the rare cases we want to), either by somehow utilise an existing language feature (e.g. await), or by a wrapping function (as OP suggested).

Regarding the OP solution, I think it's the first chance to utilise one of the allowed symbols in TypeScript: $ and/or _. While symbols can be unreadable, here they supposed to make it easer to do a repeated task.

/** native look, I'm not sure it is acheivable */
const getFormattedName = () =>
  E.do(async () => {
    const prefix = await getNamePrefix()
    const firstName = await getFirstName()
    const lastName = await getLastName()
    return E.of(prefix + ' ' + firstName + ' ' + lastName)
  })


/** functions and symboles */
const getFormattedName = () =>
  E.do(() => {
    const prefix = E.$(getNamePrefix())
    const firstName = E.$(getFirstName())
    const lastName = E.$(getLastName())
    return E.of(prefix + ' ' + firstName + ' ' + lastName)
  })

SRachamim avatar Jan 22 '21 08:01 SRachamim

I love to have a Do Notation but the medium post will throw exception and then will collect it in try catch, it's cannot be done for reader, or other monadics.

and I use the current Do notation that each data type provide in fp-ts not in fp-ts-contrib.

mohaalak avatar Jan 22 '21 08:01 mohaalak

side by side

import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'

declare const getNamePrefix: () => E.Either<string, string>
declare const getFirstName: () => E.Either<string, string>
declare const getLastName: () => E.Either<string, string>

/*
function getFormattedName(): E.Either<string, string> {
  return doEither((run) => {
    const prefix: string = run(getNamePrefix())
    const firstName = run(getFirstName())
    const lastName = run(getLastName())
    return E.right(prefix + ' ' + firstName + ' ' + lastName)
  })
}
*/

function getFormattedName(): E.Either<string, string> {
  return pipe(
    E.Do,
    E.bind('prefix', () => getNamePrefix()),
    E.bind('firstName', () => getFirstName()),
    E.bind('lastName', () => getLastName()),
    E.map(({ prefix, firstName, lastName }) => prefix + ' ' + firstName + ' ' + lastName)
  )
}

gcanti avatar Jan 22 '21 09:01 gcanti

There is a native-looking way by exploiting generators, I go in details about this in the following article: https://dev.to/matechs/abusing-typescript-generators-4m5h

The advantage compared to a pipe-based one is you are able to use yield pretty much everywhere and use plain control-flow primitives like if/else, switch-case, while, etc.

That said many times I use the pipe variant which I find rather pleasant

mikearnaldi avatar Jan 23 '21 16:01 mikearnaldi

@apoberejnyi Thank you for the post! The idea explained is simple enough to test it in one of my projects.

alarbada avatar Mar 23 '21 11:03 alarbada

I really like this, and it solves a lot issues with the current do notation (https://github.com/gcanti/fp-ts/issues/1524 , if/else, etc). Generators would be better, but IIRC the yielded type has to be the same for every yield in Typescript?

kalda341 avatar Oct 08 '21 20:10 kalda341