Null Can't Hurt You Anymore

Stefano Vozza

The Problem

  • Trivial to introduce runtime errors in JavaScript

    • Null references the biggest offender

  • Happen when least expected

  • Indecipherable error messages:

    • TypeError: undefined is not a function

    • TypeError: Cannot read property 'foo' of null

The Solution...Maybe

  • Sometimes known as Option type (Scala, Java)

  • Encode the possibility of absence during an operation

  • Handle the null case in a principled, composable way

Maybe

  • Presence = Just

  • Absence = Nothing                                             

// toMaybe :: a? -> Maybe a
const toMaybe = x => isNil(x) ? Nothing() : Just(x);

toMaybe(1)     // => Just(1)
toMaybe(null)  // => Nothing()

Maybe

  • Eventually need to access the value inside

  • Enforces fallback behaviour                                            
// fromMaybe :: a -> Maybe a -> a
const fromMaybe = curry((def, m) => m.isJust ? m.value : def);

fromMaybe(2, Just(1));      // => 1
fromMaybe(2, Nothing());    // => 2


Map
 

Arrays?

  • Arrays are only one of many mappable things

  • Map applies a function to the value(s) inside
function f(xs = []) {
    return xs.map(inc).filter(isOdd);
}


f(null) // => []
f([1, 2, 3, 4, 5, 6]) // => [3, 5, 7]

Map

  • Mapping function applied on success case

  • No-op for the failure case                                         
const obj = {a: ‘abc’};


compose(map(toUpper), toMaybe, prop('a'))(obj);   // => Just(‘ABC’)
compose(map(toUpper), toMaybe, prop('b'))(obj);   // => Nothing()
compose(toUpper, prop('b'))(obj);                 // => TypeError

Composing

  • How do we sequence multiple operations that might return null values?

                                            
// safeProp :: String -> {String: a} -> Maybe a
const safeProp = curry((x, obj) => toMaybe(prop(x, obj)));
        
compose(map(safeProp('b')), safeProp('a'))({a: {b: 9}});

Just({b: 9})

Just(Just(9))

Chain (aka flatMap)

  • The magic sauce that allows Maybes to be composed

                                            
compose(chain(safeProp('b')), safeProp('a'))({a: {b: 9}})

Just({b: 9})

Just(Just(9))

Just(9)

Separate Null Safety From Core Logic

const xs = [{id: 1, a: {b: 1}}, 
            {id: 2, a: 4}, 
            {id: 3, d: 9}, 
            {id: 4, a: {b: {c: 9}}}];


let obj = find(x => x.id === 1, xs);
let num;
if(obj && obj.a && obj.a.b) {    // <= does not compose!
   num = (obj.a.b.c || 9)  + 1
}

Code The Happy Path

// findById :: (Integer -> Boolean) -> [{String: a}] -> Maybe a
const findById = curry((id, xs) => toMaybe(find(x => x.id === id, xs)));


//safePath :: [String] -> {String: a} -> Maybe a
const safePath = curry((path, obj) => reduce((maybe, prop) => {
       return chain(safeProp(prop), maybe);
}, toMaybe(obj), path));


compose(fromMaybe(10), map(add(1)), chain(safePath(['a', 'b', 'c'])), findById(1))(xs);
// => 10

Other Types

  • Either

    • Also encapsulates computations that may fail

    • Unlike Maybes, provide some information about the failure

    • Useful for making functions that throw errors pure

  • Success = Right

  • Failure = Left

Either

// encaseEither :: (a -> a) -> a -> Either Error a
const encaseEither = curry((f, x) => {
 try {
     return Right(f(x));
 } catch (e) {
   return Left(e);
 }
});

encaseEither(JSON.parse, '{"a": 1}'); // => Right({a: 1})
encaseEither(JSON.parse, '{a: 1}'); 
// => Left(Error(SyntaxError: Unexpected token a in JSON at position 1))

Composition

// parseDate :: String -> Either Error Date
const parseDate = dateStr => {
 const date = new Date(dateStr);
 return Number.isNaN(date.getTime()) ? Left(new Error('Invalid Date')) : Right(date)
};

const jsonStr = '{"date": "2014-4-9"}';
compose(chain(parseDate), map(prop('date')), encaseEither(JSON.parse))(jsonStr);
// => Right(Date('Wed Apr 09 2014 00:00:00 GMT+0100 (GMT)'))

Future

  • Encode time-dependent computations that may fail

    • Enforces error handling

  • Allow sequencing of asynchronous actions

  • Lazy

    • Must explicitly invoke fork() method to evaluate

  • Isolate a program’s side effects

Promise

  • then === map + chain + fork                            

  • Eagerly evaluated

    • Side effects can’t be isolated

  • Resolved once

  • Implicit error handling

    • Thrown errors caught

    • Error callback optional

Conversion

// readFile :: String -> Future Error String
const readFile = file => Future((reject, resolve) => {
   fs.readFile(file, 'utf8', (err, data) => {
       if(err) return reject(err);
       return resolve(data);
   })
});

// get :: String -> Future Error Object
const get = url => Future((reject, resolve) => {
   request(url, (err, res, body) => {
       if(err) return reject(err);
       return resolve(body);
   });
});

Composition

// describe computation
const file = '/home/proj/src/package.json'
const fut = compose(chain(get), map(path(['author', ‘url’])), map(JSON.parse), readFile)(file);

// execute computation (both callbacks are mandatory!)
fut.fork(console.error, console.log);

Summary

  • Common interface underpinned by map and chain

  • Reduce boilerplate, avoid ad hoc type safety

  • Isolate unsafe parts of a program

  • Code the happy path

Questions?

deck

By Stefano Vozza