gcanti / fp-ts

Functional programming in TypeScript
https://gcanti.github.io/fp-ts/
MIT License
10.81k stars 502 forks source link

Request: free monad #42

Closed danielepolencic closed 7 years ago

danielepolencic commented 7 years ago

Flow-static-land has an implementation of the Free monad.

Is it possible to include it in this library too?

gcanti commented 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)
*/
danielepolencic commented 7 years ago

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 😄

gcanti commented 7 years ago

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
danielepolencic commented 7 years ago

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
  ) {}
}
gcanti commented 7 years ago

_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() }
}
gcanti commented 7 years ago

@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..

gcanti commented 7 years ago

Turns out I need some boilerplate in order to get the best type safety

Fortunately 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))
danielepolencic commented 7 years ago

This looks very interesting to me. Thanks for that!

A few comments:

gcanti commented 7 years ago

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'`
danielepolencic commented 7 years ago

Thanks for clarifying.

I'd like to try out the code, can this be merged as a PR?

gcanti commented 7 years ago

@danielepolencic https://github.com/gcanti/fp-ts/pull/56

danielepolencic commented 7 years ago

Thanks for this!

🎉 🍾 💪

danielepolencic commented 7 years ago

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?

gcanti commented 7 years ago

@danielepolencic not sure I'm following, program1 is already an "AST" that you can interpret. Why another AST?

danielepolencic commented 7 years ago

The second AST is serialised and can be persisted / shipped out of the app.

gcanti commented 7 years ago

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 } }
gcanti commented 7 years ago

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 } }
danielepolencic commented 7 years ago

the codomain of a natural transformation should be a functor.

I guess I lack the basics. Thanks for clarifying, this is really helpful.

sledorze commented 7 years ago

@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! :)

gcanti commented 7 years ago

@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)

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
danielepolencic commented 7 years ago

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).

gcanti commented 7 years ago

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

danielepolencic commented 7 years ago

I think this was the article I read it from: http://mandubian.com/2015/04/09/freer/

sledorze commented 7 years ago

@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