tom-weatherhead/common-utilities.ts

View on GitHub
src/functors.ts

Summary

Maintainability
A
0 mins
Test Coverage
// github:tom-weatherhead/common-utilities.ts/src/functors.ts

export interface IFunctor<T> {
    map<U>(f: (x: T) => U): IFunctor<U>;
    getValue(): T;
}

class UniversalFunctor<T> implements IFunctor<T> {
    private readonly value: T;

    constructor(value: T) {
        this.value = value;
    }

    public getValue(): T {
        return this.value;
    }

    public map<U>(f: (x: T) => U): IFunctor<U> {
        return new UniversalFunctor<U>(f(this.value));
    }
}

export function createFunctor<T>(value: T): IFunctor<T> {
    return new UniversalFunctor<T>(value);
}

/*
// From https://gist.github.com/lierdakil/2ece55b7684c5923b1ea4c36df643455 :

interface Functor<T> {
    map<U>(f: (x: T) => U): Functor<U>
}

class Box<T> implements Functor<T> {
    value: T

    constructor(x: T) {
        this.value = x
    }

    map<U>(f: (x: T) => U): Box<U> {
        return new Box(f(this.value))
    }

    toString(): string {
        return `Box(${this.value.toString()})`
    }
}

const box = <T> (x: T): Box<T> => new Box(x)

const log = (msg: string, x: any) => {
  const pre = document.createElement('pre')
  pre.innerText = `${msg} ==> ${x.toString()}`
  document.body.appendChild(pre)
}
const trim = (x: string) => x.trim()
const len = (x: string) => x.length
const inc = (x: number) => x+1
function compose<T,U,V>(x: (arg: T) => U, y: (arg: U) => V): (arg: T) => V {
  return arg => y(x(arg))
}

box(' 42 ')   // contains a string
    .map(trim)
    .map(len)
    .map(inc)

// vs

box([' 42 ', 'ABC ', 'x'])   // contains an array
    .map(array => array.map(trim))
    .map(array => array.map(len))
    .map(array => array.map(inc))

// is the same as

box(' 42 ')   // contains a string
    .map(compose(compose(trim, len), inc))

box([' 42 ', 'ABC ', 'x'])   // contains an array
    .map(array => array.map(compose(compose(trim, len), inc)))

// looks better if we do it like this

function fmap<T, U>(f: (arg: T) => U): (Fx: Functor<T>) => Functor<U> {
  return Fx => Fx.map(f)
}

log(
  "fmap(compose(compose(trim, len), inc))(box(' 42 '))",
  fmap(compose(compose(trim, len), inc))(box(' 42 '))
)

log(
  "fmap(fmap(compose(compose(trim, len), inc)))(box([' 42 ', 'ABC ', 'x']))",
  fmap(fmap(compose(compose(trim, len), inc)))(box([' 42 ', 'ABC ', 'x']))
)

// the true powef of fmap is being applicable to any structure that can be mapped over

class Maybe<T> implements Functor<T>{
  value: {val: T, type: 'just'} | {type: 'nothing'}
  constructor(x?: T) {
    if (x != null) this.value = { val: x, type: 'just' }
    else this.value = { type: 'nothing' }
  }

  map<U>(f: (x: T | undefined) => U): Functor<U> {
    if (this.value.type === 'just') return just(f(this.value.val))
    else return nothing()
  }

  toString(): string {
    if (this.value.type === 'just')
      return `just(${this.value.val.toString()})`
    else
      return `nothing()`
  }
}

const just = <T> (x: T) => new Maybe<T>(x)
const nothing = <T> () => new Maybe<T>() // bad style, I know, <T> is just a type assertion in disguise

// this is fmap over array
log(
  "fmap(compose(compose(trim, len), inc))([' 42 ', 'ABC ', 'x'])",
  fmap(compose(compose(trim, len), inc))([' 42 ', 'ABC ', 'x'])
)

//this is fmap over Maybe
log(
  "fmap(compose(compose(trim, len), inc))(just(' 42 '))",
  fmap(compose(compose(trim, len), inc))(just(' 42 '))
)
//it won't break if we don't provide a value though!
log(
  "fmap(compose(compose(trim, len), inc))(nothing())",
  fmap(compose(compose(trim, len), inc))(nothing())
)

// another simple example

class Either<T> implements Functor<T>{
  value: {msg: string, type: 'left'} | {type: 'right', val: T}
  constructor(msg: string)
  constructor(msg: null, x: T)
  constructor(msg: string | null, x?: T) {
    if (msg === null) this.value = { val: x, type: 'right' }
    else this.value = { type: 'left', msg }
  }

  map<U>(f: (x: T | undefined) => U): Functor<U> {
    if (this.value.type === 'right') return right(f(this.value.val))
    else return left(this.value.msg)
  }

  toString(): string {
    if (this.value.type === 'right')
      return `right(${this.value.val.toString()})`
    else
      return `left(${this.value.msg.toString()})`
  }
}

const left = <T>(msg: string) => new Either<T>(msg)
const right = <T>(val: T) => new Either<T>(null, val)

//this is fmap over Either
log(
  "fmap(compose(compose(trim, len), inc))(right(' 42 '))",
  fmap(compose(compose(trim, len), inc))(right(' 42 '))
)
//this will return argument verbatim
log(
  "fmap(compose(compose(trim, len), inc))(left('Something went wrong'))",
  fmap(compose(compose(trim, len), inc))(left('Something went wrong'))
)
 */