Painlessly enhance your JavaScript with monadic types.
const definition = m.define({
firstName: m.string,
lastName: Option.as(m.string),
email: Option.as(m.string),
phoneNumber: Either.as(m.string, m.number)
});
definition.parse({
firstName: "Bob",
lastName: "McAdoo",
email: null,
phoneNumber: "444-444-4444"
})
// Right({
// firstName: "Bob",
// lastName: Some("McAdoo"),
// email: None(),
// phoneNumber: Left("444-444-4444")
// })In brief, a Monad is a wrapper around a value that allows you to make safe, composable operations. It eliminates the need to throw errors, as well as the need for things like null values. A JavaScript Array is a monad-like data type, but doesn't fully satisfy the rules of being a monad, given that it doesn't have a bind (or flatMap) function.
A monad is a type that satisfies the following methods:
- A composition function, called
>>=,bind, orflatMap, that has type(A) => M<B>, and returns typeM<B>. - A
return(orunit), that has type(A) => M<A>.
Monads are common in functional programming languages.
Here are some great resources that discuss monads:
- Brian Beckman: Don't Fear The Monad
- James Coglan: Promises are the monad of asynchronous programming
- Douglas Crockford: Monads and Gonads
In this library, the monadic bind is called flatMap, in order to not conflict with Function.prototype.bind. The monadic return is called unit, to not cause any confusion with the return keyword.
npm install mandolin
bower install mandolinCurrently, you must use a CommonJS loader to require this module.
const m = require('mandolin');
const { Option, Some, None } = m;An Option is a type comprising of Some and None. A value of type Option can either be a Some, in which it holds a value, or a None, in which it holds no value. This is used in lieu of null and undefined.
Example:
const bobsEmail = new Some("bob@mcadoo.com");
// We have Bob's email
const sandrasEmail = new None();
// we do not have Sandra's emailThe way to get a reference to the actual value is through either map, flatMap or match.
bobsEmail.map((email) => doThingWithEmail(email));This is similar to writing a null check:
if (email !== null || email !== undefined) {
doThingWithEmail(email)
}A disjoint union of Left and Right, and is right-biased. map and flatMap are only called if it is a Right. This is similar to an Option, in that Left : None :: Right : Some. The difference is that a Left can also hold values.
Mandolin uses Reads objects, which are not monads, but whose purpose are to serialize values into algebraic types (Option, Either).
A Reads is created with this signature:
const r = new Reads(reader);Where reader is a function that accepts a value, and returns an Either; a Right with a successful value, or a Left with an unsuccessful value. For example, a reader of even numbers would look like this:
const reader = (v) => v % 2 ? new Left("number is not even") : new Right(v);You can think of a Reads as a rule for serializing something. Reads can be freely chained together, to further define a rule.
const greaterThan10 = new Reads((v) => v < 10 ? new Left("number is less than 10") : new Right(v));
const evenAndGreaterThan10 = r.with(greaterThan10);
evenAndGreaterThan10.getValue(4) // Left("number is not event")
evenAndGreaterThan10.getValue(14) // Right(14)Reads can be chained via with, map and flatMap.
Along with Reads, Mandolin has Parsers that accept arbitrary objects of key --> Reads pairs, and returns an either with a successful serialized object, or an error.
This provides a generic way to validate objects, and coerce values into algebraic data types.
You may create a definition with this signature:
const definition = m.define({
age: m.number,
name: m.string
});In this example, m.number and m.string are pre-baked Reads for primitives. The following are all available:
m.numberm.stringm.booleanm.arraym.objectm.undefinedm.nullm.any
The return value of m.define is a Parser, which is a special type of Reads. Thus, nested objects are simply notated as such:
const definition = m.define({
age: m.number,
name: m.string,
address: m.define({
street1: m.string
})
});Option and Either both have a generic way to create Reads.
const definition = m.define({
meta: Option.reads,
email: Option.as(m.string)
});Option.reads will return either a None(), or a Some(<Any>). Option.as() accepts a Reads argument, to perform further validation after casting away null or undefined.
Once a definition has been instantiated, calling parse will return an Either, with the Right holding the successfully parsed value, and the Left holding the first error encountered while parsing.
definition.parse({
meta: "foo",
email: null
})
// returns Right({
// meta: Some("foo"),
// email: None()
// });This library makes no assumptions about type safety. The approach, rather, is to use Reads combinators to serialize values that follow certain sets of rules. For example, in Scala, an Option of a String is notated as such:
Option[String]The seemingly equivalent example in our library is this:
Option.as(m.string)The difference is, Option.as(m.String) is not a type, but a rule for reading in values. I can just as easily write another rule for Option.as(evenNumber). Essentially, this is the same thing as types, but I think makes more sense in a world that doesn't perform any compile-time type checking.
JavaScript doesn't have pattern matching built into the language. However, each algebraic data type in this library comes with a match function that behaves like pattern matching.
new Left("foo").match({
Left (str) { ... },
Right (str) { ... }
});The return value of match is the return value of whichever function ends up being called. This is analagous to Scala's match.
foo match {
case Left(_) => ???
case Right(_) => ???
}This module is fantasy-land compliant. chain is an alias of flatMap, and of is an alias of unit.