Is there such a concept as a synchronous promise?
Benjamin is absolutely right. Promises are a type of monad. However, they are not the only type.
If you're not already aware of it then you're probably wondering what a monad is. There are lots of explanations of monads available online. However, most of them suffer from the monad tutorial fallacy.
In brief, the fallacy is that most people who understand monads don't really know how to explain the concept to others. In simple terms, monads are an abstract concept and human beings find it difficult to grasp abstract concepts. However, it's easy for humans to grok concrete concepts.
So let's begin our conquest to understand monads starting with a concrete concept. As I said, monads are an abstract concept. This means that a monad is an interface without an implementation (i.e. it defines certain operations and specifies what those operations should do, without specifying how it must be done).
Now, there are different types of monads. Each type of monad is concrete (i.e. it defines an implementation of the monad interface). Promises are a type of monad. Hence, promises are a concrete example of a monad. Thus, if we study promises then we can begin to understand monads.
So where do we begin? Fortunately, the user spike gave us a good starting point in his comment to your question:
One instance I can think of is chaining promises together with sync code. While finding an answer for this question: Generating AJAX Request Dynamically Based on Scenario I wrapped a synchronous call in a promise in order to be able to chain them with other promises.
So let's look at his code:
var run = function() {
getScenario()
.then(mapToInstruction)
.then(waitForTimeout)
.then(callApi)
.then(handleResults)
.then(run);
};
Here the run
function returns a promise which is composed of the promises returned by getScenario
, mapToInstruction
, waitForTimeout
, callApi
, handleResults
and run
itself chained together.
Now, before we proceed I want to introduce to you a new notation to visualize what these functions are doing:
run :: Unit -> Deferred a
getScenario :: Unit -> Deferred Data
mapToInstruction :: Data -> Deferred Instruction
waitForTimeout :: Instruction -> Deferred Instruction
callApi :: Instruction -> Deferred Data
handleResults :: Data -> Deferred Unit
So here's the breakdown:
- The
::
symbol means “is of the type” and the ->
symbol means “to”. Hence for example, run :: Unit -> Deferred a
reads as “run
is of the type Unit
to Deferred a
”.
- That means that
run
is a function which takes a Unit
value (i.e. no arguments) and returns a value of type Deferred a
.
- Here,
a
means any type. We don't know what type a
is and we don't care what type a
is. Hence, it can be any type whatsoever.
- Here,
Deferred
is a promise data type (with a different name) and Deferred a
means that when the promise is resolved it yields a value of type a
.
There are several things we can learn from the above visualization:
- Each function takes some value and returns a promise.
The resolved value returned by each promise becomes the input to the next function:
run :: Unit -> Deferred a
getScenario :: Unit -> Deferred Data
getScenario :: Unit -> Deferred Data
mapToInstruction :: Data -> Deferred Instruction
mapToInstruction :: Data -> Deferred Instruction
waitForTimeout :: Instruction -> Deferred Instruction
waitForTimeout :: Instruction -> Deferred Instruction
callApi :: Instruction -> Deferred Data
callApi :: Instruction -> Deferred Data
handleResults :: Data -> Deferred Unit
handleResults :: Data -> Deferred Unit
run :: Unit -> Deferred a
The next function cannot execute until the previous promise is resolved because it has to make use of the resolved value of the previous promise.
Now, as I mentioned earlier a monad is an interface which defines certain operations. One of the operations that the monad interface provides is the operation of chaining monads. In case of promises this is the then
method. For example:
getScenario().then(mapToInstruction)
We know that:
getScenario :: Unit -> Deferred Data
mapToInstruction :: Data -> Deferred Instruction
Hence:
getScenario() :: Deferred Data -- because when called, getScenario
-- returns a Deferred Data value
We also know that:
getScenario().then(mapToInstruction) :: Deferred Instruction
Thus, we can deduce:
then :: Deferred a -> (a -> Deferred b) -> Deferred b
In words, “then
is a function which takes two arguments (a value of the type Deferred a
and a function of the type a -> Deferred b
) and returns a value of type Deferred b
.” Hence:
then :: Deferred a -> (a -> Deferred b) -> Deferred b
getScenario() :: Deferred Data
-- Therefore, since a = Data
getScenario().then :: (Data -> Deferred b) -> Deferred b
mapToInstruction :: Data -> Deferred Instruction
-- Therefor, since b = Instruction
getScenario().then(mapInstruction) :: Deferred Instruction
So we got our first monad operation:
then :: Deferred a -> (a -> Deferred b) -> Deferred b
However, this operation is concrete. It is specific to promises. We want an abstract operation that can work for any monad. Hence, we generalize the function so that it can work for any monad:
bind :: Monad m => m a -> (a -> m b) -> m b
Note that this bind
function has nothing to do with Function.prototype.bind
. This bind
function is a generalization of the then
function. Then then
function is specific to promises. However, the bind
function is generic. It can work for any monad m
.
The fat arrow =>
means bounded quantification. If a
and b
can be of any type whatsoever then m
can be of any type whatsoever which implements the monad interface. We don't care what type m
is as long as it implements the monad interface.
This is how we would implement and use the bind
function in JavaScript:
function bind(m, f) {
return m.then(f);
}
bind(getScenario(), mapToInstruction);
How is this generic? Well, I could create a new data type which implements the then
function:
// Identity :: a -> Identity a
function Identity(value) {
this.value = value;
}
// then :: Identity a -> (a -> Identity b) -> Identity b
Identity.prototype.then = function (f) {
return f(this.value);
};
// one :: Identity Number
var one = new Identity(1);
// yes :: Identity Boolean
var yes = bind(one, isOdd);
// isOdd :: Number -> Identity Boolean
function isOdd(n) {
return new Identity(n % 2 === 1);
}
Instead of bind(one, isOdd)
I could just have easily written one.then(isOdd)
(which is actually much easier to read).
The Identity
data type, like promises, is also a type of monad. In fact, it is the simplest of all monads. It's called Identity
because it doesn't do anything to its input type. It keeps it as it is.
Different monads have different effects which make them useful. For example, promises have the effect of managing asynchronicity. The Identity
monad however has no effect. It is a vanilla data type.
Anyway, continuing... we discovered one operation of monads, the bind
function. There is one more operation that is left to be discovered. In fact, the user spike alluded to it in his aforementioned comment:
I wrapped a synchronous call in a promise in order to be able to chain them with other promises.
You see, the problem is that the second argument of the then
function must be a function which returns a promise:
then :: Deferred a -> (a -> Deferred b) -> Deferred b
|_______________|
|
-- second argument is a function
-- that returns a promise
This implies that the second argument must be asynchronous (since it returns a promise). However, sometimes we may wish to chain a synchronous function with then
. To do so, we wrap the return value of the synchronous function in a promise. For example, this is what spike did:
// mapToInstruction :: Data -> Deferred Instruction
// The result of the previous promise is passed into the
// next as we're chaining. So the data will contain the
// result of getScenario
var mapToInstruction = function (data) {
// We map it onto a new instruction object
var instruction = {
method: data.endpoints[0].method,
type: data.endpoints[0].type,
endpoint: data.endpoints[0].endPoint,
frequency: data.base.frequency
};
console.log('Instructions recieved:');
console.log(instruction);
// And now we create a promise from this
// instruction so we can chain it
var deferred = $.Deferred();
deferred.resolve(instruction);
return deferred.promise();
};
As you can see, the return value of the mapToInstruction
function is instruction
. However, we need to wrap it in a promise object which is why we do this:
// And now we create a promise from this
// instruction so we can chain it
var deferred = $.Deferred();
deferred.resolve(instruction);
return deferred.promise();
In fact, he does the same thing in the handleResults
function as well:
// handleResults :: Data -> Deferred Unit
var handleResults = function(data) {
console.log("Handling data ...");
var deferred = $.Deferred();
deferred.resolve();
return deferred.promise();
};
It would be nice to put these three lines into a separate function so that we don't have to repeat ourselves:
// unit :: a -> Deferred a
function unit(value) {
var deferred = $.Deferred();
deferred.resolve(value);
return deferred.promise();
}
Using this unit
function we can rewrite mapToInstruction
and handleResults
as follows:
// mapToInstruction :: Data -> Deferred Instruction
// The result of the previous promise is passed into the
// next as we're chaining. So the data will contain the
// result of getScenario
var mapToInstruction = function (data) {
// We map it onto a new instruction object
var instruction = {
method: data.endpoints[0].method,
type: data.endpoints[0].type,
endpoint: data.endpoints[0].endPoint,
frequency: data.base.frequency
};
console.log('Instructions recieved:');
console.log(instruction);
return unit(instruction);
};
// handleResults :: Data -> Deferred Unit
var handleResults = function(data) {
console.log("Handling data ...");
return unit();
};
In fact, as it turns out the unit
function is the second missing operation of the monad interface. When generalized, it can be visualized as follows:
unit :: Monad m => a -> m a
All it does it wrap a value in a monad data type. This allows you to lift regular values and functions into a monadic context. For example, promises provide an asynchronous context and unit
allows you to lift synchronous functions into this asynchronous context. Similarly, other monads provide other effects.
Composing unit
with a function allows you to lift the function into a monadic context. For example, consider the isOdd
function we defined before:
// isOdd :: Number -> Identity Boolean
function isOdd(n) {
return new Identity(n % 2 === 1);
}
It would be nicer (albeit slower) to define it as follows instead:
// odd :: Number -> Boolean
function odd(n) {
return n % 2 === 1;
}
// unit :: a -> Identity a
function unit(value) {
return new Identity(value);
}
// isOdd :: Number -> Identity Boolean
function idOdd(n) {
return unit(odd(n));
}
It would look even nicer if we used a compose
function:
// compose :: (b -> c) -> (a -> b) -> a -> c
// |______| |______|
// | |
function compose( f, g) {
// compose(f, g) :: a -> c
// |
return function ( x) {
return f(g(x));
};
}
var isOdd = compose(unit, odd);
I mentioned earlier that a monad is an interface without an implementation (i.e. it defines certain operations and specifies what those operations should do, without specifying how it must be done). Hence, a monad is an interface that:
- Defines certain operations.
- Specifies what those operations should do.
We now know that the two operations of a monad are:
bind :: Monad m => m a -> (a -> m b) -> m b
unit :: Monad m => a -> m a
Now, we'll look at what these operations should do or how they should behave (i.e. we will look at the laws that govern a monad):
// Given:
// x :: a
// f :: Monad m => a -> m b
// h :: Monad m => m a
// g :: Monad m => b -> m c
// we have the following three laws:
// 1. Left identity
bind(unit(x), f) === f(x)
unit(x).then(f) === f(x)
// 2. Right identity
bind(h, unit) === h
h.then(unit) === h
// 3. Associativity
bind(bind(h, f), g) === bind(h, function (x) { return bind(f(x), g); })
h.then(f).then(g) === h.then(function (x) { return f(x).then(g); })
Given a data type we can define then
and unit
functions for it that violate these laws. In that case those particular implementations of then
and unit
are incorrect.
For example, arrays are a type of monad that represent non-deterministic computation. Let's define an incorrect unit
function for arrays (the bind
function for arrays is correct):
// unit :: a -> Array a
function unit(x) {
return [x, x];
}
// concat :: Array (Array a) -> Array a
function concat(h) {
return h.concat.apply([], h);
}
// bind :: Array a -> (a -> Array b) -> Array b
function bind(h, f) {
return concat(h.map(f));
}
This incorrect definition of unit
for arrays disobeys the second law (right identity):
// 2. Right identity
bind(h, unit) === h
// proof
var h = [1,2,3];
var lhs = bind(h, unit) = [1,1,2,2,3,3];
var rhs = h = [1,2,3];
lhs !== rhs;
The correct definition of unit
for arrays would be:
// unit :: a -> Array a
function unit(x) {
return [x];
}
An interesting property to note is that the array bind
function was implemented in terms of concat
and map
. However, arrays are not the only monad that possess this property. Every monad bind
function can be implemented in terms of generalized monadic versions of concat
and map
:
concat :: Array (Array a) -> Array a
join :: Monad m => m (m a) -> m a
map :: (a -> b) -> Array a -> Array b
fmap :: Functor f => (a -> b) -> f a -> f b
If you're confused about what a functor is then don't worry. A functor is just a data type that implements the fmap
function. By definition, every monad is also a functor.
I won't get into the details of the monad laws and how fmap
and join
together are equivalent to bind
. You can read about them on the Wikipedia page.
On a side note, according to the JavaScript Fantasy Land Specification the unit
function is called of
and the bind
function is called chain
. This would allow you to write code like:
Identity.of(1).chain(isOdd);
Anyway, back to your main question:
Would there be any benefit to writing synchronous code using the syntax of promises?
Yes, there are great benefits to be gained when writing synchronous code using the syntax of promises (i.e. monadic code). Many data types are monads and using the monad interface you can model different types of sequential computations like asynchronous computations, non-deterministic computations, computations with failure, computations with state, computations with logging, etc. One of my favourite examples of using monads is to use free monads to create language interpreters.
Monads are a feature of functional programming languages. Using monads promotes code reuse. In that sense it is definitely good. However, it comes at a penalty. Functional code is orders of magnitude slower than procedural code. If that's not an issue for you then you should definitely consider writing monadic code.
Some of the more popular monads are arrays (for non-deterministic computation), the Maybe
monad (for computations that can fail, similar to NaN
in floating point numbers) and monadic parser combinators.
try {
foo();
bar(a, b);
bam();
} catch(e) {
handleError(e);
}
...could be written something like (but using a synchronous version of then
);
foo()
.then(bar.bind(a, b))
.then(bam)
.fail(handleError)
Yes, you can definitely write code like that. Notice that I didn't mention anything about the fail
method. The reason is that you don't need a special fail
method at all.
For example, let's create a monad for computations that can fail:
function CanFail() {}
// Fail :: f -> CanFail f a
function Fail(error) {
this.error = error
}
Fail.prototype = new CanFail;
// Okay :: a -> CanFail f a
function Okay(value) {
this.value = value;
}
Okay.prototype = new CanFail;
// then :: CanFail f a -> (a -> CanFail f b) -> CanFail f b
CanFail.prototype.then = function (f) {
return this instanceof Okay ? f(this.value) : this;
};
Then we define foo
, bar
, bam
and handleError
:
// foo :: Unit -> CanFail Number Boolean
function foo() {
if (someError) return new Fail(1);
else return new Okay(true);
}
// bar :: String -> String -> Boolean -> CanFail Number String
function bar(a, b) {
return function (c) {
if (typeof c !== "boolean") return new Fail(2);
else return new Okay(c ? a : b);
};
}
// bam :: String -> CanFail Number String
function bam(s) {
if (typeof s !== "string") return new Fail(3);
else return new Okay(s + "!");
}
// handleError :: Number -> Unit
function handleError(n) {
switch (n) {
case 1: alert("unknown error"); break;
case 2: alert("expected boolean"); break;
case 3: alert("expected string"); break;
}
}
Finally, we can use it as follows:
// result :: CanFail Number String
var result = foo()
.then(bar("Hello", "World"))
.then(bam);
if (result instanceof Okay)
alert(result.value);
else handleError(result.error);
The CanFail
monad that I described is actually the Either
monad in functional programming languages. Hope that helps.