This just isn't functional
Functional is in vogue, especially on the frontend.
It turns out that if one is simply reacting to incoming information from some source of truth, using pure functions to transform that information into a UI is a very natural thing to do. With the increasing popularity of React, lots of programmers are finding themselves having to deal with functional constructs.
When writing functional JavaScript, a person will inevitably think: “Some sort of object-orientedness would be very useful for my program.” Maybe there is some code reuse happening. Maybe, just maybe, you have an inheritance situation on your hands; an honest-to-god taxonomy. If you have gone down this route, you may wind up thinking, “Oh, I now have to use the this
keyword.”
Cold sweats. Do you really know what this
is? What is it bound to? There are four different ways that this
could have been bound. Do you, at this moment, know which applies to your program? The most terrible situation would be a this
bound to the global scope. Who knows what has been mutated now?
You might think, “No, my function is well reasoned, and well defined.” Traditionally, in functional programming, bits of state that only exist locally in a function, and are not passed around to other functions, are okay. This is because local state avoids some of the problems associated with stateful code. You might think your constructor is doing just that, but if you are wrong about what kind of this
you are dealing with, your local assignment, which was okay by functional programming standards, can suddenly mutate into something outside the local function scope.
Still not scared of this
1? Well, take a gander at THIS piece of code:
function Dog(){
this.bark = function(){
return "WOOF!";
}
}
// Then a very bad person comes along
var cat = { purring: true };
Dog.bind(cat)();
cat.bark(); // WHAT HAVE WE WROUGHT
Chilling.
Those of you who have not yet been burned by this
may not understand what is
going on. Other than the global object, the previous function scope, or an object
that can be passed in by new
, the this
object in a function can also be explicitly bound this way.
Someone has used the constructor function in a way that was not intended.
Now, the little kitty can bark. It is tragic. The problem with using this
in your constructor is that you need to trust that your users will be calling
your function with new
. This constructor is definitely not a pure function. It is possibly side-effecting. If your constructor is is not called with new
, it can be mutating.
What alternatives exist?
// Let's try...
function Person(name){
return {
name: name,
greet: function(other){
return "Hello " + other.name + ", my name is " + name + ".";
}
};
}
var karen = Person("Karen");
var tom = Person("Tom");
tom.greet(karen); // Hello Karen, my name is Tom.
// That's nice, but we can still do this...
tom.name = "Jeff";
tom.greet(karen); // Hello Karen, my name is Tom.
tom.name == "Jeff"; // true
// And now everything is inconsistent!
Our constructor does not use this
, so it is safe from one form of side effect,
but it still returns a piece of mutable state. To make matters worse, when it
creates the greet
function it uses a variable it closed over, which does not
mutate. Our object is now meaningless.
You might now be thinking, “OK, let’s just use Object.fre
–”
Let me stop you there. Object.freeze
does not actually address all of
our problems. In functional programming, the way one achieves change is
not through in place mutation, but in the creation of a new object. This
means that functional programming never deletes any old facts, it only
creates new facts. When we change the name of our functional object by
copying it and refreezing, we will not change the functions inside the
object. All those functions will still only have access to the old
version of the object through reference. In an immutable world, the
coupling of functionality and data is extremely difficult to use because
any functionality would have to be rebuilt any time the facts change.
So what do we do?
We use Protocols
2. A protocol is a namespace that provides you with a defined set of functions that can be called on anything that implements that protocol. A protocol checks an object’s type metadata at runtime, and selects the right behavior based on that type. Furthermore, a protocol is not directly attached to your object. To call a protocol,
one must pass the object into the protocol. This means we do not have to update functionality whenever we create a new version of an object.
Now we can build ourselves a new tower of object-orientedness. A tower where nothing ever changes and where we are stuck, forever, immutably. It is the functional dystopia we’ve always wanted.
As a side note; this article uses a quick and dirty implementation of protocols. To follow along, copy the source code and paste it into a Node project. It is already packaged as a Node module. This article will not concern itself with implementation details for purposes of brevity and clarity. If you are interested in implementation details, check the source; it is well commented.
To define a protocol, we call Protocol(protocolName, [fns...])
, where protocolName
is the name of the protocol, and fns are the names of all the functions that an implementation of a protocol must define.
Consider two protocols: HypeRater
and TemperatureRater
. Something can be cool in a cool/unpopular way. Something can also be cool in a cool/warm way. We, human beings, can be cool and warm, or unpopular and cool, or cool and cool, or unpopular and warm. Much like someone can be, like, cool
cool, someone can be, like, HypeRater.isCool
, you know? Someone can be TemperatureRater.isCool
, too, and those values can be different.
When we specify what kind of cool we’re talking about, we get a nifty thing called namespacing. Check out the code below.
var HypeRater = Protocol("HypeRater", ["isCool", "isUnpopular"]);
var TemperatureRater = Protocol("TemperatureRater", ["isCool", "isWarm"]);
// Imagine we had a theSun object, which implemented both protocols
HypeRater.isCool(theSun, []); //=> true
TemperatureRater.isCool(theSun, []); //=> false
Protocols solve the problem of method clashing in object-oriented programming. An object with multiple base classes might wind up having to implement two equally named methods from those two base classes. Protocols also solve the duck typing problem of “Sure, this object has a .run
function, but is it talking about the same kind of running that I need?”
Protocols require an Implementation
. An implementation provides a function that implements each method specified in the protocol.
function isCool(thiz, args){
return get(thiz, "temperature") < 15;
}
var tempImpl = Implementation({isCool: isCool, isHot: not(isCool)});
tempImpl
is a valid implementation for any protocol with two
methods, isCool
and isHot
. tempImpl
implements these two
functions by using the function isCool
. This function takes two
arguments, a record thiz
and an array of arguments args
.
In the call TemperatureRater.isCool(theSun, [1,2,3])
, theSun
would be bound to thiz
and [1,2,3]
would be bound to args
.
The final step in being able to use our protocols is to create
a sort-of-like-a-class thing that is able to implement protocols.
We will be using something called a Record
3. A record is
an immutable object that declares a set of required fields and
protocol implementations.
To obtain a record constructor, one should call Record(typeName, fieldDeclarations, implementations)
. typeName
is the name of your record, fieldDeclarations
declare the fields all records of that type must define, and implementations
is a map of protocol names to their implementations. This corrals a record into a very neat space. The fields which it is guaranteed to have are pretty well defined. The protocols for which it can be used are very well defined. Let us take a look at an example.
var Human = Record("Human", ["name", "temperature", "isTheFonz?"],
{TemperatureRater: tempImpl
HypeRater:{
isCool: isTheFonz,
isUnpopular: not(isTheFonz)}});
Our Human
constructor is created by the Record
call. We tell
it that we have a "Human"
for the name of the record.
A Human
has three fields, ["name", "temperature", "theFonz?"]
.
The Human
implements two protocols, HypeRater
and TemperatureRater
.
For the HypeRater
, it provides its own implementation. For the
TemperatureRater
, it reuses the already defined tempImpl
.
What are some of the properties of our records and what can we do with
them? First things first: to create a new Human
, we would call
the function returned to us by our Record
call, the function we
have stored in our Human
variable. Human(["Sonali", 22, true])
would
create a new human, whose name is Sonali, whose temperature is 22, who
is the Fonz (in spirit). How would we access her name?
That depends on implementation4. The implementation I have provided
with this article creates records using Mori5. Mori provides us with persistent data structures. These data structures use something called structural sharing to make copies and updates at a very low cost. If we were using Object.freeze
we would be copying the entire object every time we wanted to create an update. With persistent data structures, we share as much of the old data structure as possible.
Our use of persistent data structures mean we cannot use the traditional
JavaScript forms of property access, because we are not using traditional
JavaScript data structures. Thus, we will use assoc
and get
to interact with
our object.
var mori = require('mori');
var vec = mori.vector; // mori vectors are like immutable arrays
var get = mori.get; // get allows us to search inside an object
var assoc = mori.assoc; // assoc allows us to create new versions of our objects
var sonalisName = get(sonali, "name"); // sonalisName === "Sonali"
var sonalisTemp = get(sonali, "name"); // sonalisTemp === 22
var sonalisFonz = get(sonali, "theFonz?"); // sonalisFonz === true
// assoc will create a copy of sonali, where "name" is now equal to "Sal"
var sal = assoc(sonali, "name", "Sal");
var salsName = get(sal, "name"); // salsName === "Sal"
var sonalisName = get(sonali, "name"); // sonalisName === "Sonali"
As you can see, assoc
is not mutative. It creates a copy. This means that you can feel free to pass copies of your objects around to anywhere in your program, and not fret about it getting mangled deep inside some callbacks. This property makes working with others a breeze.
One final advantage that records have over vanilla JavaScript objects is that records can have field declarations that are generated programmatically. Let’s try and implement a tic-tac-toe board.
When creating two-dimensional boards, the most common strategy is to create a two-dimensional array. This can make some things confusing. For one, when doing things like mapping across that array, we have to zip up and down in one direction. If we are just iterating across the array, we need to access two things. This means you would have to use two for
loops to do stuff. In one, you would access a row, and in the other, you would dig inside the row for the proper cell. As human beings, we do not think about two dimensional boards in terms of “get row, and then dig inside the row.” We think about boards in terms of coordinates.
You might be asking me to wait. We can use a flat representation of a board instead, and by doing that, and utilizing modulos and other tricks, we can flatten
a two dimensional array into a single dimension.
If we try to use just one array, so that we can use a single for
loop, we exacerbate the problem of representation; that is, we make our representation of
our board very different from how we think about it in our minds so that we can utilize our old data structure. No one thinks about two dimensional boards
as a single list of cells, where one can access things inside of of it using modulo.
var cells = ["top left","top middle","top right",
"mid left","mid center","mid right",
"bot left","bot center","bot right"];
var TicTacToe = Record("Tic Tac Toe", cells, {});
var myBoard = TicTacToe([0, 0, 0,
0, 0, 0,
0, 0, 0]);
// Here we create a new board, where we change the key
// vec("center", "middle") to hold an "X"
var myBoard2 = assoc(myBoard, "mid center", "X")
get(myBoard, "mid center") // "X"
That’s pretty beautiful! As you can see, we can access fields
by get
ting the exact cell that we want. We access things in
exactly one get
. We can iterate over all the keys in the
board my using mori.map(mori.keys(myBoard), ...)
where ...
represents some sort of function. No two for
loops required.
Getting a new object with something changed is just as
easy. You do it in one fell swoop using assoc
.
But that’s relatively simple.
Typing all that stuff out for tic-tac-toe was doable because we only had nine different entries, but what if I want a chess board? We can do it! Here, instead of building that array of cells by hand, we’ll generate it.
var rows = [1,2,3,4,5,6,7,8];
var cols = ["a", "b", "c", "d", "e", "f", "g"];
var cells = rows.map(function(row){
return cols.map(function(col){
return row + col;
});
});
var ChessGame = Record("Chess Game", cells, {});
var myGame = ChessGame([]);
var myGame1 = assoc(myGame, "a1", "knight");
get(myGame1, "a1"); //=> "knight"
Well, I promised you a dystopic tower of well defined functional dispatch, and you’ve got it! While I do not recommend adopting it for any serious codebase (because this library is not tested or optimized), I encourage you to try it out for small projects. Good representation of data is a key aspect of getting lots of power out of functional programming, and without constructs like this
, you might find functional programming in JavaScript to be relatively bulky because of all the copy-on-write involved. Feel free to check out the source code. If you like the style, ask library providers to give you these tools so that you can use it in your own codebase!
-
If not, maybe You Don’t Know JS ↩
-
As an alternative to the library we will use, check out Facebook’s Immutable.js ↩