[jwarden 1.8.2019] TODO: Remove some of the chaining stuff perhaps? A lot of examples, not sure the value of multiple... maybe need to sleep (again) on it.

Also, need to show how we can compose using Maybe to make better pipelines... probably belongs in a separate section.

Maybe

A Maybe is a data type that represents data either being there or not. It's used a lot when you are getting data from effects; loading data from a server, reading from a text file, or environment variables. It can also be used when you're asking for data that might not be there such as searching Array's or keys in Objects. We've covered get in Part 3: get and getOr in Part 5: Tacit Programming. They are similar to a Maybe in practice; maybe this Object exists and has this property, and it isn't undefined... maybe not. Where get and getOr are functions that return data, Maybe is a type that holds data.

Let's import get from lodash/fp and Maybe from Folktale to compare them. Here we have a config.json that has a serverURL property (require in Node will automatically attempt to JSON.parse a JSON file).

const { get } = require('lodash/fp')
const Maybe = require('folktale/maybe')

const configFile = require('./config.json')
console.log("configFile:", configFile)
// { "serverURL": "http://cow.com" }

The get will attempt to read the "serverURL" property from the parsed JSON:

// get
const getServerURL = () =>
    get('serverURL', configFile)

console.log(getServerURL())
// http://cow.com

The Maybe just holds the response... in a Just:

// Maybe
const serverURLMaybe = () =>
    get('serverURL', configFile)
    ? Maybe.Just(get('serverURL', configFile))
    : Maybe.Nothing()

console.log(serverURLMaybe())
// folktale:Maybe.Just({ value: "http://cow.com" })

When we attempt to read the "pingURL", a non-existent property in the JSON using get, we'll get undefined:

const getPingURL = () =>
    get('pingURL', configFile)

console.log(getPingURL())
// undefined

When we do the same with Maybe, we get back a Nothing:

const getPingURLMaybe = () =>
    get('pingURL', configFile)
    ? Maybe.Just(get('pingURL', configFile))
    : Maybe.Nothing()

console.log(getPingURLMaybe())
// folktale:Maybe.Nothing({  })

Easier Usage

A lot of types have sub-types, so if we destructure those, it's a bit easier to read once you've memorized the API.

const { Just, Nothing } = Maybe

const getPingURLMaybe = () =>
    get('pingURL', configFile)
    ? Just(get('pingURL', configFile))
    : Nothing()

console.log(getPingURLMaybe())
// folktale:Maybe.Nothing({  })

Array and Object Access

Depending on language, even with strong typing, you can't always ensure at runtime an Array or Object will have the value/key you need. Here's an Array with 3 items:

const friends = ['Steven', 'Albus', 'Cow']

To get item 3, you go:

friends[2]
// Cow

What if you go item 4?

friends[3]
// undefined

What if you get item 3, but the Array is empty?

const friends = []
friends[2]
// undefined

In Part 2 - Getting and Setting Data, we showed you how using lenses allowed safer data access. However, you could still get undefined, and what we really wanted was "is data there or not"? That's where Maybe comes in. We'll create a wrapper function to get our data:

const getFriend = index =>
  friends[index]
  ? Just(friends[index])
  : Nothing()

Here's getting item 3 again:

getFriend(2)
// Just('Cow')

And the non-existent item 4:

getFriend(3)
// Nothing()

And the empty array:

getFriend(2)
// Nothing()

You can use the same technique for Objects as well. Here's a Player:

const player = {
  rightHand: 'scimitar',
  leftHand: null
}

Let's combine get and Maybe to determine if the player has a weapon equipped in her right hand:

const rightHandEquipped = () =>
  get('rightHand', player)
  ? Just(player.rightHand)
  : Nothing()

rightHandEquipped() // Just('scimitar')

And the left:

const leftHandEquipped = () =>
  get('leftHand', player)
  ? Just(player.leftHand)
  : Nothing()

leftHandEquipped() // Nothing()

Getting Data Out

There are two ways in Folktale, getOrElse and value, but value isn't really a public function so use at your own risk (or for debbugging only). The getOrElse works like getOr; either give me the value, else if it's undefined or null, give me the default I provide.

const maybeServerURL = Just('http://cow.com')
const serverURL = maybeServerURL.getOrElse('http://localhost:8080')
console.log(serverURL) // http://cow.com

const maybePingURL = Nothing()
const pingURL = maybePingURL.getOrElse('http://localhost:3000/ping')
console.log(pingURL) // http://localhost:3000/ping

Chaining Maybes

A Promise chain with a bunch of thens will continue modifying the data until the last then. A flow/compose chain will do the same thing. What a Promise chain, and a Maybe chain, have in common, however, is the abort concept. If any one of the Promises error, all the subsequent then's aren't run, and the catch is fired with what rejected the chain. Maybe is similar in that all returned Just's will continue, but a returned Nothing will abort the chain.

