📜 ⬆️ ⬇️

Functional CoffeeScript programming with the f_context library

Those who are faced with functional programming languages ​​probably know this structure:
fact(0) -> 1 fact(N) -> N * fact(N - 1) 

This is one of the classic examples of FP - factorial computing.
Now it can be done on CoffeeScript with the f_context library, simply by wrapping the code in f_context -> , for example:
  f_context -> fact(0) -> 1 fact(N) -> N * fact(N - 1) 


How it works
View more examples.

What the library can do


Modules
Pattern Matching
Destructuring
Guards
Variable _ and Matching Arguments

Real life examples
')

Modularity:

By default, f_context returns the generated module, so it can be put in some variable:
 examples = f_context -> f_range(I) -> f_range(I, 0, []) f_range(I, I, Accum) -> Accum f_range(I, Iterator, Accum) -> f_range(I, Iterator + 1, [Accum..., Iterator]) examples.f_range(10) #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 

By using the module directive, you can immediately put the module in global space,
for example window or global :
 f_context -> module("examples") f_range(I) -> f_range(I, 0, []) f_range(I, I, Accum) -> Accum f_range(I, Iterator, Accum) -> f_range(I, Iterator + 1, [Accum..., Iterator]) examples.f_range(10) #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 


Pattern Matching

what is it and why is it needed
Example:
  matching_example_1("foo") -> "foo matches" matching_example_1("bar") -> "bar matches" matching_example_1(1) -> "1 matches" matching_example_1(true) -> "true matches" matching_example_1(Str) -> "nothing matches, argument: #{Str}" 

Result:
  matching_example_1("foo") #returns "foo matches" matching_example_1("bar") #returns "bar matches" matching_example_1(1) #returns "1 matches" matching_example_1(true) #returns "true matches" matching_example_1("baz") #returns "nothing matches, argument: baz" 



Destructuring

what is it and why is it needed
Examples:
  test_destruct_1([Head, Tail...]) -> {Head, Tail} test_destruct_1_1([Head, Head1, Tail...]) -> {Head, Head1, Tail} 


  test_destruct_2([Head..., Last]) -> {Head, Last} test_destruct_2_1([Head..., Last, Last1]) -> {Head, Last, Last1} 


  test_destruct_3([Head, Middle..., Last]) -> {Head, Middle, Last} test_destruct_3_1([Head, Head2, Middle..., Last, Last2]) -> {Head, Head2, Middle, Last, Last2} 

Result:
  test_destruct_1([1,2,3]) #returns {Head: 1, Tail: [2,3]} test_destruct_1_1([1,2,3,4]) #returns {Head: 1, Head1: 2, Tail: [3,4]} test_destruct_2([1,2,3]) #returns {Head: [1,2], Last: 3} test_destruct_2_1([1,2,3,4]) #returns {Head: [1,2], Last: 3, Last1: 4} test_destruct_3([1,2,3,4]) #returns {Head: 1, Middle: [2,3], Last: 4} test_destruct_3_1([1,2,3,4,5,6]) #returns {Head: 1, Head2: 2, Middle: [3,4], Last: 5, Last2: 6} 



Guards

what is it and why is it needed
Guards are set via the where directive (% condition%) .
In the guards, you can set a more flexible comparison. An example of calculating the Fibonacci series:
  #  fibonacci_range(Count) -> fibonacci_range(Count, 0, []) fibonacci_range(Count, Count, Accum) -> Accum fibonacci_range(Count, 0, Accum) -> fibonacci_range(Count, 1, [Accum..., 0]) fibonacci_range(Count, 1, Accum) -> fibonacci_range(Count, 2, [Accum..., 1]) fibonacci_range(Count, Iterator, [AccumHead..., A, B]) -> fibonacci_range(Count, Iterator + 1, [AccumHead..., A, B, A + B]) 

  #  fibonacci_range(Count) -> fibonacci_range(Count, 0, []) fibonacci_range(Count, Count, Accum) -> Accum fibonacci_range(Count, Iterator, Accum) where(Iterator is 0 or Iterator is 1) -> fibonacci_range(Count, Iterator + 1, [Accum..., Iterator]) fibonacci_range(Count, Iterator, [AccumHead..., A, B]) -> fibonacci_range(Count, Iterator + 1, [AccumHead..., A, B, A + B]) 



