Last active
January 16, 2024 12:58
-
-
Save gcanti/453e5419fbcabe078d933ab21f0df8bf to your computer and use it in GitHub Desktop.
TypeScript port of the second half of John De Goes "FP to the max" (https://www.youtube.com/watch?v=sxudIMiOo68)
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
import { log } from 'fp-ts/lib/Console' | |
import { Type, URIS } from 'fp-ts/lib/HKT' | |
import { none, Option, some } from 'fp-ts/lib/Option' | |
import { randomInt } from 'fp-ts/lib/Random' | |
import { fromIO, Task, task, URI as TaskURI } from 'fp-ts/lib/Task' | |
import { createInterface } from 'readline' | |
// | |
// helpers | |
// | |
const getStrLn: Task<string> = new Task( | |
() => | |
new Promise(resolve => { | |
const rl = createInterface({ | |
input: process.stdin, | |
output: process.stdout | |
}) | |
rl.question('> ', answer => { | |
rl.close() | |
resolve(answer) | |
}) | |
}) | |
) | |
const putStrLn = (message: string): Task<void> => fromIO(log(message)) | |
const parse = (s: string): Option<number> => { | |
const i = +s | |
return isNaN(i) || i % 1 !== 0 ? none : some(i) | |
} | |
// | |
// type classes | |
// | |
interface ProgramSyntax<F extends URIS, A> { | |
map: <B>(f: (a: A) => B) => _<F, B> | |
chain: <B>(f: (a: A) => _<F, B>) => _<F, B> | |
} | |
type _<F extends URIS, A> = Type<F, A> & ProgramSyntax<F, A> | |
interface Program<F extends URIS> { | |
finish: <A>(a: A) => _<F, A> | |
} | |
interface Console<F extends URIS> { | |
putStrLn: (message: string) => _<F, void> | |
getStrLn: _<F, string> | |
} | |
interface Random<F extends URIS> { | |
nextInt: (upper: number) => _<F, number> | |
} | |
interface Main<F extends URIS> extends Program<F>, Console<F>, Random<F> {} | |
// | |
// instances | |
// | |
const programTask: Program<TaskURI> = { | |
finish: task.of | |
} | |
const consoleTask: Console<TaskURI> = { | |
putStrLn, | |
getStrLn | |
} | |
const randomTask: Random<TaskURI> = { | |
nextInt: upper => fromIO(randomInt(1, upper)) | |
} | |
// | |
// game | |
// | |
const checkContinue = <F extends URIS>(F: Program<F> & Console<F>) => (name: string): _<F, boolean> => | |
F.putStrLn(`Do you want to continue, ${name}?`) | |
.chain(() => F.getStrLn) | |
.chain(answer => { | |
switch (answer.toLowerCase()) { | |
case 'y': | |
return F.finish(true) | |
case 'n': | |
return F.finish(false) | |
default: | |
return checkContinue(F)(name) | |
} | |
}) | |
const gameLoop = <F extends URIS>(F: Main<F>) => (name: string): _<F, void> => | |
F.nextInt(5).chain(secret => | |
F.putStrLn(`Dear ${name}, please guess a number from 1 to 5`) | |
.chain(() => | |
F.getStrLn.chain(guess => | |
parse(guess).fold(F.putStrLn('You did not enter an integer!'), x => | |
x === secret | |
? F.putStrLn(`You guessed right, ${name}!`) | |
: F.putStrLn(`You guessed wrong, ${name}! The number was: ${secret}`) | |
) | |
) | |
) | |
.chain(() => checkContinue(F)(name)) | |
.chain(shouldContinue => (shouldContinue ? gameLoop(F)(name) : F.finish(undefined))) | |
) | |
const main = <F extends URIS>(F: Main<F>): _<F, void> => { | |
return F.putStrLn('What is your name?') | |
.chain(() => F.getStrLn) | |
.chain(name => F.putStrLn(`Hello, ${name} welcome to the game!`).chain(() => gameLoop(F)(name))) | |
} | |
const mainTask = main({ ...programTask, ...consoleTask, ...randomTask }) | |
// mainTask.run() | |
// | |
// tests | |
// | |
import { drop, snoc } from 'fp-ts/lib/Array' | |
class TestData { | |
constructor(readonly input: Array<string>, readonly output: Array<string>, readonly nums: Array<number>) {} | |
putStrLn(message: string): [TestData, void] { | |
return [new TestData(this.input, snoc(this.output, message), this.nums), undefined] | |
} | |
getStrLn(): [TestData, string] { | |
return [new TestData(drop(1, this.input), this.output, this.nums), this.input[0]] | |
} | |
nextInt(upper: number): [TestData, number] { | |
return [new TestData(this.input, this.output, drop(1, this.nums)), this.nums[0]] | |
} | |
} | |
const TestTaskURI = 'TestTask' | |
type TestTaskURI = typeof TestTaskURI | |
declare module 'fp-ts/lib/HKT' { | |
interface URI2HKT<A> { | |
TestTask: TestTask<A> | |
} | |
} | |
class TestTask<A> { | |
readonly _A!: A | |
readonly _URI!: TestTaskURI | |
constructor(readonly run: (data: TestData) => [TestData, A]) {} | |
map<B>(f: (a: A) => B): TestTask<B> { | |
return new TestTask(data => { | |
const [data2, a] = this.run(data) | |
return [data2, f(a)] | |
}) | |
} | |
chain<B>(f: (a: A) => TestTask<B>): TestTask<B> { | |
return new TestTask(data => { | |
const [data2, a] = this.run(data) | |
return f(a).run(data2) | |
}) | |
} | |
} | |
const of = <A>(a: A): TestTask<A> => new TestTask(data => [data, a]) | |
const programTestTask: Program<TestTaskURI> = { | |
finish: of | |
} | |
const consoleTestTask: Console<TestTaskURI> = { | |
putStrLn: (message: string) => new TestTask(data => data.putStrLn(message)), | |
getStrLn: new TestTask(data => data.getStrLn()) | |
} | |
const randomTestTask: Random<TestTaskURI> = { | |
nextInt: upper => new TestTask(data => data.nextInt(upper)) | |
} | |
const mainTestTask = main({ ...programTestTask, ...consoleTestTask, ...randomTestTask }) | |
const testExample = new TestData(['Giulio', '1', 'n'], [], [1]) | |
import * as assert from 'assert' | |
assert.deepEqual(mainTestTask.run(testExample), [ | |
new TestData( | |
[], | |
[ | |
'What is your name?', | |
'Hello, Giulio welcome to the game!', | |
'Dear Giulio, please guess a number from 1 to 5', | |
'You guessed right, Giulio!', | |
'Do you want to continue, Giulio?' | |
], | |
[] | |
), | |
undefined | |
]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment