Skip to content

Instantly share code, notes, and snippets.

@alistair
Last active October 15, 2016 04:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alistair/3add004522a5fd02c5d24954ebda15ce to your computer and use it in GitHub Desktop.
Save alistair/3add004522a5fd02c5d24954ebda15ce to your computer and use it in GitHub Desktop.
Composable computation descriptions.
/*
* This is a demonstration of a common programming patterns and how the
* flow control abilities of async/await ( or yield in javascript )
* support turning nested code into, beautiful functional code.
*
* What I am demonstrating here is not simple and using javascript to
* explain this has some pros but also cons.
* So please if you fail to understand don't be discouraged.
* That is a failing on my part not yours.
*
* DONT USE ANY OF THIS CODE IN PRODUCTION (OR ANY OTHER ENVIRONMENT)
*
* A Very important point is that this implementation does not
* conform 100% to libraries such as co (which I recommend).
* Use it to help understand the principals behind the practical or
* write haskell and have both at the same time.
*
* doM and Maybe parts of this code were taking from
* https://curiosity-driven.org/monads-in-javascript a good article
* which I recommend. Comments+more are my own.
*/
/*
* Lets begin the journey.
* Here is an example of the promises with the well known callback hell.
* It gets the id of a person somehow and uses that to get all the
* current orders for that person. Both those operations return
* Promises so we chain then together with then.
* It then makes an important decision and returns the result.
*/
getIdOfPerson()
.then((id) =>
getCurrentOrderForPerson(id)
.then(order => {
if (order.amount > 200) {
return Promise.resolve(`Special Customer ${id}`);
}
return Promise.resolve("Normal Customer");
}));
/*
Here is some other nested code.
It might seem strange to you, but if statements have the same
form of nesting as Promises. We will see this as we progress.
This will introduce the Maybe type, a way functional programming removes
the need for the dreaded null pointer.
*/
const maybeResult2 = (function() {
const person = getPersonNoneMaybe();
if (person !== null) {
const address = person.getAddress();
if (address !== null) {
return "This person has an address";
}
}
return undefined; // or null
})(); // returns "This person has an address";
// Just going to use this data structure in examples
const request = {
method: 'GET',
route: {
orgId: 5,
},
headers: {
'Content-Type': 'application/monads'
}
};
/*
Here is some other nested code.
It might seem strange to you, but functions can have the same
form of nesting as Promises. We will see this as we progress.
What is important to understand here is that the functions within
the outer function are actally involved in function composition.
By capturing that req variable they could very well be
written as (req) -> (...) -> .... and moved out of this function. This
would show the function composition more clearly if I had done it.
Why not try it yourself. Do you see it now?
This will introduce the Reader type. The Reader monad (also called
the Environment monad). Represents a computation, which can read
values from a shared environment, pass values from function to
function, and execute sub-computations in a modified environment.
*/
const funcResult2 = (function(req) {
function getMethod() {
return req.method;
}
function getParam(name) {
return req.route[name];
}
function getHeader(name) {
return req.headers[name];
}
const method = getMethod();
const orgId = getParam("orgId");
const contentType = getHeader("Content-Type");
return `${method} request with orgId of '${orgId}' and content type '${contentType}'`;
})(request);
/*
* Now onto the real interesting code.
*
* We will start with a normal example using Promises.
* Im going to assume most people reading this code are familar with this code but if
* you are not then
*
* getIdOfPerson and getCurrentOrderForPerson return promises
* id and order are the values contained within the promises returned to there right.
* This 'async' code can be read sequentially as if it was non-async.
* Each line executing after the previous.
* Of Course the important point being that the code will execute
* only AFTER the promise resolves, so you can't tell when that will be.
*
* It essentially generates code similar to the Promise example above.
*/
doM(function* () {
const id = yield getIdOfPerson();
const order = yield getCurrentOrderForPerson(id);
const result = (order.amount > 200) ? `Special Customer ${id}` : "Normal Customer";
return Promise.resolve(result);
})();
/*
* So what does this doM function do?
*
* As we can see doM returns a function that when called gets the
* generator from the given function and it is able to call next()
* on this. Each next corresponds to a yield in the above code.
*
* The result (as seen in handle) has 2 properties. done and value.
* done represends whether there are any more yields left ( or to put it
* another way whether we hit a yield) while value represents the right
* hand side the the yield expression.
*
* What handle then does is assumes the object returned as a then function
* and calls that passing in the result of the generators next function.
* You should notice how this is wrapped in a func tho. So it is lazely
* evaluated once the Promise(s) above complete.
*
* It is fair to say that what doM is doing is building an expression, which
* may be evaluated in the future.
*/
function doM(makeGenerator){
return function (){
var generator = makeGenerator.apply(this, arguments);
function handle(result){
if (result.done) return result.value;
return result.value.then(function (res){
return handle(generator.next(res)); // <- sets output of yield to the promise result
}, function (err){ // and returns input promise
return handle(generator.throw(err)); // <- throw promise exception to generator function
});
}
return handle(generator.next()); // <- first time call, input of yield is a promise
};
}
/*
* So to further help explain. Lets introduce the simpliest type ever, Identity.
* What does it do? Nothing, it just holds a value.
*
* Have a read it should be understandable.
*/
function Identity(value) {
this.value = value;
}
// Identity a -> (a -> b) -> Identity b
Identity.prototype.map = function(f) {
return new Identity(f(this.value));
}
// Identity a -> (a -> Identity b) -> Identity b
Identity.prototype.then = function(f) {
return f(this.value);
}
/*
* Now lets jump straight to the do function. We are going to work
* backwards through what doM is doing.
*/
doM(function* () {
const i = yield new Identity(10);
const j = yield new Identity(i + 1);
return new Identity(i*j);
})();
/*
* So lets take the last yields value and wrap everything
* following that in the lambda. See the j above and below,
* they are equivilent.
*/
doM(function* () {
const i = yield new Identity(10);
return new Identity(i + 1)
.then(j => new Identity(i*j))
})();
// We then repeat
doM(function* () {
return new Identity(10)
.then(i => new Identity(i + 1)
.then(j => new Identity(i*j)));
})();
// and then doM returns the result so lets get rid of that.
new Identity(10)
.then(i => new Identity(i + 1)
.then(j => new Identity(i*j)));
/*
*
*/
/*
* So now we want to get rid of those pescky null checks everywhere
* For this we will create a Type with 2 possible values.
* An object containing a value OR a value representing 'empty'.
* You can think of this like
*
* data Maybe a = Just a | Nothing
* ^-- Read as or.
*
* where a represents the type contained within the Maybe type.
* and Just and Nothing are contructors.
*
* therefore we can create Maybe Int, Maybe String, etc, etc.
*
* Lets start with the Just constructor
*/
function Just(value) {
this.value = value;
}
/*
* That wasn't too hard was it
*
* Now we want a function which looks like the Promises then function.
* then has a signature which looks something like. Im simplifying
* Promises then here. Promise.then is the following + some more. aka
* a function with 2 many responsibilities.
*
* Promise a -> (a -> Promise b) -> Promise b
* This is a little bit like map except that the function
* (a -> Promise b) is generating more structure which then has to
* ensure it is 'removed' (whatever that means for each type).
*
*
* Each type must return the same type as the datastructure its
* being applied to so the function can be generally described as.
*
* M a -> (a -> M b) -> M b
*
* where M might be Promise, Maybe, Function ( in these examples ) or many more.
*/
Just.prototype.then = function(transform) {
return transform(this.value);
};
/*
* We can see that Maybe just gets the value from the Just and applies
* the transform. This is one of the most simple then functions to define.
* Can you think why this wouldn't work for a Promise, a type with a more difficult
* implementation?
*/
Just.prototype.toString = function() {
return 'Just(' + this.value + ')';
};
/*
* Ah the Nothing value. As we can see. It does nothing
* and returns itself each time then is called. Therefore
* those functions will never execute. You can think of
* Nothing like an empty list/array. [].map(x => x+1) will
* always return [] and never call map.
*/
var Nothing = {
then: function() {
return this;
},
toString: function() {
return 'Nothing';
}
};
/*
* With this type we can now write very Promise looking
* code. See the callback hell. Don't you love it?
*
* On another note. We have now encapsulated the dangerous
* part of this code into a single location. Making this
* 'safe' from the million dollar mistake. So only 999,999
* more unsafe javascript parts to deal with.
*/
getPerson().then(person => {
person.getAddress().then(address => {
return new Just("This person has an address");
});
});
/*
* So lets make this code a little more easy to read then.
* Look at these 2 functions. they are equal.
*/
doM(function* () {
const person = yield getPerson();
const address = yield person.getAddress();
return new Just("This person has an address");
})();
// Functions
// Reader :: r -> a
Function.prototype.then = function(f) {
return (r) => f(this.call(null, r))(r);
};
/*
* If you just wanted normal function composition then you
* could define map.
*
* Function.prototype.map = function(f) {
* return (a) => f(this.call(null, a));
* }
*
* This would allow you to then do
* ((a) => a + 1).map((a) => a * 10)(5) === 60
*/
/* Reader functions */
function getMethod(req) {
return req.method;
}
function getParam(name) {
return (req) => req.route[name];
}
function getHeader(name) {
return (req) => req.headers[name];
}
function id(name) {
return name;
}
const funcResult = doM(function* () {
const method = yield getMethod;
const orgId = yield getParam('orgId');
const contentType = yield getHeader('Content-Type');
const r = yield id;
return () => `${method} request with orgId of '${orgId}' and content type '${contentType}'`;
})()(request);
/*
* Promise functions. Everything below this point are used in the examples above.
* Read them if you are interested.
*/
function getIdOfPerson() {
return Promise.resolve(5);
}
function getCurrentOrderForPerson(x) {
return Promise.resolve({ orderId: 22, amount: 500});
}
/* Maybe Functions */
// Made getAddress a function so that we are 'doing' something expensive
// like maybe using google maps api.
// Makes the nested if statement approach more 'real'.
function getPerson() {
return new Just({
id: 1,
name: 'Alistair',
getAddress: () => new Just('Hollywood')
});
}
/* Maybe Functions */
function getPersonNoneMaybe() {
return {
id: 1,
name: 'Alistair',
getAddress: () => 'Hollywood'
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment