Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Phantom `empty` value and Phantom container compatible with Fantasy Land

See WIP implementation HERE

In Haskell we can define Monoid instances like this:

instance Monoid b => Monoid (Identity b) where
  mempty = mempty
  mappend (Identity a) (Identity b) = Identity (a `mappend` b)

instance (Monoid a, Monoid b) => Monoid (Pair a b) where
  mempty = Pair mempty mempty
  mappend (Pair u v) (Pair w x) = Pair (u `mappend` w) (v `mappend` x)

instance Monoid b => Monoid (a -> b) where
  mempty _ = mempty
  mappend f g x = f x `mappend` g x

But we can't do same in JavaScript as we don't have type information upfront

Identity.empty = ?? 
Pair.empty = ??
Function.empty = ??

One thing we can do is have ListIdentity for example:

const makeIdentity = M => {
  function Identity ...
  Identity.empty = Identity(M.empty)
  return Identity
}
ListIdentity = makeIdentity(List)
StringIdentity = makeIdentity(String)

But it would be a clanky to implement map we might go from Identity of List to Identity of String:

const makeIdentity = M => {
  ...
  Identity.prototype.map = function(f) {
    const nextVal = f(this.value)
    // we might memoize makeIdentity to not create multiple ListIdentity
    // but i's not nice we can do better
    return makeIdentity(nextVal.constructor)(nextVal)
  }
  ...
}

So what if in cases when we don't know what where to find desired empty value we had some special universal value. let's create one and see how it could be useful with Identity:

//    empty :: (Monoid m) => m
// this is a special Monoid value but we don't know for which type.
const isEmpty = m => m['@functional/empty'] === true
const empty = {
  '@functional/empty': true,
  constructor: { empty },
  concat: a => a
}

// [1] - if value in `this` is that empty value than 
//       concating to it is same as just returning `b`
// [2] - if value in `b` is that `empty` value or if `b` itself is the 
//       `empty` than concating to it is same as just returning `this`
// [3] - if non of them are empty then we can just concat inner values
Identity.empty = Identity(empty)
Identity.prototype.concat = function(b) {
  if (isEmpty(this.value)) { // [1]
    return b
  } else if (isEmpty(b) || isEmpty(b.value)) { // [2]
    return this
  } else { // [3]
    return Identity(this.value.concat(b.value))
  }
}
empty.concat(Identity([])) // = Identity([])
Identity.empty.concat(Identity([])) // = Identity([])
Identity([]).concat(empty) // = Identity([])
Identity([]).concat(Identity.empty) // = Identity([])

Now let's do it for Pair:

Pair.empty = Pair(empty, empty)
Pair.prototype.concat = function(b) {
  if (isEmpty(this._1) || isEmpty(this._2)) {
    return b
  } else if (isEmpty(b) || isEmpty(b._1) || isEmpty(b._2)) {
    return this
  } else {
    return Pair(this._1.concat(b._1), this._2.concat(b._2))
  }
}
empty.concat(Pair("", [])) // = Pair("", [])
Pair.empty.concat(Pair("", [])) // = Pair("", [])
Pair("", []).concat(empty) // = Pair("", [])
Pair("", []).concat(Pair.empty) // = Pair("", [])

Nice! And here is for Function:

Function.empty = (_) => empty
Function.prototype.concat = function(g) {
  if (isEmpty(g)) {
    return this
  }
  const f = this
  return (x) => {
    const a = this(x)
    const b = g(x)
    if (isEmpty(a)) {
      return b
    } else if (isEmpty(b)) {
      return a
    } else {
      return a.concat(b)
    }
  }
}
empty.concat((i) => [i+1, i-1]) // = ((i) => [i+1, i-1])
Function.empty.concat((i) => [i+1, i-1]) // = ((i) => [i+1, i-1])
;((i) => [i+1, i-1]).concat(empty) // = ((i) => [i+1, i-1])
;((i) => [i+1, i-1]).concat(Function.empty) // = ((i) => [i+1, i-1])

Lets's go even crazier and implement Monoid for task which executes task in parallel and concat's it's results

Task.empty = Task((rej, res) => void res(empty))
Task.prototype.concat = function(g) {
  if (isEmpty(g)) {
    return this
  }
  return Task.parallel([this, g]).map(([a, b]) => {
    if (isEmpty(a)) {
      return b
    } else if (isEmpty(b)) {
      return a
    } else {
      return a.concat(b)
    }
  })
}

This technique could be used for universal of method which just takes a value and does not need constructor, but returns a "partial" or "phantom" value like this:

const isOf = a => a['@functional/of'] === true
const of = (a) => ({
  '@functional/of': true,
  value: a,
  constructor: {
    of,
    chainRec: (f, i) => {
      const step = f(
        value => ({ done:false, value }),
        value => ({ done:true, value }),
        i
      )
      return step.chain(({ value, done }) => 
        done ? step.constructor.of(value) : step.constructor.chainRec(f, value) 
      )
    }
  },
  map: (f) => of(f(a)),
  ap: (f) => isOf(f) ? of(f.value(a)) : f.constructor.of(a).ap(f),
  concat: (b) => {
    if (isOf(b)) {
      return of(a.concat(b.value))
    } else if (isEmpty(b)) {
      return of(a)
    } else {
      return b.constructor.of(a).concat(b),
    }
  }
  chain: (f) => f(a),
})

of('/posts').chain(url => of(url).chain(of)) // = of('/posts')
of('/posts').chain(of).chain(of) // = of('/posts')
of('/posts').chain(url => Http.get(url)) // = Http.get('/posts')
of('/posts').ap(url => Http.get(url)) // = Http.get('/posts')
of('/posts').ap(of(a => a)) // = of('/posts')
of(empty).concat(of(empty)) // = of(empty)
empty.concat(of(empty)) // = of(empty)
of(empty).concat(empty) // = of(empty)

Identity.prototype.ap = function(f) {
  if (isOf(f) {
    return this.map(f.value)
  } else {
    Identity(f.value(this.value))
  }
}

of(10) // [1]
  .ap(of(a => a)) // [2]
  .chain(a => of(a)) // [3]
  .chain(a => Http.get(a)) // [4]
// [1,2,3] - We don't know in which type we are wrapping values, we have `Identity` 
//           like default implementation for every interface it could posibly become
// [4]- now as Http.get returns Task, value would be of that type. 
//      we don't know and don't need to know the type upfront.
//      1,2,3 could be in seperate function without knowing which 
//      type we would use next.

We could have this isEmpty/empty/of/isOf in libs like ramda or maybe as separate module or in fantasy-land.

Implementations might not be 100% correct, I have not checked them but it gives a general idea. In next couple days I would write tests for it and check if all works correctly.


Inspiration: Monads in Dynamically-Typed Languages

@safareli

This comment has been minimized.

Copy link
Owner Author

commented Oct 6, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.