Maybe Monad

Rob Hilgefort

Definition

"A monad is a design pattern that allows structuring programs generically while automating away boilerplate code needed by the program logic."

 

A way to wrap a value into a context, and a way to unwrap the value from the context:

What are we Solving?

- Eliminating distributed error checking

- SRP (Single Responsibility Principal)

- Linear control flow

const USERS = [
  { id: 1, friendId: 2, first: 'John', last: 'Smith' },
  { id: 2, friendId: 3, first: 'Rob', last: 'Hilgefort' }, 
  { id: 3, friendId: null, first: 'Jane', last: 'Doe' }, 
]

// findUser :: UserID -> User | undefined
const findUser = (userId) => USERS.find(({ id }) => id === userId)

// getFriend :: User -> User | undefined
const getFriend = ({ friendId }) => findUser(friendId)

// getFullName :: User -> String
const getFullName = ({ first, last }) => {
  if (first !== null && first !== undefined && last !== null && last !== undefined) {
    return `${first} ${last}`
  } else {
    return null
  }
}

// getUserFriendName :: UserID -> String
const getUserFriendName = (userId) => {
  if (userInput !== null && userInput !== undefined) {
    const user = findUser(userId)
    
    if (user !== null && user !== undefined) {
      const friend = getFriend(user.friendId)
      
      if (friend !== null && friend !== undefined) {
        const fullname = getFullName(friend)
      } else {
        throw new Error('Cannot fetch user friend name')  
      }
    } else {
      throw new Error('Cannot fetch user friend name')
    }
  } else {
    throw new Error('Cannot fetch user friend name')
  }
}

Monad Characteristics

Monads are, and depend on:

 

- Functor

- Apply

- Applicative

- Chain

Container

  • Think of this as a new data type
  • Holds data and that's it
  • ... not useful ... yet.
const log = console.log

const Container = (x) => ({
  cata: fn => fn(x),
})

log(Container(3))
// Container(3)

log(Container('hotdogs'))
// Container("hotdogs")

log(Container(Container({ name: 'yoda' })))
// Container(Container({ name: 'yoda' }))

Container('foo').cata(x => log(x))
// 'foo'

Functor

  • A container like before
  • Implements `map`... that's it!
  • Think of it like an `Array` with one element in it.
  • This is the `Identity` Monad (incomplete implementation)
  • Still, only mildly useful ...
const Container = (x) => ({
  map: fn => Container(fn(x)),
  cata: fn => fn(x),
})

Container(3) // Container(3)

Container(3)
  .map(x => x * 2) // Container(6)
  .map(x => x * 10) // Container(16)
  .cata(x => x) // 16

Maybe Monad

  • Still just a container.
  • Actually just a concept- encapsulates `Just` and `Nothing` data types.
  • No more `null` checks everywhere! Only when you want to see what it is!
  • Linear control flow.
  • (still incomplete implementation)
const Just = (x) => ({
  map: fn => Just(fn(x)),
  cata: (whenJust, whenNothing) => whenJust(x),
})

const Nothing = () => ({
  map: () => Nothing,
  cata: (whenJust, whenNothing) => whenNothing(),
})

// MaybeOf :: Any A -> Just A | Nothing
const MaybeOf = x => (x === null || x === undefined)
  ? Nothing()
  : Just(x)

MaybeOf(null) // Nothing
MaybeOf(undefined) // Nothing
MaybeOf('foo') // Just('foo')

MaybeOf('foo')
  .map(x => x.toUpper()) // Just('FOO')
  .map(x => x.split('')) // Just(['F', 'O', 'O'])
  .map(x => x.length()) // Just(3)
  .cata(x => x, () => throw new Error('danger!')) 
  // 16

MaybeOf(null)
  .map(x => x.toUpper()) // Nothing
  .map(x => x.split('')) // Nothing
  .map(x => x.length()) // Nothing
  .cata(x => x, () => throw new Error('danger!')) 
  // Error('danger!')

Flatmap / Chain

  • Flatmap  or Chain
  • Unwraps nested Monads
  • (still incomplete implementation, missing `ap`)
const Just = (x) => ({
  map: fn => Just(fn(x)),
  chain: fn => fn(x),
  cata: (whenJust, whenNothing) => whenJust(x),
})

const Nothing = () => ({
  map: () => Nothing(),
  chain: () => Nothing(),
  cata: (whenJust, whenNothing) => whenNothing(),
})

// MaybeOf :: Any A -> Just A | Nothing
const MaybeOf = x => (x === null || x === undefined)
  ? Nothing()
  : Just(x)

// find :: (Any -> Boolean) -> Array<A> -> Maybe<A>
const find = fn => arr => MaybeOf(arr.find(fn))

// upperLetterInString('b', 'rob')
const upperLetterInString = letter => data =>
  MaybeOf(data)
    .map(x => x.split('')) // Just(['r', 'o', 'b'])
    .map(find(x => x === letter)) // Just(Just('b'))
    // ???

// upperLetterInString('b', 'rob')
const upperLetterInString = letter => data =>
  MaybeOf(data)
    .map(x => x.split('')) // Just(['r', 'o', 'b'])
    .chain(find(x => x === letter)) // Just('b')
    .map(x => x.toUpper()) // Just('B')
    .cata(x => x, () => throw new Error('danger!')) 
    // 'B'

Look Familiar?

  • You probably already use Monads
  • JS Promises are unlawful Monads.
  • then === mapOrChain
const fetchUser = (userId) => new Promise((success, error) =>
  userId === undefined 
    ? error('no user') 
    : success({ 
        userId, 
        friend: 5, 
        name: 'someone' ,
      })
)

const transform = user => ({
  ...user,
  name: user.name.toUpper(),
})

const fetchAndTransformUser = (userId) =>
  fetchUser(userId) // Promise(User)
    .then(transform) // Promise(User)
    .then(user => fetchUser(user.friend)) // Promise(User)
    // ^ should be Promise(Promise(User)) if lawful
    .then(transform) // Proimse(User)
  

Resources

Maybe Monad

By rjhilgefort

Maybe Monad

  • 47