We are going to switch our mindset. From here on out, we'll stop telling the computer how to do its job and instead write a specification of what we'd like as a result. I'm sure you'll find it much less stressful than trying to micromanage everything all the time.
Declarative, as opposed to imperative, means that we will write expressions, as opposed to step by step instructions.
Think of SQL. There is no "first do this, then do that". There is one expression that specifies what'd like from the database. We don't decide how to do the work, it does. When the database is upgraded and the SQL engine optimized, we don't have to change our query. This is because there are many ways to interpret our specification and achieve the same result.
For some folks, myself included, it's hard to grasp the concept of declarative coding at first so let's point out a few examples to get a feel for it.
// imperative
var makes = [];
for (i = 0; i < cars.length; i++) {
makes.push(cars[i].make);
}
// declarative
var makes = cars.map(function(car){ return car.make; });
The imperative loop must first instantiate the array. The interpreter must evaluate this statement before moving on. Then it directly iterates through the list of cars, manually increasing a counter and showing its bits and pieces to us in a vulgar display of explicit iteration.
The map
version is one expression. It does not require any order of evaluation. There is much freedom here for how the map function iterates and how the returned array may be assembled. It specifies what, not how. Thus, it wears the shiny declarative sash.
In addition to being clearer and more concise, the map function may be optimized at will and our precious application code needn't change.
For those of you who are thinking "Yes, but it's much faster to do the imperative loop", I suggest you educate yourself on how the JIT optimizes your code. Here's a terrific video that may shed some light
Here is another example.
// imperative
var authenticate = function(form) {
var user = toUser(form);
return logIn(user);
};
// declarative
var authenticate = compose(logIn, toUser);
Though there's nothing necessarily wrong with the imperative version, there is still an encoded step-by-step evaluation baked in. The compose
expression simply states a fact: Authentication is the composition of toUser
and logIn
. Again, this leaves wiggle room for support code changes and results in our application code being a high level specification.
Because we are not encoding order of evaluation, declarative coding lends itself to parallel computing. This coupled with pure functions is why FP is a good option for the parallel future - we don't really need to do anything special to achieve parallel/concurrent systems.
We will now build an example application in a declarative, composable way. We'll still cheat and use side effects for now, but we'll keep them minimal and separate from our pure codebase. We are going to build a browser widget that sucks in flickr images and displays them. Let's start by scaffolding the app. Here's the html:
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.11/require.min.js"></script>
<script src="flickr.js"></script>
</head>
<body></body>
</html>
And here's the flickr.js skeleton:
requirejs.config({
paths: {
ramda: 'https://cdnjs.cloudflare.com/ajax/libs/ramda/0.13.0/ramda.min',
jquery: 'https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min'
}
});
require([
'ramda',
'jquery'
],
function (_, $) {
var trace = _.curry(function(tag, x) {
console.log(tag, x);
return x;
});
// app goes here
});
We're pulling in ramda instead of lodash or some other utility library. It includes compose
, curry
, and more. I've used requirejs, which may seem like overkill, but we'll be using it throughout the book and consistency is key. Also, I've started us off with our nice trace
function for easy debugging.
Now that that's out of the way, on to the spec. Our app will do 4 things.
- Construct a url for our particular search term
- Make the flickr api call
- Transform the resulting json into html images
- Place them on the screen
There are 2 impure actions mentioned above. Do you see them? Those bits about getting data from the flickr api and placing it on the screen. Let's define those first so we can quarantine them.
var Impure = {
getJSON: _.curry(function(callback, url) {
$.getJSON(url, callback);
}),
setHtml: _.curry(function(sel, html) {
$(sel).html(html);
})
};
Here we've simply wrapped jQuery's methods to be curried and we've swapped the arguments to a more favorable position. I've namespaced them with Impure
so we know these are dangerous functions. In a future example, we will make these two functions pure.
Next we must construct a url to pass to our Impure.getJSON
function.
var url = function (term) {
return 'https://api.flickr.com/services/feeds/photos_public.gne?tags=' +
term + '&format=json&jsoncallback=?';
};
There are fancy and overly complex ways of writing url
pointfree using monoids[^we'll learn about these later] or combinators. We've chosen to stick with a readable version and assemble this string in the normal pointful fashion.
Let's write an app function that makes the call and places the contents on the screen.
var app = _.compose(Impure.getJSON(trace("response")), url);
app("cats");
This calls our url
function, then passes the string to our getJSON
function, which has been partially applied with trace
. Loading the app will show the response from the api call in the console.
We'd like to construct images out of this json. It looks like the srcs are buried in items
then each media
's m
property.
Anyhow, to get at these nested properties we can use a nice universal getter function from ramda called _.prop()
. Here's a homegrown version so you can see what's happening:
var prop = _.curry(function(property, object){
return object[property];
});
It's quite dull actually. We just use []
syntax to access a property on whatever object. Let's use this to get at our srcs.
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var srcs = _.compose(_.map(mediaUrl), _.prop('items'));
Once we gather the items
, we must map
over them to extract each media url. This results in a nice array of srcs. Let's hook this up to our app and print them on the screen.
var renderImages = _.compose(Impure.setHtml("body"), srcs);
var app = _.compose(Impure.getJSON(renderImages), url);
All we've done is make a new composition that will call our srcs
and set the body html with them. We've replaced the trace
call with renderImages
now that we have something to render besides raw json. This will crudely display our srcs directly in the body.
Our final step is to turn these srcs into bonafide images. In a bigger application, we'd use a template/dom library like Handlebars or React. For this application though, we only need an img tag so let's stick with jQuery.
var img = function (url) {
return $('<img />', { src: url });
};
jQuery's html()
method will accept an array of tags. We only have to transform our srcs into images and send them along to setHtml
.
var images = _.compose(_.map(img), srcs);
var renderImages = _.compose(Impure.setHtml("body"), images);
var app = _.compose(Impure.getJSON(renderImages), url);
And we're done!
Here is the finished script:
requirejs.config({
paths: {
ramda: 'https://cdnjs.cloudflare.com/ajax/libs/ramda/0.13.0/ramda.min',
jquery: 'https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min'
}
});
require([
'ramda',
'jquery'
],
function (_, $) {
////////////////////////////////////////////
// Utils
var Impure = {
getJSON: _.curry(function(callback, url) {
$.getJSON(url, callback);
}),
setHtml: _.curry(function(sel, html) {
$(sel).html(html);
})
};
var img = function (url) {
return $('<img />', { src: url });
};
var trace = _.curry(function(tag, x) {
console.log(tag, x);
return x;
});
////////////////////////////////////////////
var url = function (t) {
return 'http://api.flickr.com/services/feeds/photos_public.gne?tags=' +
t + '&format=json&jsoncallback=?';
};
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var srcs = _.compose(_.map(mediaUrl), _.prop('items'));
var images = _.compose(_.map(img), srcs);
var renderImages = _.compose(Impure.setHtml("body"), images);
var app = _.compose(Impure.getJSON(renderImages), url);
app("cats");
});
Now look at that. A beautifully declarative specification of what things are, not how they come to be. We now view each line as an equation with properties that hold. We can use these properties to reason about our application and refactor.
There is an optimization available - we map over each item to turn it into a media url, then we map again over those srcs to turn them into img tags. There is a law regarding map and composition:
// map's composition law
var law = compose(map(f), map(g)) == map(compose(f, g));
We can use this property to optimize our code. Let's have a principled refactor.
// original code
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var srcs = _.compose(_.map(mediaUrl), _.prop('items'));
var images = _.compose(_.map(img), srcs);
Let's line up our maps. We can inline the call to srcs
in images
thanks to equational reasoning and purity.
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var images = _.compose(_.map(img), _.map(mediaUrl), _.prop('items'));
Now that we've lined up our map
's we can apply the composition law.
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var images = _.compose(_.map(_.compose(img, mediaUrl)), _.prop('items'));
Now the bugger will only loop once while turning each item into an img. Let's just make it a little more readable by extracting the function out.
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var mediaToImg = _.compose(img, mediaUrl);
var images = _.compose(_.map(mediaToImg), _.prop('items'));
We have seen how to put our new skills into use with a small, but real world app. We've used our mathematical framework to reason about and refactor our code. But what about error handling and code branching? How can we make the whole application pure instead of merely namespacing destructive functions? How can we make our app safer and more expressive? These are the questions we will tackle in part 2.