📜 ⬆️ ⬇️

We write on JS in functional and declarative style



Introduction


I love functional languages ​​for their simplicity, clarity and predictability. I write mainly on Elixir / Erlang / OTP, I tried other languages, but Erlang with its actors is still much closer to me than Lisp or Haskell, for example. As you know, Erlang == web, and something written for the web sometimes has a client web interface: html, css, js - content. Alas, js is the standard of the modern web, for it there are libraries for almost any task for almost all occasions, and this is more or less the only available tool to do something in the client-side browser. Therefore, we still need js. At first I thought “Lambda and functions of a higher order are there, it will be easy to write on js. I will learn the syntax and write as I write in Erlang / Lisp / Haskell. ” How wrong I was.


Choose a language


Let's start with the fact that pure js is absolutely no good for writing code. From the abundance of braces and semicolons in the eyes. The word return written anywhere in the function body completely opaquely hints at the imperativeness of the language and destroys my belief in the best. There are many languages, including and functional ( purescript , fay , clojurescript ) compiled in js. But for myself, I chose coffeescript - a fairly compromise version that both functionaries and imperatives can understand. In part, this choice was justified by the fact that I use to build projects brunch , which is written in coffeescript. Well, compared to fay, for example, the overhead for switching from js to coffeescript is almost equal to 0.
')

Choosing a framework


The second intriguing question is how to connect our code and html-page. There are a lot of options. There are a lot of huge frameworks that actually look quite holistic. I myself used angularjs for some time, but after some practice, obvious disadvantages became apparent: in order to do something, directives are needed, if they are not there, you need to write your bikes, and to understand the internal structure of the angulyar is more difficult than it seems, also view frankly - the most frequently used ng-model directive provides two-way data binding and presentation, which from the point of view of the functional manager is completely ideomatically incorrect and breaks encapsulation in general, moreover, all these Angulyar applications are otroller etc. itp quite heavy weight code. Yes, and by the way, the performance of an angulyar is really so-so. Some time ago I met with react js - and my choice fell not on him. The idea impresses with its simplicity and more or less functional-declarative style. There is a state that can change in the course of the application operation. We just pass it from time to time to jreact for rendering.

widget = require("widget") do_render = () -> React.render(widget(state), domelement) if domelement? render_process = () -> try do_render() catch error console.log error setTimeout(render_process, 500) 


And that's it! Madness is simple and effective. React is concerned with the optimization of rendering, we can now not be interested in this issue at all. It remains to ensure that the state object changes in a way that fits the functional paradigm. And here the most interesting begins.

Trouble in the kitchen


The first and not very significant problem is the soft types js. In Erlang, they are of course also soft, but in js they are really soft as, sorry for the expression, shit. For example, a not very informative, but rather funny video on this topic. But in practice, type conversions happen infrequently (in any case, if your code is good) - so I took the soft types js more or less as they are.

When I started practicing a little js at first everything was more or less good, but at some point the applications for some reason started to work not at all as I wanted. I climbed deeper and saw the terrible:

 coffee> map = {a: 1} { a: 1 } coffee> lst = [] [] coffee> lst.push map 1 coffee> map.a = 2 2 coffee> lst.push map 2 coffee> map.a = 3 3 coffee> lst.push map 3 coffee> lst [ { a: 3 }, { a: 3 }, { a: 3 } ] 


although in this case I certainly expected to see

 coffee> lst [ { a: 1 }, { a: 2 }, { a: 3 } ] 


It really was a shock. The bingo data in js is mutable! And as it turned out - everything that is more complicated than the number, string, null and undefined will be passed by reference!

But when I saw that

 coffee> [1,2,3] == [1,2,3] false coffee> {a: 1} == {a: 1} false 


then my hair began to move in various places. For such a life I did not prepare. It turns out that the data types of maps and lists in js are not compared by value, but also by reference.

I began to think how to be. With regards to mutability, for example, it was decided to wrap the constant data (for example, to initialize any values) in the lambda-function of arity zero, in such cases they really remained in general not mutable. You can simply call them in those expressions where they are needed and not be afraid that they (the data) will change.

 coffee> const_lst = () -> [1,2,3] [Function] coffee> new_lst = const_lst().concat([4,5,6]) [ 1, 2, 3, 4, 5, 6 ] coffee> const_lst() [ 1, 2, 3 ] 


