Last active
March 28, 2020 23:53
-
-
Save paduc/e908068a080c7f89421c7743338db0f1 to your computer and use it in GitHub Desktop.
Result and Future Monads in Typescript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Prenons pour base le monad Result de paralogs (en un peu simplifié) | |
// | |
interface OkParams<T> { | |
isSuccess: true | |
value: T | |
} | |
interface FailParams<T> { | |
isSuccess: false | |
error: string | |
} | |
type ConstructorParams<T> = OkParams<T> | FailParams<T> | |
class Result<T> { | |
public isSuccess: boolean | |
public error?: string | |
private _val?: T | |
public constructor(params: ConstructorParams<T>) { | |
const { isSuccess } = params | |
this.isSuccess = isSuccess | |
if (params.isSuccess) { | |
this._val = params.value | |
} else { | |
this.error = params.error | |
} | |
Object.freeze(this) | |
} | |
public getOrThrow(): T { | |
if (this.error) { | |
throw new Error( | |
this.error ?? "Can't retrieve the value from a failed result." | |
) | |
} | |
return this._val! | |
} | |
public static ok<U>(value?: U): Result<U> { | |
return new Result<U>({ isSuccess: true, value }) | |
} | |
public static fail<U>(error: string): Result<U> { | |
return new Result<U>({ isSuccess: false, error }) | |
} | |
public map<K>(f: (param: T) => K): Result<K> { | |
return this.flatMap(param => Result.ok<K>(f(param))) | |
} | |
public flatMap<K>(f: (param: T) => Result<K>): Result<K> { | |
if (this.error) return Result.fail(this.error) | |
return f(this.getOrThrow()) | |
} | |
} | |
// Prenons également quelques méthodes imaginaires | |
const makeResult = (arg: string): Result<string> => { | |
if (arg.length > 3) return Result.ok(arg) | |
else return Result.fail('Not long enough') | |
} | |
const makePromise = async (arg: string): Promise<string> => { | |
return new Promise((resolve, reject) => { | |
if (arg === 'John') resolve('Good name !') | |
else reject('I only like the name John...') | |
}) | |
} | |
// | |
// Maintenant place au problème | |
// | |
// Imaginons que nous voulions chainer les deux méthodes précédentes | |
const resProm = makeResult('Henri').map(makePromise) | |
// typeof resProm = Result<Promise> | |
// Nous remarquons qu'il y a 3 cas possibles: | |
// 1) | |
const resProm1 = makeResult('Li').map(makePromise) | |
// --> makeResult fail | |
resProm1.isSuccess // False | |
resProm1.error // 'Not long enough' | |
// 2) | |
const resProm2 = makeResult('John').map(makePromise) | |
// --> makeResult passe | |
resProm2.isSuccess // True | |
// --> makePromise resolve | |
await resProm2.getOrThrow() // "Good name !" | |
// 3) | |
const resProm3 = makeResult('Larry').map(makePromise) | |
// --> makeResult passe | |
resProm3.isSuccess // True (ça commence à sentir mauvais...) | |
// --> makePromise reject | |
await resProm3.getOrThrow() // Throws 'I only like the name John...' | |
// C'est finalement ce troisième cas de figure qui est moche... On a un Result en état Ok mais qui cache une erreur dans sa promise. | |
// Problème A : Result<Promise<T>> a deux cas d'erreur distincts qu'il faut vérifier individuellement | |
// Allons plus loin, en chainant notre Result<Promise> avec une autre méthode qui renvoit un Result: | |
const makeOtherResult = (str: string): Result<string> => { | |
if (str.indexOf('Good') !== -1) return Result.ok('Perfect !') | |
else return Result.fail('Damn!') | |
} | |
const resPromRes = makeResult('Henri') | |
.map(makePromise) | |
.map(msgProm => msgProm.then(msg => makeOtherResult(msg))) | |
// Deux remarques: | |
// 1) On doit chainer en intercalant un then dans le map (pas très joli) | |
// 2) typeof resPromRes = Result<Promise<Result>>> | |
// Maintenant pour extraire les cas d'erreurs, ça devient sportif ! | |
if (resPromRes.isSuccess) { | |
try { | |
const res = await resPromRes.getOrThrow() | |
try { | |
res.getOrThrow() | |
} catch (e) { | |
// makeOtherResult fail nous mène ici | |
} | |
} catch (error) { | |
// makePromise reject nous mène ici | |
} | |
} else { | |
// makeResult fail nous mène ici | |
} | |
// Bref c'est moche | |
// Problème B : Result<Promise<T>> n'est pas chainable | |
// | |
// Parlons SOLUTION | |
// | |
// L'idée est qu'on ne peut pas remonter l'erreur de la promise dans le Result tout en haut. La promise est asynchrone alors que le Result est synchrone. On doit forcément attendre la Promise pour déterminer si elle est en resolve ou reject. | |
// Par contre, tout ce qui est après la promise peut remonter jusqu'à la promise. | |
// L'idée est de traiter le Result en aval : | |
// - s'il est en erreur, alors throw (i.e) transformer le Result.err en Promise.reject, qui saura être chainé avec la Promise qui est en amont | |
// - s'il est en success, alors return une promise de sa valeur, qui saura être chainée avec le Promise qui est en amont | |
const resPromRes2 = makeResult('Henri') | |
.map(makePromise) | |
.map(async msgProm => { | |
const res = await msgProm.then(makeOtherResult) | |
if (!res.isSuccess) throw res.error | |
return res.getOrThrow() | |
}) | |
// typeof resPromRes2 = Result<Promise<T>> Bingo ! | |
// Reste à faire : | |
// - intégrer cette logique d'aplatissement dans un type Future<T> ~ Result<Promise<T>> | |
// - intégrer la logique d'évaluation de l'erreur dans une méthode propre plutot que res.isSuccess | |
// On dirait que Future<T> est un cas particulier de Result<T> mais en réalité, je pense que Result<T> peut être vu comme un cas particulier de Future<T> (une valeur non-promesse est un cas particulier d'une promesse, celui d'une promesse résolue immédiatement) | |
// Ainsi je pense qu'on peut remplacer totalement Result<T> par Future<T> et faire la manoeuvre d'aplatissement à-la resPromRes2 dans map/flatMap quand le callback renvoit une promesse et la manoeuvre "classique" quand le callback renvoit une valeur | |
// map(f: T => U) : Future<U> | |
// map(f: T => Promise<U>) : Future<U> (aplatissement: le promesse en aval remonte dans la promesse interne de Future) | |
// flatMap(f: T => Future<U>) : Future<U> | |
// flatMap(f: T => Promise<Future<U>>) : Future<U> (aplatissement ?) | |
// Je n'ai pas eu le temps de l'implémenter mais seulement de commencer: | |
class Future<T> { | |
private ok: boolean | |
private error?: string | |
private value: Promise<T> | |
public async isSuccess(): Promise<boolean> { | |
if (!this.ok) { | |
// Outer result is an error | |
return false | |
} | |
try { | |
await this.value | |
// All good | |
return true | |
} catch (e) { | |
// Inner promise is rejected | |
return false | |
} | |
} | |
public async getError(): Promise<string> { | |
if (this.error) return this.error | |
try { | |
await this.value | |
throw new Error("Can't retrieve the error from a successful result.") | |
} catch (error) { | |
return error | |
} | |
} | |
public async getOrThrow(): Promise<T> { | |
if (!this.ok) | |
throw new Error("Can't retrieve the value from a failed result.") | |
return this.value | |
} | |
public map(f: T => U): Future<U> { | |
// TODO | |
} | |
public flatMap(f: T => Future<U>): Future<U> { | |
// TODO | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment