Skip to content

Instantly share code, notes, and snippets.

@gcanti
Last active January 16, 2024 12:58
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save gcanti/453e5419fbcabe078d933ab21f0df8bf to your computer and use it in GitHub Desktop.
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)
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