📜 ⬆️ ⬇️

Entertaining javascript: no braces

image


I've always been surprised by JavaScript, primarily because it probably like no other widely used language supports both paradigms at the same time: normal and abnormal programming. And if almost all is read about adequate best practices and templates, then the wonderful world of how not to write code, but you can, remains only slightly ajar.


In this article we will examine another contrived task that requires unforgivable abuse of a normal solution.


Previous task:



Wording


Implement a decorating function that counts the number of calls to the transferred function and provides the ability to get this number on demand. The decision is forbidden to use braces and global variables.

The call counter is just a reason, because there is console.count () . The bottom line is that our function accumulates some data when calling the wrapped function and provides some interface for accessing it. This may be the preservation of all the results of the call, and the collection of logs, and some memoization. Just a counter - primitive and clear to everyone.


All complexity is in abnormal limitation. You can not use curly brackets, and therefore will have to revise the everyday practices and ordinary syntax.


Habitual decision


First you need to choose a starting point. Usually, if the language or its extension does not provide the necessary decoration function, we will implement some container on our own: the wrapped function, the accumulated data and the access interface to them. This is often a class:


class CountFunction { constructor(f) { this.calls = 0; this.f = f; } invoke() { this.calls += 1; return this.f(...arguments); } } const csum = new CountFunction((x, y) => x + y); csum.invoke(3, 7); // 10 csum.invoke(9, 6); // 15 csum.calls; // 2 

It does not suit us immediately, because:


  1. In JavaScript, it is impossible to implement a private property in this way: we can both read the calls of the instance (which we need) and write values ​​to it from the outside (which we do NOT need). Of course, we can use a closure in the constructor , but then what is the point of the class? And I would still be afraid to use fresh private fields without babel 7 .
  2. The language supports the functional paradigm, and creating an instance through new does not seem to be the best solution here. It is more pleasant to write a function that returns another function. Yes!
  3. Finally, the syntax ClassDeclaration and MethodDefinition will not allow us at all desire to get rid of all curly braces.

But we have a wonderful Pattern Module that implements privacy using a closure:


 function count(f) { let calls = 0; return { invoke: function() { calls += 1; return f(...arguments); }, getCalls: function() { return calls; } }; } const csum = count((x, y) => x + y); csum.invoke(3, 7); // 10 csum.invoke(9, 6); // 15 csum.getCalls(); // 2 

With this you can already work.


Entertaining decision


What are braces generally used for? These are 4 different cases:


  1. Definition of the body of the function count ( FunctionDeclaration )
  2. Initialization of the returned object
  3. The definition of the body of the function invoke ( FunctionExpression ) with two expressions
  4. Body definition of getCalls ( FunctionExpression ) function with one expression

Let's start with the second paragraph. In fact, we have no reason to return a new object, while complicating the call to the final function through invoke . We can take advantage of the fact that a function in JavaScript is an object, and therefore it can contain its own fields and methods. Create our return function df and add the getCalls method to it , which through the closure will have access to the calls as before:


 function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = function() { return calls; } return df; } 

It is more pleasant to work with this:


 const csum = count((x, y) => x + y); csum(3, 7); // 10 csum(9, 6); // 15 csum.getCalls(); // 2 

With the fourth point, everything is clear: we simply replace FunctionExpression with ArrowFunction . The absence of braces will provide us with a short write of the switch function in the case of a single expression in its body:


 function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = () => calls; return df; } 

With the third - all the more difficult. Remember that first of all we replaced FunctionExpression of the invoke function with FunctionDeclaration df . To rewrite it on ArrowFunction, two problems will have to be solved: not to lose access to the arguments (now it is the arguments pseudo-array) and to avoid the body of the function from two expressions.


The parameter args with the spread operator, which is explicitly specified for the function, will help us with the first problem. And to combine two expressions into one, you can use logical AND . Unlike the classical logical conjunction operator that returns a boolean, it calculates the operands from left to right to the first "false" and returns it, and if all "true" - the last value. The first increment of the counter will give us 1, which means that this sub-expression will always be reduced to true. The reducibility to "truth" of the result of a function call in the second sub-expression does not interest us: the calculator will stop on it in any case. Now we can use ArrowFunction :


 function count(f) { let calls = 0; let df = (...args) => (calls += 1) && f(...args); df.getCalls = () => calls; return df; } 

You can slightly decorate the record using the prefix increment:


 function count(f) { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; } 

We will start the solution of the first and most difficult point by replacing FunctionDeclaration with ArrowFunction . But we still have the body in curly brackets:


 const count = f => { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; }; 

If we want to get rid of curly brackets framing the function body, we will have to avoid declaring and initializing variables through let . And we have two variables: calls and df .


First, let's deal with the counter. We can create a local variable by defining it in the function parameter list, and transfer the initial value by calling IIFE (Immediately Invoked Function Expression):


 const count = f => (calls => { let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; })(0); 

It remains to concatenate three expressions into one. Since all three expressions are functions that are always true, we can also use logical AND :


 const count = f => (calls => (df = (...args) => ++calls && f(...args)) && (df.getCalls = () => calls) && df)(0); 

But there is another option for concatenating expressions: using the comma operator . It is preferable, since it does not deal with unnecessary logical transformations and requires fewer brackets. Operands are calculated from left to right, and the result is the value of the latter:


 const count = f => (calls => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0); 

I guess I managed to fool you? We boldly got rid of the declaration of the variable df and left only the assignment of our switch function. In this case, this variable will be declared globally, which is unacceptable! For df , we repeat the initialization of a local variable in the parameters of our IIFE function, but we will not pass any initial value:


 const count = f => ((calls, df) => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0); 

Thus the goal is achieved.


Variations on the topic


Interestingly, we were able to avoid the creation and initialization of local variables, several expressions in functional blocks, and the creation of an object literal. At the same time, they preserved the purity of the original solution: the absence of global variables, the privacy of the counter, access to the arguments of the function being wrapped.


In general, you can take any implementation and try to do this. For example, the polyfill for the bind function in this plan is quite simple:


 const bind = (f, ctx, ...a) => (...args) => f.apply(ctx, a.concat(args)); 

However, if the argument f is not a function, in the good we must throw an exception. A throw exception cannot be thrown in the context of an expression. You can wait for throw expressions (stage 2) and try again. Or does someone already have thoughts?


Or consider a class that describes the coordinates of a certain point:


 class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x}, ${this.y})`; } } 

Which can be represented by a function:


 const point = (x, y) => (p => (px = x, py = y, p.toString = () => ['(', x, ', ', y, ')'].join(''), p))(new Object); 

Only here we have lost prototype inheritance: toString is a property of the Point prototype object, and not a separately created object. Can this be avoided if you try hard?


In the results of transformations, we get an unhealthy mix of functional programming with imperative hacks and some features of the language itself. If you think about it, an interesting (but not practical) obfuscator of the source code can come out of this. You can come up with your own version of the “brace obfuscator” task and entertain the colleagues and friends of JavaScript's in their spare time.


Conclusion


The question is, who cares and why? This is completely harmful for beginners, as it forms a false idea of ​​the excessive complexity and deviance of the language. But it may be useful for practitioners, as it allows you to look at the peculiarities of the language from the other side: a call not to avoid, but a call to try, to avoid in the future.


')

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


All Articles