alexanderjarvis / maybe

Maybe is a type that wraps optional values
MIT License
302 stars 10 forks source link

Wrapping multiple optional values #8

Open benadamstyles opened 7 years ago

benadamstyles commented 7 years ago

What would the maybe version of the following be?

let valueX = null
let valueY = null

if (valueX && valueY) {
  // do something with valueX and valueY
}

Would it be this?

let valueX = null
let valueY = null

maybe(valueX)
.map(() => valueY)
.foreach(() => {
  // do something with valueX and valueY
})

This doesn't seem very functional. Maybe I'm missing something super obvious. Thanks!

alexanderjarvis commented 7 years ago

Unfortunately the syntax isn't too great:

maybe(valueX)
  .flatMap(x => maybe(valueY).map(y => ({x, y})))
  .foreach({x, y} => {
    // use x and y
  })

you can make it a tiny bit shorter by wrapping the values with maybe first e.g.

const maybeX = maybe(valueX)
const maybeY = maybe(valueY)

maybeX
  .flatMap(x => maybeY.map(y => ({x, y})))
  .foreach({x, y} => {
    // use x and y
  })

Something that Scala has which is quite nice for this is a for comprehension which would look like:

for {
  x <- maybeX
  y <- maybeY
  // do something with x, y / return result
} yield

... but I think your if statement is fine :)

benadamstyles commented 7 years ago

Ah ok, yep. I was close. One thing I'm struggling with actually is when to use .map and when to use .flatMap. I know there's the simple rule of "flatMap if you're returning a Maybe", but I'm not always sure whether I want to return a Maybe or a "possibly null value". Perhaps I never want to return a "possibly null value" – should I always be wrapping one of those in maybe()?

I wonder if there could be an extra method called something like and or join which would work something like:

maybe(valueX)
  .and(valueY)
  .and(valueZ)
  .foreach(([x, y, z]) => {
    // use x, y and z
  })
benadamstyles commented 7 years ago

Just thought, couldn't you do this to make it even shorter? Given that you already know maybeX is a Maybe?

const maybeX = maybe(valueX)
const maybeY = maybe(valueY)

maybeX
  .flatMap(x => maybeY)
  .foreach(y => {
    const x = maybeX.just() // we know maybeX is a Just because it passed the flatMap test
    // use x and y
  })
alexanderjarvis commented 7 years ago

You use map when you transform the value inside the maybe to another value. flatMap is when you need to return a Maybe instead and is most useful unwrapping more than one Maybe or when using the just and nothing.

The best way to visualise and understand flatMap is to know that it's shorthand for flatten and would be the same as doing map and then flatten. For example:

maybeX.map(x => maybeY) // Maybe(Maybe(y))

whereas

maybeX.flatMap(x => maybeY) // Maybe(y)

so Maybe(Maybe(y)) becomes Maybe(y) by using flatMap.

alexanderjarvis commented 7 years ago

It would be possible to make another type of Maybe that takes an array and only lets you map / forEach when they're all non empty values.

For example:

maybe([1, 2, 3])
  .filter(a => a.every(e => maybe(e).isJust()))
  .foreach(console.log)
alexanderjarvis commented 7 years ago

Also in response to your last comment: you could do it like that. But I wouldn't.. as you're creating a dependency that might break if someone updates it. It's also perhaps less clear.

The preferred method would be to have default values for your Maybe's by using orJust().

benadamstyles commented 7 years ago

This is so useful, thanks. I think I've finally "grokked" flatMap now... Thanks for all your help!

benadamstyles commented 7 years ago

For example:

maybe([1, 2, 3])
 .filter(a => a.every(e => maybe(e).isJust()))
 .foreach(console.log)

@alexanderjarvis I'm finding myself doing this a lot. Would you consider a PR that adds a new method, something like .all() (or whatever you think) which does this? The code would be something along the lines of:

all() {
  if (Array.isArray(this.value) && this.value.every(x => maybe(x).isJust())) {
    return this
  } else {
    return nothing
  }
}

And you would use it like:

maybe([1, 2, 3])
  .all()
  .map(transform)
  .orJust([])