Closed danielepolencic closed 7 years ago
A first attempt
// adapted from http://okmij.org/ftp/Computation/free-monad.html
import { HKT, HKTS } from 'fp-ts/lib/HKT'
import { FantasyMonad, StaticMonad } from 'fp-ts/lib/Monad'
export type NaturalTransformation<F extends HKTS, G extends HKTS> = <A>(fa: HKT<A>[F]) => HKT<A>[G]
declare module 'fp-ts/lib/HKT' {
interface HKT<A> {
Free: Free<any, A>
}
}
export const URI = 'Free'
export type URI = typeof URI
export type Free<F extends HKTS, A> = Pure<F, A> | Impure<F, A, any>
export class Pure<F extends HKTS, A> implements FantasyMonad<URI, A> {
static of = of
readonly _tag: 'Pure'
readonly _F: F
readonly _A: A
readonly _URI: URI
constructor(public readonly a: A) {}
map<B>(f: (a: A) => B): Free<F, B> {
return new Pure<F, B>(f(this.a))
}
of<B>(b: B): Free<F, B> {
return of<F, B>(b)
}
ap<B>(fab: Free<F, (a: A) => B>): Free<F, B> {
return fab.chain(f => this.map(f)) // <- derived
}
chain<B>(f: (a: A) => Free<F, B>): Free<F, B> {
return f(this.a)
}
foldMap<M extends HKTS>(monad: StaticMonad<M>, f: NaturalTransformation<F, M>): HKT<A>[M] {
return monad.of(this.a)
}
}
export class Impure<F extends HKTS, A, X> implements FantasyMonad<URI, A> {
static of = of
readonly _tag: 'Impure'
readonly _F: F
readonly _A: A
readonly _X: X
readonly _URI: URI
constructor(public readonly fx: HKT<X>[F], public readonly f: (x: X) => Free<F, A>) {}
map<B>(f: (a: A) => B): Free<F, B> {
return new Impure<F, B, X>(this.fx, x => this.f(x).map(f))
}
of<B>(b: B): Free<F, B> {
return of<F, B>(b)
}
ap<B>(fab: Free<F, (a: A) => B>): Free<F, B> {
return fab.chain(f => this.map(f)) // <- derived
}
chain<B>(f: (a: A) => Free<F, B>): Free<F, B> {
return new Impure<F, B, X>(this.fx, x => this.f(x).chain(f))
}
foldMap<M extends HKTS>(monad: StaticMonad<M>, f: NaturalTransformation<F, M>): HKT<A>[M] {
return monad.chain<X, A>((x: X) => this.f(x).foldMap(monad, f), f<X>(this.fx))
}
}
export function of<F extends HKTS, A>(a: A): Free<F, A> {
return new Pure<F, A>(a)
}
export function liftFree<F extends HKTS, A>(fa: HKT<A>[F]): Free<F, A> {
return new Impure<F, A, A>(fa, of)
}
Usage
//
// Adapted from http://typelevel.org/cats/datatypes/freemonad.html
//
// Create an ADT representing your grammar
import { Option, fromNullable } from 'fp-ts/lib/Option'
declare module 'fp-ts/lib/HKT' {
interface HKT<A> {
KVStoreA: KVStoreA<A>
}
}
export class KVStoreA<A> {
_A: A
}
class Put<T> extends KVStoreA<void> {
constructor(public readonly key: string, public readonly value: T) {
super()
}
}
class Get<T> extends KVStoreA<Option<T>> {
constructor(public readonly key: string) {
super()
}
}
class Delete extends KVStoreA<void> {
constructor(public readonly key: string) {
super()
}
}
//
// Free your ADT
//
// Create a Free type based on your ADT
type KVStore<A> = Free<'KVStoreA', A>
// Create smart constructors using liftF
function put<T>(key: string, value: T): KVStore<void> {
return liftFree<'KVStoreA', void>(new Put(key, value))
}
function get<T>(key: string): KVStore<Option<T>> {
return liftFree<'KVStoreA', Option<T>>(new Get<T>(key))
}
function del(key: string): KVStore<void> {
return liftFree<'KVStoreA', void>(new Delete(key))
}
// Update composes get and set, and returns nothing
function update<T>(key: string, f: (t: T) => T): KVStore<void> {
return get<T>(key).chain(o => o.map(v => put<T>(key, f(v))).getOrElse(() => of<'KVStoreA', void>(undefined)))
}
// Build a program
// program :: Free<"KVStoreA", Option<number>>
const program = put('wild-cats', 2)
.chain(() => update('wild-cats', (n: number) => n + 12))
.chain(() => put('tame-cats', 5))
.chain(() => get<number>('wild-cats'))
.chain(n => del('tame-cats').map(() => n))
//
// Write a compiler for your program
//
import * as identity from 'fp-ts/lib/Identity'
// a very simple (and imprecise) key-value store
const kvs: { [key: string]: any } = {}
// the program will crash if a key is not found,
// or if a type is incorrectly specified
function compile<A>(fa: KVStoreA<A>): identity.Identity<A> {
if (fa instanceof Put) {
console.log(`put(${fa.key}, ${fa.value})`)
kvs[fa.key] = fa.value
return identity.of(undefined) as any
} else if (fa instanceof Get) {
console.log(`get(${fa.key})`)
return identity.of(fromNullable(kvs[fa.key])) as any
} else if (fa instanceof Delete) {
console.log(`del(${fa.key})`)
delete kvs[fa.key]
return identity.of(undefined) as any
}
throw new Error('never')
}
// Run your program
const result = program.foldMap(identity, compile).value
console.log(result) // =>
/*
put(wild-cats, 2)
get(wild-cats)
put(wild-cats, 14)
put(tame-cats, 5)
get(wild-cats)
del(tame-cats)
Some(14)
*/
This is even better than flow-static-land.
With the above implementation I can follow this Scala tutorial about free monads as if it were ts 😄
Nice. We can adapt it in order to write an official tutorial.
Some considerations:
(1) I don't like the way Scala implements unions using inheritance. TypeScript supports actual unions, I'd love to use them with ADTs
(2) the interpreter is not type safe (note how many any
in the compile
implementation above)
(3) no exhaustivity checks in the body of interpreters
(4) there's a lot of boilerplate involved
As a possible solution I propose this pattern (which uses phantom types)
// ...definition of Free as above...
// additional definitions
export interface ADT<URI extends HKTS, A> {
_A: A
_URI: URI
}
export type AnyADT = ADT<any, any>
export type UriOf<FA extends AnyADT> = FA['_URI']
export type TypeOf<FA extends AnyADT> = FA['_A']
export function liftADT<FA extends AnyADT>(fa: FA): Free<UriOf<FA>, TypeOf<FA>> {
return liftFree(fa as any)
}
Usage
ADTs
//
// Adapted from http://blog.scalac.io/2016/06/02/overview-of-free-monad-in-cats.html
//
export class Degree {
readonly value: number
constructor(d: number) {
this.value = (d + 360) % 360
}
}
export class Position {
constructor(
public readonly x: number,
public readonly y: number,
public readonly heading: Degree
) {}
}
export type InstructionURI = 'Instruction'
export class Forward implements ADT<InstructionURI, Position> {
readonly _tag: 'Forward'
readonly _A: Position
readonly _URI: InstructionURI
constructor(
public readonly position: Position,
public readonly length: number
) {}
}
export class RotateRight implements ADT<InstructionURI, Position> {
readonly _tag: 'RotateRight'
readonly _A: Position
readonly _URI: InstructionURI
constructor(
public readonly position: Position,
public readonly degree: Degree
) {}
}
Lifting
export type Instruction = Forward | RotateRight
declare module 'fp-ts/lib/HKT' {
interface HKT<A> {
Instruction: Instruction
}
}
// less boilerplate here using liftADT
const forward = (position: Position, length: number) => liftADT(new Forward(position, length))
const right = (position: Position, degree: Degree) => liftADT(new RotateRight(position, degree))
(type-safe) Interpreters
const computation = {
forward(position: Position, length: number): Position {
const degree = position.heading.value
if (degree === 0) {
return new Position(position.x + length, position.y, position.heading)
} else if (degree === 90) {
return new Position(position.x, position.y + length, position.heading)
} else if (degree === 180) {
return new Position(position.x - length, position.y, position.heading)
} else if (degree === 270) {
return new Position(position.x, position.y - length, position.heading)
}
throw new Error(`Unkonwn direction ${degree}`)
},
right(position: Position, degree: Degree): Position {
return new Position(position.x, position.y, new Degree(position.heading.value - degree.value))
}
}
import * as identity from 'fp-ts/lib/Identity'
function interpretIdentity(fa: Instruction): identity.Identity<TypeOf<typeof fa>> {
if (fa instanceof Forward) {
return identity.of(computation.forward(fa.position, fa.length))
} else {
return identity.of(computation.right(fa.position, fa.degree))
}
}
const start = new Position(0, 0, new Degree(90))
const program1 = (start: Position) => {
return forward(start, 10)
.chain(p1 => right(p1, new Degree(90)))
.chain(p2 => forward(p2, 10))
}
console.log(program1(start).foldMap(identity, interpretIdentity).value) // => Position { x: 10, y: 10, heading: Degree { value: 0 } }
import * as option from 'fp-ts/lib/Option'
const nonNegative = (position: Position): option.Option<Position> =>
position.x >= 0 && position.y >= 0 ? option.some(position) : option.none
function interpretOption(fa: Instruction): option.Option<TypeOf<typeof fa>> {
if (fa instanceof Forward) {
return nonNegative(computation.forward(fa.position, fa.length))
} else {
return nonNegative(computation.right(fa.position, fa.degree))
}
}
const program2 = (start: Position) => {
return forward(start, 10)
.chain(p1 => right(p1, new Degree(90)))
.chain(p2 => forward(p2, 10))
.chain(p1 => right(p1, new Degree(180)))
.chain(p2 => forward(p2, 20)) // Here the computation stops, because result will be None
}
console.log(program2(start).foldMap(option, interpretOption)) // => None
Is there a way to hide some of the boilerplate code for the ADT such as _tag
, _A
, and _URI
?
export class Forward implements ADT<InstructionURI, Position> {
readonly _tag: 'Forward'
readonly _A: Position
readonly _URI: InstructionURI
constructor(
public readonly position: Position,
public readonly length: number
) {}
}
_tag
can be removed if we test using instanceof
. Also we can make ADT
a class
export class ADT<URI extends HKTS, A> {
readonly _A: A
readonly _URI: URI
}
and then
export class InstructionADT<A> extends ADT<'Instruction', A> {}
export class Forward extends InstructionADT<Position> {
constructor(
public readonly position: Position,
public readonly length: number
) { super() }
}
export class RotateRight extends InstructionADT<Position> {
constructor(
public readonly position: Position,
public readonly degree: Degree
) { super() }
}
@danielepolencic I'm not satisfied. I think we can remove _URI
by currying liftADT
, also need a solution for composing several sets of instructions (Composing
section of the tutorial). Working on an amended version..
Turns out I need some boilerplate in order to get the best type safety
_tag
(at the type level AND at runtime) in order to use switch
and get exhaustivity checks in the base interpreters_URI
(at the type level AND at runtime) in order to use switch
and get exhaustivity checks in the composed interpreterFortunately we need less boilerplate than Scala while composing ADTs and interpreters.
Here's the complete tutorial
// adapted from http://okmij.org/ftp/Computation/free-monad.html
import { HKT, HKTS } from 'fp-ts/lib/HKT'
import { FantasyMonad, StaticMonad } from 'fp-ts/lib/Monad'
import { identity as id } from 'fp-ts/lib/function'
export type NaturalTransformation<F extends HKTS, G extends HKTS> = <A>(fa: HKT<A>[F]) => HKT<A>[G]
declare module 'fp-ts/lib/HKT' {
interface HKT<A> {
Free: Free<any, A>
}
}
export const URI = 'Free'
export type URI = typeof URI
export type Free<F extends HKTS, A> = Pure<F, A> | Impure<F, A, any>
export class Pure<F extends HKTS, A> implements FantasyMonad<URI, A> {
static of = of
readonly _tag = 'Pure'
readonly _F: F
readonly _A: A
readonly _URI: URI
constructor(public readonly a: A) {}
map<B>(f: (a: A) => B): Free<F, B> {
return new Pure<F, B>(f(this.a))
}
of<B>(b: B): Free<F, B> {
return of<F, B>(b)
}
ap<B>(fab: Free<F, (a: A) => B>): Free<F, B> {
return fab.chain(f => this.map(f)) // <- derived
}
chain<B>(f: (a: A) => Free<F, B>): Free<F, B> {
return f(this.a)
}
foldMap<M extends HKTS>(monad: StaticMonad<M>, f: NaturalTransformation<F, M>): HKT<A>[M] {
return monad.of(this.a)
}
}
export class Impure<F extends HKTS, A, X> implements FantasyMonad<URI, A> {
static of = of
readonly _tag = 'Impure'
readonly _F: F
readonly _A: A
readonly _X: X
readonly _URI: URI
constructor(public readonly fx: HKT<X>[F], public readonly f: (x: X) => Free<F, A>) {}
map<B>(f: (a: A) => B): Free<F, B> {
return new Impure<F, B, X>(this.fx, x => this.f(x).map(f))
}
of<B>(b: B): Free<F, B> {
return of<F, B>(b)
}
ap<B>(fab: Free<F, (a: A) => B>): Free<F, B> {
return fab.chain(f => this.map(f)) // <- derived
}
chain<B>(f: (a: A) => Free<F, B>): Free<F, B> {
return new Impure<F, B, X>(this.fx, x => this.f(x).chain(f))
}
foldMap<M extends HKTS>(monad: StaticMonad<M>, f: NaturalTransformation<F, M>): HKT<A>[M] {
return monad.chain<X, A>((x: X) => this.f(x).foldMap(monad, f), f<X>(this.fx))
}
}
export function of<F extends HKTS, A>(a: A): Free<F, A> {
return new Pure<F, A>(a)
}
export function liftF<F extends HKTS, A>(fa: HKT<A>[F]): Free<F, A> {
return new Impure<F, A, A>(fa, of)
}
export interface ADT<URI extends HKTS, A> {
readonly _A: A
readonly _URI: URI
}
export type AnyADT = ADT<any, any>
export type UriOf<FA extends AnyADT> = FA['_URI']
export type TypeOf<FA extends AnyADT> = FA['_A']
export function liftADT<FA extends AnyADT>(fa: FA): Free<UriOf<FA>, TypeOf<FA>> {
return liftF(fa as any)
}
export function inject<G extends HKTS>(): <F extends G>(free: Free<F, any>) => Free<G, TypeOf<typeof free>> {
return id
}
//
// Adapted from http://blog.scalac.io/2016/06/02/overview-of-free-monad-in-cats.html
//
export class Degree {
readonly value: number
constructor(d: number) {
this.value = (d + 360) % 360
}
}
export class Position {
constructor(
public readonly x: number,
public readonly y: number,
public readonly heading: Degree
) {}
}
export const InstructionURI = 'Instruction'
export type InstructionURI = typeof InstructionURI
export class Forward {
readonly _tag = 'Forward'
readonly _A: Position
readonly _URI = InstructionURI
constructor(
public readonly position: Position,
public readonly length: number
) { }
}
export class Backward {
readonly _tag = 'Backward'
readonly _A: Position
readonly _URI = InstructionURI
constructor(
public readonly position: Position,
public readonly length: number
) { }
}
export class RotateRight {
readonly _tag = 'RotateRight'
readonly _A: Position
readonly _URI = InstructionURI
constructor(
public readonly position: Position,
public readonly degree: Degree
) { }
}
export class Show {
readonly _tag = 'Show'
readonly _A: void
readonly _URI = InstructionURI
constructor(
public readonly position: Position
) { }
}
export type Instruction = Forward | Backward | RotateRight | Show
declare module 'fp-ts/lib/HKT' {
interface HKT<A> {
Instruction: Instruction
}
}
export const forward = (position: Position, length: number) => liftADT(new Forward(position, length))
export const backward = (position: Position, length: number) => liftADT(new Backward(position, length))
export const right = (position: Position, degree: Degree) => liftADT(new RotateRight(position, degree))
export const show = (position: Position) => liftADT(new Show(position))
const computation = {
forward(position: Position, length: number): Position {
const degree = position.heading.value
if (degree === 0) {
return new Position(position.x + length, position.y, position.heading)
} else if (degree === 90) {
return new Position(position.x, position.y + length, position.heading)
} else if (degree === 180) {
return new Position(position.x - length, position.y, position.heading)
} else if (degree === 270) {
return new Position(position.x, position.y - length, position.heading)
}
throw new Error(`Unkonwn direction ${degree}`)
},
backward(position: Position, length: number): Position {
return computation.forward(
new Position(position.x, position.y, new Degree(position.heading.value + 180)),
length
)
},
right(position: Position, degree: Degree): Position {
return new Position(position.x, position.y, new Degree(position.heading.value - degree.value))
}
}
import * as identity from 'fp-ts/lib/Identity'
export function interpretIdentity(fa: Instruction): identity.Identity<TypeOf<typeof fa>> {
switch (fa._tag) {
case 'Forward' :
return identity.of(computation.forward(fa.position, fa.length))
case 'Backward' :
return identity.of(computation.backward(fa.position, fa.length))
case 'RotateRight' :
return identity.of(computation.right(fa.position, fa.degree))
case 'Show' :
console.log('interpretIdentity', fa.position)
return identity.of(fa.position)
}
}
const start = new Position(0, 0, new Degree(90))
const program1 = (start: Position) => {
return forward(start, 10)
.chain(p1 => right(p1, new Degree(90)))
.chain(p2 => forward(p2, 10))
.chain(p3 => show(p3))
}
console.log('--program1--')
program1(start).foldMap(identity, (fa: Instruction) => interpretIdentity(fa)).value // => interpretIdentity Position { x: 10, y: 10, heading: Degree { value: 0 } }
import * as option from 'fp-ts/lib/Option'
const nonNegative = (position: Position): option.Option<Position> =>
position.x >= 0 && position.y >= 0 ? option.some(position) : option.none
export function interpretOption(fa: Instruction): option.Option<TypeOf<typeof fa>> {
switch (fa._tag) {
case 'Forward' :
return nonNegative(computation.forward(fa.position, fa.length))
case 'Backward' :
return nonNegative(computation.backward(fa.position, fa.length))
case 'RotateRight' :
return nonNegative(computation.right(fa.position, fa.degree))
case 'Show' :
console.log('interpretOption', fa.position)
return option.some(fa.position)
}
}
const program2 = (start: Position) => {
return forward(start, 10)
.chain(p1 => right(p1, new Degree(90)))
.chain(p2 => forward(p2, 10))
.chain(p3 => right(p3, new Degree(180)))
.chain(p4 => forward(p4, 20)) // Here the computation stops, because result will be None
.chain(p5 => show(p5))
}
console.log('--program2--')
program2(start).foldMap(option, (fa: Instruction) => interpretOption(fa))
// Composing
export const PencilInstructionURI = 'PencilInstruction'
export type PencilInstructionURI = typeof PencilInstructionURI
export class PencilUp {
readonly _tag = 'PencilUp'
readonly _A: void
readonly _URI = PencilInstructionURI
constructor(
public readonly position: Position
) { }
}
export class PencilDown {
readonly _tag = 'PencilDown'
readonly _A: void
readonly _URI = PencilInstructionURI
constructor(
public readonly position: Position
) { }
}
export type PencilInstruction = PencilUp | PencilDown
declare module 'fp-ts/lib/HKT' {
interface HKT<A> {
PencilInstruction: PencilInstruction
}
}
export const pencilUp = (position: Position) => liftADT(new PencilUp(position))
export const pencilDown = (position: Position) => liftADT(new PencilDown(position))
export type LogoAppURI = InstructionURI | PencilInstructionURI
export type LogoApp = Instruction | PencilInstruction
const inj = inject<LogoAppURI>()
const program3 = (start: Position) => {
return inj(forward(start, 10))
.chain(p1 => right(p1, new Degree(90)))
.chain(p2 => {
return inj(pencilUp(p2))
.chain(() => forward(p2, 10))
.chain(p3 => {
return inj(pencilDown(p3))
.chain(() => backward(p3, 20))
.chain(p4 => show(p4))
})
})
}
export function penInterpretIdentity(fa: PencilInstruction): identity.Identity<TypeOf<typeof fa>> {
if (fa instanceof PencilUp) {
console.log(`stop drawing at position ${JSON.stringify(fa.position)}`)
return identity.of(undefined)
} else {
console.log(`start drawing at position ${JSON.stringify(fa.position)}`)
return identity.of(undefined)
}
}
export function interpret(fa: LogoApp): identity.Identity<TypeOf<typeof fa>> {
switch (fa._URI) {
case InstructionURI :
return interpretIdentity(fa)
case PencilInstructionURI :
return penInterpretIdentity(fa)
}
}
console.log('--program3--')
program3(start).foldMap(identity, (fa: LogoApp) => interpret(fa))
/*
stop drawing at position {"x":0,"y":10,"heading":{"value":0}}
start drawing at position {"x":10,"y":10,"heading":{"value":0}}
interpretIdentity Position { x: -10, y: 10, heading: Degree { value: 180 } }
*/
// should raise an error
// program3(start).foldMap(identity, (fa: LogoApp) => interpretIdentity(fa))
This looks very interesting to me. Thanks for that!
A few comments:
_
are generally assumed to be private. I think in this case making the field private would not help much since we would not be able to access the value inside the switch
. Is there a specific reason why you want to prefix them with an underscore?export class Forward {
readonly _tag = 'Forward'
readonly _A: Position
readonly _URI = InstructionURI
constructor(
public readonly position: Position,
public readonly length: number
) { }
}
_tag
property. I guess I could have myInstance.constructor.name
instead of _tag
or instanceof
if the name of the class is the same as the _tag
property. I don't think Typescript is smart enough to narrow down the selection with that, though.Is there a specific reason why you want to prefix them with an underscore?
Because they were phantom types so far
readonly _tag: 'Forward'
readonly _A: Position
readonly _URI: InstructionURI
However in the last version _tag
and _URI
are also runtime values so we could change their name
readonly _A: Position // <= this is still a phantom type
readonly tag = 'Forward'
readonly URI = InstructionURI
I could still use the trick you mentioned in this comment?
Not sure that trick is useful anymore since we also need runtime values (or maybe there is another way..)
export class ADT<URI extends HKTS, T, A> {
readonly _A: A
readonly tag: T
readonly URI: URI
constructor(URI: URI, tag: T) {
this.URI = URI
this.tag = tag
}
}
// new, a bit verbose
export class Forward extends ADT<'Instruction', 'Forward', Position> {
constructor(
public readonly position: Position,
public readonly length: number
) { super('Instruction', 'Forward') } // <= these values are duplicated but at least are type-checked
}
// old
export class Forward {
readonly _A: Position
readonly tag = 'Forward'
readonly URI = InstructionURI
constructor(
public readonly position: Position,
public readonly length: number
) { }
}
I'm concerned that I end up duplicating code in the name of the class and in the _tag property
I'm afraid is unavoidable: you need both the runtime value and the literal type
class MyClass {
tag = MyClass.constructor.name
}
const y = new MyClass()
y.tag // <- type `string`, we need the literal type `'MyClass'`
Thanks for clarifying.
I'd like to try out the code, can this be merged as a PR?
@danielepolencic https://github.com/gcanti/fp-ts/pull/56
Thanks for this!
🎉 🍾 💪
I have another question that has to do more with the theory behind free(r) monads than this library.
Let's assume that I implemented the logo language as showed above. I also have the following program written in the DSL:
const program1 = (start: Position) => {
return forward(start, 10)
.chain(p1 => right(p1, new Degree(90)))
.chain(p2 => forward(p2, 10))
.chain(p3 => show(p3))
}
I'd like to write an interpreter that instead of returning a value as such, it returns a representation of the program (AST). In other words, I'd like to have an interpreter that at the end of the computation produces the following abstract syntax tree:
[
{action: 'forward', position: {...}, length: 10},
{action: 'right', position: {...}, degree: 90},
{action: 'forward', position: {...}, length: 10},
{action: 'show'}
]
I tried to play with the identity interpreter and the best I could come up with is this:
interface INode {
action: string
position: INode | Position
length?: number
degree?: Degree
}
export function interpretIdentity(fa: Instruction): identity.Identity<INode> {
switch (fa._tag) {
case 'Forward' :
return identity.of({action: 'forward', position: fa.position, length: fa.length});
case 'Backward' :
return identity.of({action: 'backward', position: fa.position, length: fa.length});
case 'RotateRight' :
return identity.of({action: 'right', position: fa.position, degree: fa.degree})
case 'Show' :
return identity.of({action: 'show', position: fa.position})
}
}
which returns the following recursive data structure:
{
"action": "show",
"position": {
"action": "forward",
"position": {
"action": "right",
"position": {
"action": "forward",
"position": {
"x": 0,
"y": 0,
"heading": {
"value": 90
}
},
"length": 10
},
"degree": {
"value": 90
}
},
"length": 10
}
}
Let's assume now I want to evaluate the AST, is there a way I can feed that back to the free monad, but this time using the real identity interpreter with computations?
@danielepolencic not sure I'm following, program1
is already an "AST" that you can interpret. Why another AST?
The second AST is serialised and can be persisted / shipped out of the app.
I see. Theoretically the signature of your interpreter is not correct though
export function interpretIdentity(fa: Instruction): identity.Identity<INode>
should be
export function interpretIdentity<A>(fa: Instruction<A>): identity.Identity<A>
// or
export function interpretIdentity(fa: Instruction): identity.Identity<free.TypeOf<typeof fa>>
that is, the codomain of a natural transformation should be a functor.
Indeed the type of result
here is void
// result :: void
const result = program1(start).foldMap(identity, (fa: Instruction) => interpretNode(fa)).value
so result
can't contain any recursive data structure.
In order to build an AST we could use State
export interface ForwardStatement {
action: 'forward',
length: number
}
export interface BackwardStatement {
action: 'backward',
length: number
}
export interface RotateRightStatement {
action: 'right',
degree: Degree
}
export interface ShowStatement {
action: 'show'
}
export type Statement =
| ForwardStatement
| BackwardStatement
| RotateRightStatement
| ShowStatement
export type Program = Array<Statement>
export class AST<A> extends state.State<Program, A> {}
function push(s: Program, n: Statement): Program {
return s.concat(n)
}
export function interpretAST(fa: Instruction): AST<free.TypeOf<typeof fa>> {
switch (fa._tag) {
case 'Forward' :
return new AST(s => [
computation.forward(fa.position, fa.length),
push(s, {action: 'forward', length: fa.length})
])
case 'Backward' :
return new AST(s => [
computation.backward(fa.position, fa.length),
push(s, {action: 'backward', length: fa.length})
]);
case 'RotateRight' :
return new AST(s => [
computation.right(fa.position, fa.degree),
push(s, {action: 'right', degree: fa.degree})
])
case 'Show' :
return new AST(s => [
undefined,
push(s, {action: 'show'})
])
}
}
const result: [void, Program] = program1(start).foldMap(state, (fa: Instruction) => interpretAST(fa)).run([])
console.log(JSON.stringify(result[1], null, 2))
/*
[
{
"action": "forward",
"length": 10
},
{
"action": "right",
"degree": {
"value": 90
}
},
{
"action": "forward",
"length": 10
},
{
"action": "show"
}
]
*/
function run(start: Position, program: Program): Position {
let out = start
program.forEach(statement => {
switch (statement.action) {
case 'forward' :
out = computation.forward(out, statement.length)
break
case 'backward' :
out = computation.backward(out, statement.length)
break
case 'right' :
out = computation.right(out, statement.degree)
break
case 'show' :
// do nothing
}
})
return out
}
console.log('--program from ast--')
console.log(run(start, result[1])) // => Position { x: 10, y: 10, heading: Degree { value: 0 } }
Maybe a better option than run
is writing a function from the AST to free.Free<"Instruction", Position | void>
. Then you can use any interpreter
function fromStatement(position: Position, statement: Statement): free.Free<"Instruction", Position | void> {
switch (statement.action) {
case 'forward' :
return forward(position, statement.length)
case 'backward' :
return backward(position, statement.length)
case 'right' :
return right(position, statement.degree)
case 'show' :
return show(position)
}
}
export function fromProgram(program: Program): (start: Position) => free.Free<"Instruction", Position | void> {
if (program.length === 0) {
throw new Error('Invalid program')
}
return start => {
let out = fromStatement(start, program[0])
program.slice(1).forEach(statement => {
out = out.chain(p => {
if (p) {
return fromStatement(p, statement)
}
throw new Error('Runtime error: unknown position')
})
})
return out
}
}
const program4 = fromProgram(result[1])
console.log('--fromProgram--')
program4(start).foldMap(identity, (fa: Instruction) => interpretIdentity(fa)).value // => interpretIdentity Position { x: 10, y: 10, heading: Degree { value: 0 } }
the codomain of a natural transformation should be a functor.
I guess I lack the basics. Thanks for clarifying, this is really helpful.
@danielepolencic a natural transformation maps a functor (input domain) to another functor (output domain aka codomain)
export type NaturalTransformation<F extends HKTS, G extends HKTS> = <A>(fa: HKT<A>[F]) => HKT<A>[G]
That's it! :)
@danielepolencic Another example of free applied to a real world task: drawing on canvas
I made two drawings (https://github.com/gcanti/graphics-ts/blob/master/example/index.ts)
IO
(module canvas
https://github.com/gcanti/graphics-ts/blob/master/src/canvas.ts) Free
(module free-canvas
https://github.com/gcanti/graphics-ts/blob/master/src/free-canvas.ts)Note: the free version is slower, even if I cheated writing an impure interpreter (returns Identity
, should return IO
)
ioSnowflake: 433.783935546875ms
freeSnowflake: 853.5810546875ms
Nice example, thanks!
Note: the free version is slower, even if I cheated writing an impure interpreter (returns Identity, should return IO)
I'm aware of it. I read a paper somewhere suggesting that the quadratic nature of Free could be simplified to improve performance.
Tbh, I'm not too concerned about performance as long as the code is safe and horizontally scalable (might not be doable for graphics-ts).
I'm aware of it. I read a paper somewhere suggesting that the quadratic nature of Free could be simplified to improve performance.
I suspect my first version of free is pretty naive in this respect (and probably also not stack safe), I'll try to dig into this as soon as I have some time
I think this was the article I read it from: http://mandubian.com/2015/04/09/freer/
@gcanti regarding the stack, in a nodejs context, one may rely on that: https://medium.com/@dai_shi/tail-call-optimization-tco-in-node-v6-e2492c9d5b7c
Flow-static-land has an implementation of the Free monad.
Is it possible to include it in this library too?