Here's a chain of Promises that modify data on down the line:

Promise.resolve('warden jesse')
.then(name => Promise.reject(new Error('b00m b00m')))
.then(name => name.reverse())
.then(name => startCase(name))
.then(name => console.log("name:", name))
.catch(boom => console.log("boom:", boom))
// Jesse Warden

The same chain, but with an error in the first then:

Promise.resolve('warden jesse')
.then(name => Promise.reject(new Error('b00m b00m')))
.then(name => name.reverse())
.then(name => startCase(name))
.then(name => console.log("name:", name))
.catch(boom => console.log("boom:", boom))
// boom: Error: b00m b00m

Maybe's can be chained in a similar way:

console.log(
    Just('warden jesse')
    .chain(name => Just(name.split(' ')))
    .chain(name => Just(name.reverse()))
    .chain(name => Just(startCase(name)))
    .getOrElse('parsing failed, brah')
)
// Jesse Warden

And a Nothing will abort the chain, just like a rejected Promise will abort a Promise chain:


console.log(
    Just('warden jesse')
    .chain(name => Nothing())
    .chain(name => Just(name.reverse()))
    .chain(name => Just(startCase(name)))
    .getOrElse('parsing failed, brah')
)
// parsing failed, brah

Just remember to return a Maybe to keep the chain going. A Promise is flexible in that you can return any value or a Promise; it supports both whereas chain only supports returning a Maybe.

Changing The Value

You can also change the value instead using map which works like an Array's map:

console.log(
    Just('warden jesse')
    .map(name => name.split(' '))
    .map(name => name.reverse())
    .map(name => startCase(name))
    .getOrElse('parsing failed, brah')
)
// Jesse Warden

Just be aware it is mapping the value, so it doesn't handle errors, or handle Nothing... it's just a map:

console.log(
    Just('warden jesse')
    .map(name => Nothing()) // Nothing doesn't have a reverse method...
    .map(name => name.reverse())
    .map(name => startCase(name))
    .getOrElse('parsing failed, brah')
)
// TypeError: name.reverse is not a function

Error Handling

You can use Maybe's to represent data that might work. However, instead of getting a default value, you may want to run a function, just like you do in a Promise's catch function. You can use orElse.

It's ignored if it succeeds with a Just:

console.log(
    Just('warden jesse')
    .chain(name => Just(name.split(' ')))
    .chain(name => Just(name.reverse()))
    .chain(name => Just(startCase(name)))
    .orElse(() => {
        console.log('Parsing failed.')
        return false
    })
)
// folktale:Maybe.Just({ value: "Jesse Warden" })

If any return a Nothing, it'll fire the orElse:

console.log(
    Just('warden jesse')
    .chain(name => Just(name.split(' ')))
    .chain(name => Just(name.reverse()))
    .chain(name => Just(startCase(name)))
    .orElse(() => {
        console.log('Parsing failed.')
        return false
    })
)
// false

Pattern Matching

If you want to react to both, you can use Pattern Matching via matchWith. It's a flexible function, acting like an if/else, and you can return whatever you want from the matcher Object you pass in, and you can ignore the value passed in from the Just as well:

const maybeCow = Just('cow')
const result = maybeCow.matchWith({
    Just: ( { value } ) => `We have a value: ${value}`,
    Nothing: () => 'No cow... sadness.'
})
console.log(result)
// We have a value: cow

const hopefullyNotNothing = Nothing()
const ohNo = hopefullyNotNothing.matchWith({
    Just: () => `Dark sequel, man...`,
    Nothing: () => 'Say my name, Bastion!'
})
console.log(ohNo)
// Say my name, Bastion!

You can end cap your long chains for Just:

console.log(
    Just('warden jesse')
    .chain(name => Just(name.split(' ')))
    .chain(name => Just(name.reverse()))
    .chain(name => Just(startCase(name)))
    .matchWith({
        Just: ({value}) => `Parse result: ${value}`,
        Nothing: () => 'Parsing failed.'
    })
)
// Parse result: Jesse Warden

... as well as long chains for Nothing:

console.log(
    Just('warden jesse')
    .chain(name => Nothing())
    .chain(name => Just(name.reverse()))
    .chain(name => Just(startCase(name)))
    .matchWith({
        Just: ({value}) => `Parse result: ${value}`,
        Nothing: () => 'Parsing failed.'
    })
)
// Parsing failed.

Conclusions

Whenever you are getting data from an outside source of your code like servers, files, and environment variables, Maybe is a great return value choice. When calling functions that may not have the data you're looking for, like looking in Array's or for specific key/values in Objects, Maybe is a much safer, and easier to work with return value, than undefined, null, or -1. Learn more from the Folktale Maybe documentation.

results matching ""

    No results matching ""