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
deck
- 176