Skip to content

Instantly share code, notes, and snippets.

@paduc
Last active March 28, 2020 23:53
Show Gist options
  • Save paduc/e908068a080c7f89421c7743338db0f1 to your computer and use it in GitHub Desktop.
Save paduc/e908068a080c7f89421c7743338db0f1 to your computer and use it in GitHub Desktop.
Result and Future Monads in Typescript
//
// 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