In principle, I thought, if I recursively wrap all the data in general, if all functions take lambdas and return lambdas, the language will become really functional! In principle, this is a solution. You just need to describe these lambda data types based on ordinary types, write functions for recursive direct and inverse transformations into ordinary js types, as well as higher-order functions for working with these lambda types (map, reduce, filter, zip, etc.). At the same time, by the way, you can make these new types less soft. The task is, in principle, solved, but rather voluminous, partly by the way already implemented, for example, in this library . But this approach has quite significant drawbacks:

1) Since our code is usually not suspended in the air, but has dependencies on other js libraries, each time referring to them, you need to remember to convert the lambda type to the usual type, and vice versa.
2) With this approach, we will certainly to some extent ensure the purity of the functions and the immunity of the data, but there will still not be transactional
3) This code will not be very clear to those who prefer the imperative approach.

Thus, I have so far refused this idea (but if I pay attention to it in the future) and decided to do something less radical, but just as simple and understandable. Obviously, in order to locally solve the problem of mutability and comparing data by reference, I needed to learn how to recursively copy and compare any js data by value.

We write the clone function
 clone = (some) -> switch Object.prototype.toString.call(some) when "[object Undefined]" then undefined when "[object Boolean]" then some when "[object Number]" then some when "[object String]" then some when "[object Function]" then some.bind({}) when "[object Null]" then null when "[object Array]" then some.map (el) -> clone(el) when "[object Object]" then Object.keys(some).reduce ((acc, k) -> acc[clone(k)] = clone(some[k]); acc), {} 


Writing the equal function
 equal = (a, b) -> [type_a, type_b] = [Object.prototype.toString.call(a), Object.prototype.toString.call(b)] if type_a == type_b switch type_a when "[object Undefined]" then a == b when "[object Boolean]" then a == b when "[object Number]" then a == b when "[object String]" then a == b when "[object Function]" then a.toString() == b.toString() when "[object Null]" then a == b when "[object Array]" len_a = a.length len_b = b.length if len_a == len_b [0..len_a].every (n) -> equal(a[n], b[n]) else false when "[object Object]" keys_a = Object.keys(a).sort() keys_b = Object.keys(b).sort() if equal(keys_a, keys_b) keys_a.every (k) -> equal(a[k], b[k]) else false else false 


It turned out to be simpler than I thought, the only "but" is that if there are circular references in the data, of course, we will get a stack overflow. For me, in principle, this is not a problem, since I do not use such abstractions as "circular references". I think you can somehow and process them in the process of data cloning, but then of course the code will not be so simple and elegant. In general, I collected these and some other functions in the library and I think that the problem of data mutability in js for some time has been solved for me.

Actors


Now let's talk why we need the transactional change in data. Suppose there is some more or less complex state and we change it in the process of performing a function.
 state.aaa = 20 state.foo = 100 state.bar = state.bar.map (el) -> baz(el, state) 


In practice, the process of changing the state of course can be more complex and lengthy, contain asynchronous calls to external api, etc., etc. But the bottom line is that if somewhere in the process of changing a state, the function func (state) will be called somewhere else — what will happen? Will the half-changed state be valid? And maybe due to the single-threaded js semi-modified state, there will not be any at all and everything is fine? And if not? And what should I do if I need to make some external calls and it is vital that while I make them state changed? In order not to wrestle with such difficult issues, we will make the state change transactional.

Here I think many will remember about mutexes. I also remembered mutexes. And about the states of the race. And about deadlocks. And I realized that I do not want this at all, so we will not write a mutex, but will borrow the concept of “actor” from the Erlang language. In the context of js, the actor will simply be some kind of object that is initialized by a certain state, and will do only three things.

1) receive “messages” as arity 1 or 0 functions and add them to the queue
2) independently “raking” the message queue by applying the arity 1 functions to its internal state (the arity 0 functions are simply called, the state does not change) - all this is done strictly in the order in which the messages were received.
3) on request, return the value of its internal state

