神刀安全网

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 ? 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 . 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 . 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 implementation. The implementation I have provided with this article creates records using Mori. 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!

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » This just isn't functional

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址