Let's generalize and say that there are two ways in which we can write code: imperative and declarative.
We could define the difference as follows:
- Imperative programming: telling the "machine" how to do something, and as a result what you want to happen will happen.
- Declarative programming: telling the "machine"1 what you would like to happen, and let the computer figure out how to do it.
Examples of imperative and declarative code
Taking a simple example, let's say we wish to double all the numbers in an array.
We could do this in an imperative style like so:
var numbers = [1,2,3,4,5]
var doubled = []
for(var i = 0; i < numbers.length; i++) {
var newNumber = numbers[i] * 2
doubled.push(newNumber)
}
console.log(doubled) //=> [2,4,6,8,10]
doubled
array at each step until we are done.A more declarative approach might use the
Array.map
function and look like:var numbers = [1,2,3,4,5] var doubled = numbers.map(function(n) { return n * 2 }) console.log(doubled) //=> [2,4,6,8,10]
map
creates a new array from an existing array, where
each element in the new array is created by passing the elements of the
original array into the function passed to map
(function(n) { return n*2 }
in this case).What the
map
function does is abstract away the process of explicitly iterating over the array, and lets us focus on what
we want to happen. Note that the function we pass to map is pure; it
doesn't have any side effects (change any external state), it just takes
in a number and returns the number doubled.There are other common declarative abstractions for lists that are available in languages with a functional bent. For example, to add up all the items in a list imperatively we could do this:
Or we could do it declaratively, using thevar numbers = [1,2,3,4,5] var total = 0 for(var i = 0; i < numbers.length; i++) { total += numbers[i] } console.log(total) //=> 15
reduce
function:var numbers = [1,2,3,4,5] var total = numbers.reduce(function(sum, n) { return sum + n }); console.log(total) //=> 15
reduce
boils a list down into a single value using the
given function. It takes the function and applies it to all the items in
the array. On each invocation, the first argument (sum
in this case) is the result of calling the function on the previous element, and the second (n
) is the current element. So in this case, for each element we add n
to sum
and return that on each step, leaving us with the sum of the entire array at the end.Again,
reduce
abstracts over the how and deals
with the iteration and state management side of things for us, giving us
a generic way of collapsing a list to a single value. All we have to do
is specify what we are looking for.Strange?
If you have not seen
map
or reduce
before, this will feel and look strange at first, I guarantee it. As programmers we are very used to specifying how
things should happen. "Iterate over this list", "if this then that",
"update this variable with this new value". Why should you have to learn
this slightly bizarre looking abstraction when you already know how to
tell the machine how to do things?In many situations imperative code is fine. When we write business logic we usually have to write mostly imperative code, as there will not exist a more generic abstraction over our business domain.
But if we take the time to learn (or build!) declarative abstractions we can take dramatic and powerful shortcuts when we write code. Firstly, we can usually write less of it, which is a quick win. But we also get to think and operate at a higher level, up in the clouds of what we want to happen, and not down in the dirty of
how
it should happen.SQL
You may not realise it, but one place where you have already used declarative abstractions effectively is in SQL.
You can think of SQL as a declarative query language for working with sets of data. Would you write an entire application in SQL? Probably not. But for working with sets of related data it is incredibly powerful.
Take a query like:
SELECT * from dogs
INNER JOIN owners
WHERE dogs.owner_id = owners.id
Yuck! Now, I'm not saying that SQL is always easy to understand, or necessarily obvious when you first see it, but it's a lot clearer than that mess.//dogs = [{name: 'Fido', owner_id: 1}, {...}, ... ] //owners = [{id: 1, name: 'Bob'}, {...}, ...] var dogsWithOwners = [] var dog, owner for(var di=0; di < dogs.length; di++) { dog = dogs[di] for(var oi=0; oi < owners.length; oi++) { owner = owners[oi] if (owner && dog.owner_id == owner.id) { dogsWithOwners.push({ dog: dog, owner: owner }) } }} }
But it's not just shorter and easier to read, SQL gives us plenty of other benefits. Because we have abstracted over the how we can focus on the what and let the database optimise the how for us.
If we were to use it, our imperative example would be slow because we would have to iterate over the full list of owners for every dog in the list.
But in the SQL example we can let the database deal with how to get the correct results. If it makes sense to use an index (providing we've set one up) the database can do so, resulting in a large performance gain. If it's just done the same query a second ago it might serve it from a cache almost instantly. By letting go of how we can get a whole host of benefits by letting computers do the hardwork, with little cognitive overhead.
d3.js
Another place where declarative approaches are really powerful is in user interfaces, graphics and animations.
Coding user interfaces is hard work. Because we have user interaction and we want to make nice dynamic user interactions, we typically end up with a lot of state management, and generic how code that could be abstracted away, but frequently isn't.
A great example of a declarative abstraction is d3.js. D3 is a library that helps you build interactive and animated visualisations of data using JavaScript and (typically) SVG.
The first time (and fifth time, and possibly even the tenth time) you see or try and write d3 code your head will hurt. Like SQL, d3 is an incredibly powerful abstraction over visualising data that deals with almost all of the how for you, and lets you just say what you want to happen.
Here's an example (I recommend viewing the demo for some context). This is a d3 visualization that draws a circles for each object in the
data
array. To demonstrate what's going on we add a circle every second.The interesting bit of code is:
It's not essential to understand exactly what's going on here (it will take a while to get your head around regardless), but the gist of it is this://var data = [{x: 5, y: 10}, {x: 20, y: 5}] var circles = svg.selectAll('circle') .data(data) circles.enter().append('circle') .attr('cx', function(d) { return d.x }) .attr('cy', function(d) { return d.y }) .attr('r', 0) .transition().duration(500) .attr('r', 5)
First we make a selection object of all the svg
circle
s in the visualisation (initially there will be none). Then we bind some data to the selection (our data array).D3 keeps track of which data point is bound to which circle in the diagram. So initially we have two datapoints, but no circles; we can then use the
.enter()
method to get the datapoints which
have "entered". For those points, we say we would like a circle added to
the diagram, centered on the x
and y
values of the datapoint, with an initial radius of 0
but transitioned over half a second to a radius of 5
.So why is this interesting?
Look through the code again and think about whether we are describing what we want our visualisation to look like, or how to draw it? You'll see that there is almost no how code at all. We are just describing at quite a high level what we want:
I want this data drawn as circles, centered on the point specified in the data, and if there are any new circles you should add them and animate their radius.This is awesome, we haven't written a single loop, there is no state management here. Coding graphics is often hard, confusing and ugly, but here d3 has abstracted away most of the crap and left us to just specify what we want.
Now, is d3.js easy to understand? Nope, it definitely takes a while to learn. And most of that learning is in giving up your desire to specify how things should happen and instead learning how to specify what you want.
Initially this is hard work, but after a few hours something magical happens - you become really, really productive. By abstracting away the how d3.js really lets you focus on what you want to see, which frankly is the only thing you should care about when designing something like a visualisation. It frees you from the fiddly details of the how and lets you interact with the problem at a much higher level, opening up the possibilities for creativity.
Finally
Declarative programming allows us to describe what we want, and let the underlying software/computer/etc deal with how it should happen.
In many areas, as we have seen, this can lead to some real improvements in how we write code, not just in terms of fewer lines of code, or (potentially) performance, but by writing code at a higher level of abstraction we can focus much more on what we want, which ultimately is all we should really care about as problem solvers.
The problem is, as programmers we are very used to describing the how. It makes us feel good and comfortable - powerful, even - to be able to control what is happening, and not leave it to some magic process we can't see or understand.
Sometimes it's okay to hold on to the how. If we need to fine tune code for high performance we might need to specify the what in more detail. Or for business logic, where there isn't anything that a generic declarative library could abstract over, we're left writing imperative code.
But frequently we can, and I'd argue should, look for declarative approaches to writing code, and if we can't find them, we should be building them. Will it be hard at first? Yes, almost certainly! But as we've seen with SQL and D3.js the long term benefits can be huge!
No comments:
Post a Comment