fp-ts
fp-ts copied to clipboard
Alternative do notation implementation
🚀 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.
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)
})
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.
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)
)
}
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
@apoberejnyi Thank you for the post! The idea explained is simple enough to test it in one of my projects.
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?