Skip to content

Instantly share code, notes, and snippets.

@mdubourg001
Last active February 25, 2020 14:41
Show Gist options
  • Save mdubourg001/f30c4feec3a97d04e9efbaa7a781c8d6 to your computer and use it in GitHub Desktop.
Save mdubourg001/f30c4feec3a97d04e9efbaa7a781c8d6 to your computer and use it in GitHub Desktop.
Typescript IO Monad Implementation
abstract class Monad<T> {
protected value: T;
public abstract map(f: Function): Monad<T>;
public abstract flatMap(f: Function): Monad<T>;
}
type Effect<T> = () => T;
class IO<T> extends Monad<Effect<T>> {
constructor(val: Effect<T>) {
super();
this.value = val;
}
static of<T>(val: Effect<T>): IO<T> {
return new IO(val);
}
map<B>(f: (val: T) => T | B): IO<T | B> {
return new IO(() => f(this.eval()));
}
asyncMap<B>(f: (val: T) => Promise<T | B>): IO<Promise<T | B>> {
return new IO(async () => f(await this.eval()));
}
flatMap<B>(f: (val: T) => IO<T | B>): IO<T | B> {
return new IO(() => f(this.eval()).eval());
}
asyncFlatMap<B>(f: (val: T) => IO<Promise<T | B>>): IO<Promise<T | B>> {
return new IO(async () => f(await this.eval()).eval());
}
eval(): T {
return this.value();
}
toString(): string {
return `IO ${typeof this.value}`;
}
}
// -----
// utils
// -----
const log = (...args: any[]) => console.log(`=> ${args.join(' ')}`);
const tapLog = (x: any): any => {
log(x);
return x;
};
const randBetween = (...args: Array<any>): any => {
const rand = Math.random();
const step = 1 / args.length;
return args.find((a, index) => {
if (rand >= index * step && rand < (index + 1) * step) return a;
});
};
// -----
// examples
// -----
// -----
// sync usage
// -----
const readdirSyncIO = (...args: any[]) => IO.of(() => fs.readdirSync(...args));
const readFileSyncIO = (...args: any[]) =>
IO.of(() => fs.readFileSync(...args));
const writeFileSyncIO = (...args: any[]) =>
IO.of(() => fs.writeFileSync(...args));
const countAndWriteFilesWithExt = (ext: string): IO<string> =>
readdirSyncIO('./')
.map(
files => files.filter((filename: any) => filename.endsWith(ext)).length,
)
.flatMap(count => writeFileSyncIO(`count.txt`, count.toString(), 'utf8'))
.flatMap(() => readFileSyncIO(`count.txt`, 'utf8'));
log(countAndWriteFilesWithExt('.json').eval());
// -----
// async usage with error handling
// -----
const unsafeFetchDataIO = () =>
IO.of(
randBetween(
() => tapLog('1 worked') && Promise.resolve({ status: 200, data: 10 }),
() =>
tapLog('1 failed') &&
Promise.reject({ status: 500, error: 'Internal server error.' }),
),
); // same chances to fail or succeed ;
const unsafeFetchMoreDataIO = () =>
IO.of(
randBetween(
() =>
tapLog('2 worked') &&
Promise.resolve({ status: 200, data: 'Am some API data' }),
() =>
tapLog('2 failed') &&
Promise.reject({ status: 500, error: 'Internal server error.' }),
),
); // same chances to fail or succeed ;;
const asyncTasksMess = unsafeFetchDataIO()
.asyncMap(res => ({ ...res, data: res.data * 2 }))
.asyncFlatMap((data: any) =>
writeFileSyncIO(`data.txt`, JSON.stringify(data), 'utf8'),
)
.asyncFlatMap(() => readFileSyncIO(`data.txt`, 'utf8'))
.asyncMap(log) // => { status: 200, data: 20 }
.asyncFlatMap(() => unsafeFetchMoreDataIO())
.asyncFlatMap((data: any) =>
writeFileSyncIO(`data.txt`, JSON.stringify(data), 'utf8'),
)
.asyncFlatMap(() => readFileSyncIO(`data.txt`, 'utf8'));
asyncTasksMess
.eval()
.then(log) // => { status: 200, data: "Am some API data" }
.catch(e => log(e.error)); // => Internal server error.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment