Sanctuary
Sanctuary is a small functional programming library inspired by Haskell and PureScript. It depends on and works nicely with Ramda. Sanctuary makes it possible to write safe code without null checks.
In JavaScript it's trivial to introduce a possible run-time type error:
words[0].toUpperCase()
If words
is []
we'll get a familiar error at run-time:
TypeError: Cannot read property 'toUpperCase' of undefined
Sanctuary gives us a fighting chance of avoiding such errors. We might write:
R.map(R.toUpper, S.head(words))
Types
Sanctuary uses Haskell-like type signatures to describe the types of
values, including functions. 'foo'
, for example, has type String
;
[1, 2, 3]
has type [Number]
. The arrow (->
) is used to express a
function's type. Math.abs
, for example, has type Number -> Number
.
That is, it takes an argument of type Number
and returns a value of
type Number
.
R.map
has type (a -> b) -> [a] -> [b]
. That is, it takes
an argument of type a -> b
and returns a value of type [a] -> [b]
.
a
and b
are type variables: applying R.map
to a value of type
String -> Number
will give a value of type [String] -> [Number]
.
Sanctuary embraces types. JavaScript doesn't support algebraic data types, but these can be simulated by providing a group of constructor functions whose prototypes provide the same set of methods. A value of the Maybe type, for example, is created via the Nothing constructor or the Just constructor.
It's necessary to extend Haskell's notation to describe implicit arguments
to the methods provided by Sanctuary's types. In x.map(y)
, for example,
the map
method takes an implicit argument x
in addition to the explicit
argument y
. The type of the value upon which a method is invoked appears
at the beginning of the signature, separated from the arguments and return
value by a squiggly arrow (~>
). The type of the map
method of the Maybe
type is written Maybe a ~> (a -> b) -> Maybe b
. One could read this as:
When the map
method is invoked on a value of type Maybe a
(for any type a
) with an argument of type a -> b
(for any type b
),
it returns a value of type Maybe b
.
API
Combinator
K :: a -> b -> a
The K combinator. Takes two values and returns the first. Equivalent to
Haskell's const
function.
> S.K('foo', 'bar')
"foo"
> R.map(S.K(42), R.range(0, 5))
[42, 42, 42, 42, 42]
Maybe type
Maybe :: Type
The Maybe type represents optional values: a value of type Maybe a
is
either a Just whose value is of type a
or a Nothing (with no value).
The Maybe type satisfies the Monoid and Monad specifications.
Maybe.empty :: -> Maybe a
Returns a Nothing.
> S.Maybe.empty()
Nothing()
Maybe.of :: a -> Maybe a
Takes a value of any type and returns a Just with the given value.
> S.Maybe.of(42)
Just(42)
Maybe#ap :: Maybe (a -> b) ~> Maybe a -> Maybe b
Takes a value of type Maybe a
and returns a Nothing unless this
is a Just and the argument is a Just, in which case it returns a
Just whose value is the result of of applying this Just's value to
the given Just's value.
> S.Nothing().ap(S.Just(42))
Nothing()
> S.Just(R.inc).ap(S.Nothing())
Nothing()
> S.Just(R.inc).ap(S.Just(42))
Just(43)
Maybe#chain :: Maybe a ~> (a -> Maybe b) -> Maybe b
Takes a function and returns this
if this
is a Nothing; otherwise
it returns the result of applying the function to this Just's value.
> S.Nothing().chain(S.parseFloat)
Nothing()
> S.Just('xxx').chain(S.parseFloat)
Nothing()
> S.Just('12.34').chain(S.parseFloat)
Just(12.34)
Maybe#concat :: Maybe a ~> Maybe a -> Maybe a
Returns the result of concatenating two Maybe values of the same type.
a
must have a Semigroup (indicated by the presence of a concat
method).
If this
is a Nothing and the argument is a Nothing, this method returns
a Nothing.
If this
is a Just and the argument is a Just, this method returns a
Just whose value is the result of concatenating this Just's value and
the given Just's value.
Otherwise, this method returns the Just.
> S.Nothing().concat(S.Nothing())
Nothing()
> S.Just([1, 2, 3]).concat(S.Just([4, 5, 6]))
Just([1, 2, 3, 4, 5, 6])
> S.Nothing().concat(S.Just([1, 2, 3]))
Just([1, 2, 3])
> S.Just([1, 2, 3]).concat(S.Nothing())
Just([1, 2, 3])
Maybe#empty :: Maybe a ~> Maybe a
Returns a Nothing.
> S.Just(42).empty()
Nothing()
Maybe#equals :: Maybe a ~> b -> Boolean
Takes a value of any type and returns true
if:
-
it is a Nothing and
this
is a Nothing; or -
it is a Just and
this
is a Just, and their values are equal according toR.equals
.
> S.Nothing().equals(S.Nothing())
true
> S.Nothing().equals(null)
false
> S.Just([1, 2, 3]).equals(S.Just([1, 2, 3]))
true
> S.Just([1, 2, 3]).equals(S.Just([3, 2, 1]))
false
> S.Just([1, 2, 3]).equals(S.Nothing())
false
Maybe#filter :: Maybe a ~> (a -> Boolean) -> Maybe a
Takes a predicate and returns this
if this
is a Just whose value
satisfies the predicate; Nothing otherwise.
> S.Just(42).filter(function(n) { return n % 2 === 0; })
Just(42)
> S.Just(43).filter(function(n) { return n % 2 === 0; })
Nothing()
Maybe#map :: Maybe a ~> (a -> b) -> Maybe b
Takes a function and returns this
if this
is a Nothing; otherwise
it returns a Just whose value is the result of applying the function to
this Just's value.
> S.Nothing().map(R.inc)
Nothing()
> S.Just(42).map(R.inc)
Just(43)
Maybe#of :: Maybe a ~> b -> Maybe b
Takes a value of any type and returns a Just with the given value.
> S.Nothing().of(42)
Just(42)
Maybe#toBoolean :: Maybe a ~> Boolean
Returns false
if this
is a Nothing; true
if this
is a Just.
> S.Nothing().toBoolean()
false
> S.Just(42).toBoolean()
true
Maybe#toString :: Maybe a ~> String
Returns the string representation of the Maybe.
> S.Nothing().toString()
"Nothing()"
> S.Just([1, 2, 3]).toString()
"Just([1, 2, 3])"
Maybe#type :: Type
A reference to the Maybe type. Useful for determining whether two
values such as S.Nothing()
and S.Just(42)
are of the same type.
Nothing :: -> Maybe a
Returns a Nothing. Though this is a constructor function the new
keyword needn't be used.
> S.Nothing()
Nothing()
Just :: a -> Maybe a
Takes a value of any type and returns a Just with the given value.
Though this is a constructor function the new
keyword needn't be
used.
> S.Just(42)
Just(42)
fromMaybe :: a -> Maybe a -> a
Takes a default value and a Maybe, and returns the Maybe's value if the Maybe is a Just; the default value otherwise.
> S.fromMaybe(0, S.Just(42))
42
> S.fromMaybe(0, S.Nothing())
0
toMaybe :: a? -> Maybe a
Takes a value and returns Nothing if the value is null or undefined; Just the value otherwise.
> S.toMaybe(null)
Nothing()
> S.toMaybe(42)
Just(42)
encase :: (* -> a) -> (* -> Maybe a)
Takes a function f
which may throw and returns a curried function
g
which will not throw. The result of applying g
is determined by
applying f
to the same arguments: if this succeeds, g
returns Just
the result; otherwise g
returns Nothing.
> S.encase(eval)('1 + 1')
Just(2)
> S.encase(eval)('1 +')
Nothing()
Either type
Either :: Type
The Either type represents values with two possibilities: a value of type
Either a b
is either a Left whose value is of type a
or a Right whose
value is of type b
.
The Either type satisfies the Semigroup and Monad specifications.
Either.of :: b -> Either a b
Takes a value of any type and returns a Right with the given value.
> S.Either.of(42)
Right(42)
Either#ap :: Either a (b -> c) ~> Either a b -> Either a c
Takes a value of type Either a b
and returns a Left unless this
is a Right and the argument is a Right, in which case it returns
a Right whose value is the result of applying this Right's value to
the given Right's value.
> S.Left('Cannot divide by zero').ap(S.Right(42))
Left("Cannot divide by zero")
> S.Right(R.inc).ap(S.Left('Cannot divide by zero'))
Left("Cannot divide by zero")
> S.Right(R.inc).ap(S.Right(42))
Right(43)
Either#chain :: Either a b ~> (b -> Either a c) -> Either a c
Takes a function and returns this
if this
is a Left; otherwise
it returns the result of applying the function to this Right's value.
> void (sqrt = function(n) { return n < 0 ? S.Left('Cannot represent square root of negative number') : S.Right(Math.sqrt(n)); })
undefined
> S.Left('Cannot divide by zero').chain(sqrt)
Left("Cannot divide by zero")
> S.Right(-1).chain(sqrt)
Left("Cannot represent square root of negative number")
> S.Right(25).chain(sqrt)
Right(5)
Either#concat :: Either a b ~> Either a b -> Either a b
Returns the result of concatenating two Either values of the same type.
a
must have a Semigroup (indicated by the presence of a concat
method), as must b
.
If this
is a Left and the argument is a Left, this method returns a
Left whose value is the result of concatenating this Left's value and
the given Left's value.
If this
is a Right and the argument is a Right, this method returns a
Right whose value is the result of concatenating this Right's value and
the given Right's value.
Otherwise, this method returns the Right.
> S.Left('abc').concat(S.Left('def'))
Left("abcdef")
> S.Right([1, 2, 3]).concat(S.Right([4, 5, 6]))
Right([1, 2, 3, 4, 5, 6])
> S.Left('abc').concat(S.Right([1, 2, 3]))
Right([1, 2, 3])
> S.Right([1, 2, 3]).concat(S.Left('abc'))
Right([1, 2, 3])
Either#equals :: Either a b ~> c -> Boolean
Takes a value of any type and returns true
if:
-
it is a Left and
this
is a Left, and their values are equal according toR.equals
; or -
it is a Right and
this
is a Right, and their values are equal according toR.equals
.
> S.Right([1, 2, 3]).equals(S.Right([1, 2, 3]))
true
> S.Right([1, 2, 3]).equals(S.Left([1, 2, 3]))
false
> S.Right(42).equals(42)
false
Either#map :: Either a b ~> (b -> c) -> Either a c
Takes a function and returns this
if this
is a Left; otherwise it
returns a Right whose value is the result of applying the function to
this Right's value.
> S.Left('Cannot divide by zero').map(R.inc)
Left("Cannot divide by zero")
> S.Right(42).map(R.inc)
Right(43)
Either#of :: Either a b ~> b -> Either a b
Takes a value of any type and returns a Right with the given value.
> S.Left('Cannot divide by zero').of(42)
Right(42)
Either#toBoolean :: Either a b ~> Boolean
Returns false
if this
is a Left; true
if this
is a Right.
> S.Left(42).toBoolean()
false
> S.Right(42).toBoolean()
true
Either#toString :: Either a b ~> String
Returns the string representation of the Either.
> S.Left('Cannot divide by zero').toString()
"Left(\\"Cannot divide by zero\\")"
> S.Right([1, 2, 3]).toString()
"Right([1, 2, 3])"
Either#type :: Type
A reference to the Either type. Useful for determining whether two
values such as S.Left('Cannot divide by zero')
and S.Right(42)
are of the same type.
Left :: a -> Either a b
Takes a value of any type and returns a Left with the given value.
Though this is a constructor function the new
keyword needn't be
used.
> S.Left('Cannot divide by zero')
Left("Cannot divide by zero")
Right :: b -> Either a b
Takes a value of any type and returns a Right with the given value.
Though this is a constructor function the new
keyword needn't be
used.
> S.Right(42)
Right(42)
either :: (a -> c) -> (b -> c) -> Either a b -> c
Takes two functions and an Either, and returns the result of applying the first function to the Left's value, if the Either is a Left, or the result of applying the second function to the Right's value, if the Either is a Right.
> S.either(R.toUpper, R.toString, S.Left('Cannot divide by zero'))
"CANNOT DIVIDE BY ZERO"
> S.either(R.toUpper, R.toString, S.Right(42))
"42"
Control
and :: a -> a -> a
Takes two values of the same type and returns the second value
if the first is "true"; the first value otherwise. An array is
considered "true" if its length is greater than zero. The Boolean
value true
is also considered "true". Other types must provide
a toBoolean
method.
> S.and(S.Just(1), S.Just(2))
Just(2)
> S.and(S.Nothing(), S.Just(3))
Nothing()
or :: a -> a -> a
Takes two values of the same type and returns the first value if it
is "true"; the second value otherwise. An array is considered "true"
if its length is greater than zero. The Boolean value true
is also
considered "true". Other types must provide a toBoolean
method.
> S.or(S.Just(1), S.Just(2))
Just(1)
> S.or(S.Nothing(), S.Just(3))
Just(3)
xor :: a -> a -> a
Takes two values of the same type and returns the "true" value
if one value is "true" and the other is "false"; otherwise it
returns the type's "false" value. An array is considered "true"
if its length is greater than zero. The Boolean value true
is
also considered "true". Other types must provide toBoolean
and
empty
methods.
> S.xor(S.Nothing(), S.Just(1))
Just(1)
> S.xor(S.Just(2), S.Just(3))
Nothing()
List
slice :: Number -> Number -> [a] -> Maybe [a]
Returns Just a list containing the elements from the supplied list from a beginning index (inclusive) to an end index (exclusive). Returns Nothing unless the start interval is less than or equal to the end interval, and the list contains both (half-open) intervals. Accepts negative indices, which indicate an offset from the end of the list.
Dispatches to its third argument's slice
method if present. As a
result, one may replace [a]
with String
in the type signature.
> S.slice(1, 3, ['a', 'b', 'c', 'd', 'e'])
Just(["b", "c"])
> S.slice(-2, -0, ['a', 'b', 'c', 'd', 'e'])
Just(["d", "e"])
> S.slice(2, -0, ['a', 'b', 'c', 'd', 'e'])
Just(["c", "d", "e"])
> S.slice(1, 6, ['a', 'b', 'c', 'd', 'e'])
Nothing()
> S.slice(2, 6, 'banana')
Just("nana")
at :: Number -> [a] -> Maybe a
Takes an index and a list and returns Just the element of the list at the index if the index is within the list's bounds; Nothing otherwise. A negative index represents an offset from the length of the list.
> S.at(2, ['a', 'b', 'c', 'd', 'e'])
Just("c")
> S.at(5, ['a', 'b', 'c', 'd', 'e'])
Nothing()
> S.at(-2, ['a', 'b', 'c', 'd', 'e'])
Just("d")
head :: [a] -> Maybe a
Takes a list and returns Just the first element of the list if the list contains at least one element; Nothing if the list is empty.
> S.head([1, 2, 3])
Just(1)
> S.head([])
Nothing()
last :: [a] -> Maybe a
Takes a list and returns Just the last element of the list if the list contains at least one element; Nothing if the list is empty.
> S.last([1, 2, 3])
Just(3)
> S.last([])
Nothing()
tail :: [a] -> Maybe [a]
Takes a list and returns Just a list containing all but the first of the list's elements if the list contains at least one element; Nothing if the list is empty.
> S.tail([1, 2, 3])
Just([2, 3])
> S.tail([])
Nothing()
init :: [a] -> Maybe [a]
Takes a list and returns Just a list containing all but the last of the list's elements if the list contains at least one element; Nothing if the list is empty.
> S.init([1, 2, 3])
Just([1, 2])
> S.init([])
Nothing()
take :: Number -> [a] -> Maybe [a]
Returns Just the first N elements of the given collection if N is
greater than or equal to zero and less than or equal to the length
of the collection; Nothing otherwise. Supports Array, String, and
any other collection type which provides a slice
method.
> S.take(2, ['a', 'b', 'c', 'd', 'e'])
Just(["a", "b"])
> S.take(4, 'abcdefg')
Just("abcd")
> S.take(4, ['a', 'b', 'c'])
Nothing()
drop :: Number -> [a] -> Maybe [a]
Returns Just all but the first N elements of the given collection
if N is greater than or equal to zero and less than or equal to the
length of the collection; Nothing otherwise. Supports Array, String,
and any other collection type which provides a slice
method.
> S.drop(2, ['a', 'b', 'c', 'd', 'e'])
Just(["c", "d", "e"])
> S.drop(4, 'abcdefg')
Just("efg")
> S.drop(4, 'abc')
Nothing()
find :: (a -> Boolean) -> [a] -> Maybe a
Takes a predicate and a list and returns Just the leftmost element of the list which satisfies the predicate; Nothing if none of the list's elements satisfies the predicate.
> S.find(function(n) { return n < 0; }, [1, -2, 3, -4, 5])
Just(-2)
> S.find(function(n) { return n < 0; }, [1, 2, 3, 4, 5])
Nothing()
indexOf :: a -> [a] -> Maybe Number
Takes a value of any type and a list, and returns Just the index of the first occurrence of the value in the list, if applicable; Nothing otherwise.
Dispatches to its second argument's indexOf
method if present.
As a result, String -> String -> Maybe Number
is an alternative
type signature.
> S.indexOf('a', ['b', 'a', 'n', 'a', 'n', 'a'])
Just(1)
> S.indexOf('x', ['b', 'a', 'n', 'a', 'n', 'a'])
Nothing()
> S.indexOf('an', 'banana')
Just(1)
> S.indexOf('ax', 'banana')
Nothing()
lastIndexOf :: a -> [a] -> Maybe Number
Takes a value of any type and a list, and returns Just the index of the last occurrence of the value in the list, if applicable; Nothing otherwise.
Dispatches to its second argument's lastIndexOf
method if present.
As a result, String -> String -> Maybe Number
is an alternative
type signature.
> S.lastIndexOf('a', ['b', 'a', 'n', 'a', 'n', 'a'])
Just(5)
> S.lastIndexOf('x', ['b', 'a', 'n', 'a', 'n', 'a'])
Nothing()
> S.lastIndexOf('an', 'banana')
Just(3)
> S.lastIndexOf('ax', 'banana')
Nothing()
pluck :: String -> [{String: *}] -> [Maybe *]
Takes a list of objects and plucks the value of the specified key for each object in the list. Returns the value wrapped in a Just if an object has the key and a Nothing if it does not.
> S.pluck('a', [{a: 1, b: 2}, {a: 4, b: 5}, {b: 3, c: 7}])
[Just(1), Just(4), Nothing()]
> S.pluck('x', [{x: 1}, {x: 2}, {x: undefined}])
[Just(1), Just(2), Just(undefined)]
Object
get :: String -> Object -> Maybe *
Takes a property name and an object and returns Just the value of the specified property of the object if the object has such an own property; Nothing otherwise.
> S.get('x', {x: 1, y: 2})
Just(1)
> S.get('toString', {x: 1, y: 2})
Nothing()
gets :: [String] -> Object -> Maybe *
Takes a list of property names and an object and returns Just the value at the path specified by the list of property names if such a path exists; Nothing otherwise.
> S.gets(['a', 'b', 'c'], {a: {b: {c: 42}}})
Just(42)
> S.gets(['a', 'b', 'c'], {})
Nothing()
Parse
parseDate :: String -> Maybe Date
Takes a string and returns Just the date represented by the string if it does in fact represent a date; Nothing otherwise.
> S.parseDate('2011-01-19T17:40:00Z')
Just(new Date("2011-01-19T17:40:00.000Z"))
> S.parseDate('today')
Nothing()
parseFloat :: String -> Maybe Number
Takes a string and returns Just the number represented by the string if it does in fact represent a number; Nothing otherwise.
> S.parseFloat('-123.45')
Just(-123.45)
> S.parseFloat('foo.bar')
Nothing()
parseInt :: Number -> String -> Maybe Number
Takes a radix (an integer between 2 and 36 inclusive) and a string, and returns Just the number represented by the string if it does in fact represent a number in the base specified by the radix; Nothing otherwise.
This function is stricter than parseInt
: a string
is considered to represent an integer only if all its non-prefix
characters are members of the character set specified by the radix.
> S.parseInt(10, '-42')
Just(-42)
> S.parseInt(16, '0xFF')
Just(255)
> S.parseInt(16, '0xGG')
Nothing()
parseJson :: String -> Maybe *
Takes a string which may or may not be valid JSON, and returns Just
the result of applying JSON.parse
to the string if valid; Nothing
otherwise.
> S.parseJson('["foo","bar","baz"]')
Just(["foo", "bar", "baz"])
> S.parseJson('[')
Nothing()
RegExp
match :: RegExp -> String -> Maybe [Maybe String]
Takes a pattern and a string, and returns Just a list of matches
if the pattern matches the string; Nothing otherwise. Each match
has type Maybe String
, where a Nothing represents an unmatched
optional capturing group.
> S.match(/(good)?bye/, 'goodbye')
Just([Just("goodbye"), Just("good")])
> S.match(/(good)?bye/, 'bye')
Just([Just("bye"), Nothing()])