Variable _ and Matching Arguments

The variable _ serves to “drop” arguments, if their value is not important, but the number of arguments is important.
An example of the implementation of the function all:
 f_all([Head, List...], F) -> f_all(List, F, F(Head)) f_all(_, _, false) -> false f_all([], _, _) -> true f_all([Head, List...], F, Memo) -> f_all(List, F, F(Head)) 

If you specify the same argument names, they will be automatically compared.
An example of the implementation of the range function:
 f_range(I) -> f_range(I, 0, []) f_range(I, I, Accum) -> Accum f_range(I, Iterator, Accum) -> f_range(I, Iterator + 1, [Accum..., Iterator]) 



Life examples

Sometimes it is much more convenient to implement one or another algorithm using recursion.
For example, the reduce and quick sort functions in the functional style:
 f_context -> f_reduce(List, F) -> f_reduce(List, F, 0) f_reduce([], _, Memo) -> Memo f_reduce([X, List...], F, Memo) -> f_reduce(List, F, F(X, Memo)) 

 f_context -> f_qsort([]) -> [] f_qsort([Pivot, Rest...]) -> [ f_qsort((X for X in Rest when X < Pivot))..., Pivot, f_qsort((Y for Y in Rest when Y >= Pivot))... ] 

in my opinion it is simpler and clearer than their imperative counterparts.


How it works


Let's return to the very first example in this article - factorial calculation.
  fact(0) -> 1 fact(N) -> N * fact(N - 1) 

In CoffeeScript, this is an absolutely valid construction.

An example is more complicated, counting the number of elements in the list in the functional style:
  count(List) -> count(List, 0) count([], Iterator) -> Iterator count([Head, List...], Iterator) -> count(List, Iterator + 1) 

And this is also valid CoffeeScript code.
Thus, we can write in the usual for functional language style directly on coffee, without the need to use a precompiler.

For clarity, I will continue to talk about the example of the same factorial. So,
this code is:
  fact(0) -> 1 fact(N) -> N * fact(N - 1) 

is bundled in js as follows:
  fact(0)(function() { return 1; }); fact(N)(function() { return N * fact(N - 1); }); 

It can even be executed, but with “ReferenceError” errors, about the fact that fact and N are not declared.
You can wrap this code in a wrapper function so that it is passed as an argument
  function_wrapper -> fact(0) -> 1 fact(N) -> N * fact(N - 1) 

in js we get the following:
 function_wrapper(function() { fact(0)(function() { return 1; }); fact(N)(function() { return N * fact(N - 1); }); }); 

Now function_wrapper can analyze the function passed to it in
argument and pass into it all the missing variables. Like this:
 var function_wrapper = function(fn){ var fn_body = fn.toString().replace(/function.*\{([\s\S]+)\}$/ig, "$1"); var new_function = Function.apply(null, fn_body, /* */ 'fact', 'N'); var fact_stub = function(){ return function(){}; }; //  N   var N_stub = function(){}; N_stub.type = "variable"; N_stub.name = "N"; new_function(fact_stub, N_stub) } 

Now the code is executed without errors, but so far does nothing.
Next is the fact_stub , which can also analyze its arguments and
generate your local branch of the current function. For factorial it will look something like this:

After the parsing tree has been built, you can easily generate a module and create it through the very same Function.apply
The body of the module should be like this:
 var f_fact_local_0 = function(){ return 1; }; var f_fact_local_1 = function(N){ return N * f_fact(N - 1); }; var f_fact = function(){ if(arguments[0] === 0){ return f_fact_local_0(); } return f_fact_local_1(arguments[0]); }; return { f_fact: f_fact }; 


In fact, everything is a little more complicated, but here I tried to set out the basic principles of work.
If the article is not clear - write, I will try to fix it.

The library itself is here: github.com/nogizhopaboroda/f_context

Any comments are welcome. Like feature-request.

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


All Articles