Functional Turtle
Photo by Adolfo FélixEven knowning the basic concepts of functional programming (like pure functions, immutability), reading code in functional programming style can be daunting, specially in more FP oriented languages like clojure, Haskell, Elm or purescript (to name a few).
One of the reason (that I want to show) is how arguments order and currified functions affects how we write programs.
LOGO Turtle #
To show those differences, we're going to write the same (trivial) example of a LOGO Turtle implementation in both OOP and FP styles.
The example has three parts (basic, extended and usage) and it's presented in four flavours:
- object oriented programming (with inheritance, with composition)
- functional programming ("normal", "reversed currified")
1. OOP #
I guess we'll all agree that the OOP version is the most simple to understand for most of us. Let's dive in.
OOP Basic turtle #
The basic version of the Turtle can move horizontally, vertically and, when the pen is down, draw lines.
An OOP implementation with no surprises:
class Turtle {
constructor(display) {
this.display = display;
this.drawing = false;
this.x = 0;
this.y = 0;
}
penDown() {
this.drawing = true;
}
penUp() {
this.drawing = false;
}
moveX(ammount) {
const nextX = this.x + ammount;
if (this.drawing) {
this.display.drawLine(this.x, this.y, nextX, this.y);
}
this.x = nextX;
}
moveY(ammount) {
const nextY += this.y + ammount;
if (this.drawing) {
this.display.drawLine(this.x, this.y, this.x, nextY);
}
this.y = nextY;
}
}
Let's say that it does it's job quite well.
OOP Extended turtle #
Let suppose that after a while, we want to extend the API of the Turtle to more complex drawings.
One option, if possible, is to add a new method to the class itself. Work done. 👍
But sometimes is not possible, because it's part of a library. Or maybe the Turtle class itself has become so big, that adding more methods it's impractical.
Then you have two options:
Option A: Inheritance #
class LegoWithSquare extends Lego {
constructor(display) {
super(displah);
}
square(size) {
this.moveX(size);
this.moveY(size);
this.moveX(-size);
this.moveY(-size);
}
}
Option B: Composition (delegate pattern) #
class TurtleWithSquare {
constructor(turtle) {
this.delegate = turtle;
}
square(size) {
this.delegate.moveX(size);
this.delegate.moveY(size);
this.delegate.moveX(-size);
this.delegate.moveY(-size);
}
}
Although composition is generally preferred over inheritance, in this case, we want TurtleWithSquare
to expose the methods of the original Turtle
.
That will add more boilerplate to our code (unless we employ black powers like object introspection, proxies or ... ruby 🤷♀️)
OOP Usage #
Draw a square in the center of the screen with our magnificient OOP Turtle:
const turtle = new TurtleWithSquare(display);
turtle.moveX(10);
turtle.moveY(10);
turtle.penDown();
turtle.square(5);
turtle.penUp();
2. FP v1 - state as parameter #
The most simple way to move from OOP thinking into FP thinking is by extracting the state into a parameter of a function (split the data+code encapsulation of OOP).
To do so, the constructor is just a simple function that returns the initial state. And methods are replaced by functions that receives the state as a first parameter:
FP v1 - Basic turtle #
function Turtle(display) {
return { display, x: 0, y: 0, drawing: false };
}
function penDown(turtle) {
turtle.drawing = true;
}
function penUp(turtle) {
turtle.drawing = false;
}
function moveX(turtle, ammount) {
const nextX = turtle.x + ammount;
if (turtle.drawing) {
turtle.display.drawLine(turtle.x, turtle.y, nextX, turtle.y);
}
turtle.x = nextX;
}
function moveY(turtle, ammount) {
const nextY += turtle.y + ammount;
if (turtle.drawing) {
turtle.display.drawLine(turtle.x, turtle.y, turtle.x, nextY);
}
turtle.y = nextY;
}
The code is almost the same as OOP, but we use turtle
parameter instead this
. I think you get the idea.
But with this change, we're following one of the FP rules: write pure functions when possible.
OOP methods are non pure: for example turtle.moveUp(5)
could produce different results based on the instace internal state.
In this new version moveUp(turtle, 5)
will always do the same if the parameters are the same (pure function: same parameters -> same output).
FP v1 - Extended turtle #
Extending an API with FP is just simpler than OOP: add more functions. Anyware, modules will take care of avoiding collisions. No need to modify anything or create new abstractions:
function square(turtle, size) {
moveX(turtle, size);
moveY(turtle, size);
moveX(turtle, -size);
moveY(turtle, -size);
}
FP v1 - Usage #
Again, the usage is almost the same as the OOP version, except we don't use new
keyword in the constructor:
const turtle = Turtle(); // Look ma! no `new` 🚲
moveX(turtle, 10);
square(turtle, 5);
3. FP v2 - Curry data-last #
Let's make now a twist. Although in the previous example we wrote code following FP conventions, it's not how is usually written.
We need to make two changes:
First change: data-last #
this
(aka state) should be the last parameter
The first thing to notice when reading FP code is that sometimes, the order of the parameters are reversed, more specifially, the this parameter is the last one (see the lodash vs ramnda battle).
So instead of:
function moveX(turtle, ammount) {
...
}
we should write:
function moveX(ammount, turtle) {
...
}
That, at eyes of a OOP coders, could look quite akward:
const turtle = Turtle(); // Look ma! no `new` keyword
moveX(10, turtle);
moveY(10, turtle);
square(5, turtle);
Second change: currify functions #
Currify functions means that if you invoke it with less parameters, it will return a function that accepts the rest of the parameters.
So, if you have a currified function dance
that accepts three parameters, you can invoke them directly:
// invoke with all
dance("Nico", "Circe", "Rai");
But also one by one:
// one
const danceWithNico = dance("Nico");
const danceWithNicoAndCirce = danceWithNico("Circe");
danceWithNicoAndCirce("Rai");
Or arbitrarily grouped:
const danceWithNico = dance("Nico");
danceWithNico("Circe", "Rai");
Currify functions in Javascript #
In most of FP programming languages, functions are currified by default. No need to make anything.
But it's not the case in Javascript. We can use an utility function (like rambda's curry) or write a poors-man-version of writing "a function that returns a function".
So instead of the reversed version:
function moveX(ammount, turtle) {
...
}
We'll write a reversed currified version:
function moveX(ammount) {
return function(turtle) {
...
}
}
(Notice that is not strictly currified. I must invoke one parameter at a time: moveX(10)(turtle)
and not: moveX(10, turtle)
)
We can make the code less verbose using ES6 arrow functions:
const moveX = ammount => turtle => {
const nextX = turtle.x + ammount;
};
FP v2 - Basic turtle #
Ok, with this all new knowledge, let's rewrite the basic version:
const Turtle = display => {
return { display, x: 0, y: 0, drawing: false };
};
const penDown = () => turtle => {
turtle.drawing = true;
};
const moveX = ammount => turtle => {
const nextX = turtle.x + ammount;
if (turtle.drawing) {
turtle.display.drawLine(turtle.x, turtle.y, x, turtle.y);
}
turtle.x = nextX;
};
Code it's getting more strange (at OOP eyes), but almost the same as before.
FP v2 - Extended turtle #
Ok. Here's when the fun part begins. We want to extend the function, but we're going to use another FP ubiquous concept: composition.
Combine function #
Most FP languages comes with (one or more variations of) the combine
functions. Not Javascript, so we need to write our own (or use a library. Recommended).
Our combine
function is very simple: it receives an array of functions and returns another function. Calling that function with a parameter will make all functions to be called with that same parameter.
The code:
// `...fns` it's a ES6 feature (arguments in an array)
function combine(...fns) {
return function(param) {
fns.forEach(fn => fn(param));
};
}
// or if your prefer:
const combine = (...fns) => param => fns.forEach(fn => fn(param));
Now, we can write the square
function using combine
. And this is how lot of FP code looks like:
const square = (size) => combine(
penDown()
moveX(size)
moveY(size)
moveX(-size)
moveY(-size)
penUp()
)
Notice that is common in FP creating new functions without the function
keyword.
The combine functions pattern is so common in FP that, sometimes, there are several built-in language operators , that makes ode even more weird (for a forengeir eyes).
FP v2 - Usage #
Although you can use the functions this way:
const turtle = Turtle();
moveX(10)(turtle);
moveY(10)(turtle);
square(5)(turtle);
Again, normally is more like this:
combine(
moveX(5)
moveY(5)
square(10)
)(Turtle())
The curry+data-last as a kind of dependency injection pattern for FP, where you can think in terms of what to do (move, etc) without thinking in how to do it or what is required to be done (the turtle)
More 🐢 #
Those ideas are normally combined with immutability or state / effects separation