littleball-games / lb-f

A collection of functional programming functions
MIT License
0 stars 1 forks source link

Add map and chain to maybes #25

Closed skylize closed 2 years ago

skylize commented 6 years ago
skylize commented 6 years ago

Seems like this is working. One thing that's bothering me is how exposed everything is. Should a console log really dump out all the internals like this? We could hide most of this stuff by putting it on a prototype.

Also, with the current implementation, I'm not sure it's possible to do a true chain. I had to use a functionally equivalent shortcut.

console.log(just(2))
//  { 
//    value: 2,
//    type: 'JUST',
//    map: [Function: map],
//    chain: [Function: chain]
//  }
Eruant commented 6 years ago

While map and chain do work, I'm wondering what value they add. I can see more of a case for map, but I'm not sure we should be extracting the value of the maybe without having both case (nothing or just).


Seems like this is working. One thing that's bothering me is how exposed everything is. Should a console log really dump out all the internals like this? We could hide most of this stuff by putting it on a prototype.

I was thinking we could solve this by using a closure to store the value of just in a navtive map.

Something like

const justValues = new Map()

// just would set the key
export const just = value => {
  const key = {}
  justValues.set(key, value)
  return key
}

export const maybe = (defaultValue = '[nothing]', successCallback) => aMaybe => {
  const isJust = justValues.get(aMaybe)
  if (isJust) {
    justValues.delete(aMaybe)
    return successCallback(isJust)
  } else {
    return defaultValue
  }
}

It does make it more complex, and I'm not sure if it would prevent map from working. But it would hide the inner workings.

skylize commented 6 years ago

While map and chain do work, I'm wondering what value they add.

I'm not 100% on the details yet because I've never actually built anything of my own using maybes. But map, chain, etc. are fulfilling parts of the monad laws so the we can compose these things cleanly into complex data pipelines instead of just doing some basic if/else-like switches in some toy examples.

I can see more of a case for map

chain is for undoing nesting incurred by mapping in complex scenarios. If you end up with a just(just(just(just(5)))) how are you ever going to get your value back out again? It doesn't look like it in my code, because I'm using a functionally equivalent implementation, but chain is effectively like calling map and then unwrapping the first layer.

I'm not sure we should be extracting the value of the maybe without having both case (nothing or just).

We've just split the implementation across the two cases. nothing must have every method that just has in order to handle the nothing case. But if called on a nothing, they will always just return anothernothing.

We are not doing anything internally to decide whether something is a nothing or a just. Instead user will be passing functions into our methods that return a nothing or just depending on result. This is all just scaffolding. User writes the business logic.

I was thinking we could solve this by using a closure to store the value of just in a navtive map.

That is a viable option. I'm flip-flopping all around on my opinion of how we should approach the basic architecture.

Eruant commented 6 years ago

I didn't think I could merge changes directly on to your branch... I thought that would make a PR onto your fork of the branch.

skylize commented 6 years ago

Oh I usually leave the github setting on that allows maintainers to make changes to my pull requests.

skylize commented 6 years ago

The reason I had curry imported is because I was going to curry maybe, but I forgot to do it. I was just working on that. We can't provide a default defaultValue, but I think we want to try to curry everything where we can.

Eruant commented 6 years ago

chain is for undoing nesting incurred by mapping in complex scenarios. If you end up with a just(just(just(just(5)))) how are you ever going to get your value back out again? It doesn't look like it in my code, because I'm using a functionally equivalent implementation, but chain is effectively like calling map and then unwrapping the first layer.

From my understanding of the code, mapping over a just value, will return a new just value (not a nested just value), so I'm still unsure of where chain comes in. To be honest, it's not something I've used before - so I may be missing a point here.

skylize commented 6 years ago

From my understanding of the code, mapping over a just value, will return a new just value (not a nested just value),

The function you want to map over might itself need to split its results into another just or nothing. In that case you could either map over that function and end up with nested result, or you can call chain instead and flatten it back down to a single maybe.

const just1 = just(1)
const justNeg25 = just(-25)
const add10 = x => x + 10
const  maybeGreaterThan0 = x => x > 0 ? just(x) : return nothing()

just1.map(add10) // just(11)
just1.map(maybeGreaterThan0) // just(just(1))
justNeg25.map(add10) // just(-15)
justNeg25.map(maybeGreaterThan0) // just(nothing)

just1.chain(maybeGreaterThan0) // just(1)
justNeg25.chain(maybeGreaterThan0) // nothing

// I am unclear if you are _supposed to_ be able to extract the final internal
// value like this with chain, but it works the way I defined it.
just1.chain(add10) // 11 
justNeg25.chain(add10) // -15
skylize commented 6 years ago

I really don't like this Map idea at all. It's a big global state object that our maybe script file will be holding for all the existing justs in someone's app. If capturing state in a closure, it should be a closure internal to the just.

Eruant commented 6 years ago

I really don't like this Map idea at all. It's a big global state object that our maybe script file will be holding for all the existing justs in someone's app. If capturing state in a closure, it should be a closure internal to the just.

Yep, the more I think about it, the more issues I have with it.


I don't like having more of a class structure. I think needing to call this on an object is not a good idea.

If we are going to use the chain function it can remove the value visibility.

const JUST = 'JUST'
const NOTHING = 'NOTHING'

export const maybe = (defaultValue = '[nothing]', successCallback) => aMaybe => {
  switch (aMaybe.type) {
    case JUST:
      return aMaybe.chain(successCallback)
    case NOTHING:
    default:
      return defaultValue
  }
}

export const just = value =>
  ({
    type: JUST,
    map: fn => just(fn(value)),
    chain: fn => fn(value)
  })

export const nothing = _ =>
  ({
    type: NOTHING,
    map: _ => nothing(),
    chain: _ => nothing()
  })
skylize commented 6 years ago

I'm definitely preferable to avoiding classes in general. But from a usability perspective, I think it's worth consideration for a library. The default console output (for both browsers and node) on my most recent commit d766ed1 is exactly what user would want to know.

console.log( just(10) ) // Just { value: 10 }
console.log( nothing() ) // Nothing {}
skylize commented 6 years ago

By the way, if we can solve map and chain, I think that's probably sufficient to know we have the architecture worked out and merge something into master. Whatever style we choose for adding map and chain we can just duplicate for extending with other application methods.

skylize commented 6 years ago

Ok. Just pushed another version 14d4f26. This gets away from classes by defining non-enumerable properties. It's not as clean and pretty as the simple object, but I think I kind of like this.