Naturally, in order to comply with non-data mutability, we will each time clone a state when changing, and in the get function we will return not its state itself, but its copy. For this we will use the library written earlier. As a result, we get the code.

 window.Act = (init_state, timeout) -> obj = { # # priv # state: Imuta.clone(init_state) queue: [] init: () -> try @state = Imuta.clone(@queue.shift()(@state)) while @queue.length != 0 catch error console.log "Actor error" console.log error this_ref = this setTimeout((() -> this_ref.init()), timeout) # # public # cast: (func) -> if (func.length == 1) and Imuta.is_function(func) @queue.push(Imuta.clone(func)) @queue.length else throw(new Error("Act expects functions arity == 1 (single arg is actor's state)")) zcast: (func) -> if (func.length == 0) and Imuta.is_function(func) @queue.push( ((state) -> Imuta.clone(func)(); state) ) @queue.length else throw(new Error("Act expects functions arity == 0")) get: () -> Imuta.clone(@state) } obj.init() obj 

* Functions that put lambda in a queue are called cast and zcast, by analogy with Erlang functions handle_cast

Since not all js-data is cloned (remember cyclic references and external libraries), we will create another version of the constructor for the actor in which we will remove the internal state cloning and collect the whole thing into the library .

Enjoying:
 coffee> actor = new Act({a: 1}, "pure", 500) { state: { a: 1 }, queue: [], init: [Function], cast: [Function], zcast: [Function], get: [Function] } coffee> actor.cast((state) -> state.b = 1; state) 1 coffee> actor.get() { a: 1, b: 1 } coffee> actor.cast((state) -> state.c = 1; state) 1 coffee> value = actor.get() { a: 1, b: 1, c: 1 } coffee> value.d = 123 123 coffee> value { a: 1, b: 1, c: 1, d: 123 } coffee> actor.get() { a: 1, b: 1, c: 1 } coffee> actor.zcast(() -> console.log "hello") 1 coffee> hello coffee> actor.get() { a: 1, b: 1, c: 1 } coffee> global_var = {foo: "bar"} { foo: 'bar' } coffee> actor.cast((_) -> global_var) 1 coffee> actor.get() { foo: 'bar' } coffee> global_var.baz = "baf" 'baf' coffee> global_var { foo: 'bar', baz: 'baf' } coffee> actor.get() { foo: 'bar' } 


All state changes are made exclusively through the queue using the cast function. As we can see in the “clean” version of the state, it is completely encapsulated inside the actor, no matter what we do with it after getting from the get function (the same is true if we add something from the outside world to the cast function). Transactional provided by the message queue. We received practically Erlang code, only on js. If we want to use non-clonable data in our state for some reason, then we will simply use the “dirty” version of the actor with a global state. In principle, even this option (if the state is changed strictly through the actor) is acceptable and ensures the transactional change of the data. There was also the idea to make changes to the data not just transactional, but in some sense even atomic, transferring not one lambda, but three (for example, in case of some use in external libraries).

 actor.cast( { prepare: (state) -> prepare_process(state) apply: (state, args) -> do_work(state, args) rollback: (state, args) -> do_rollback(state, args, error) }) 


But I thought that this is already a bend, especially since in the current version you can just write
 actor.cast((state) -> args = init_args(state) try do_work(state, args) catch error rollback(state, args, error)) 

if suddenly atomicity is so vital.

Conclusion


Js ended up not as hopeless as it seemed to me at the beginning. Behind its lambda functions, there is a real functional power and, with the right level of skill, you can write in a rather declarative style on it. And for a snack, a simple example using the actors + react + jade + sass + bullet (Erlang web sockets). Stay functional, stay web!

UPD


Residents of Habr pointed out a number of gross errors in my code, thanks to them for it :) Corrected the shortcomings, namely:

1) Replaced in the clone binding function with a more adequate and obvious operation of cloning the function
2) In the equal function, I added a fairly obvious test at the beginning which should improve performance :)
3) I have removed the silly function check by the value of func.toString ().
4) When comparing arrays, I no longer use the possibly redefined field length, but simply count the elements
5) When storing objects, I do not use the "Object.keys" function, but simply collect all the keys from the object. In a sense, the code has become so even slimmer and more functional.

Thanks again to all those who contributed to making this code better. Actual versions of libraries can be found here and here . I am pleased to hear your wishes and suggestions.

Source: https://habr.com/ru/post/260961/


All Articles