Functional CoffeeScript programming with the f_context library

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

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


    How it works
    See more examples

    What the library can do


    Modules
    Pattern Matching
    Destructuring
    Guards
    Variable _ and Matching Same Arguments

    Real-Life Examples


    Modularity:

    By default, f_context returns the generated module, i.e. it can be put in some kind of 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]
    

    Using the module directive, you can immediately put a module in a global space,
    such as 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 it is and why it is needed.
    Guards are specified through the where directive (% condition%) .
    In guards, you can specify a more flexible comparison. 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 the same arguments

    The _ variable is used to "reset" the arguments, if their value is not important, but the number of arguments is important.
    Example of the implementation of the all function:
    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 set the same names for the arguments, they will be automatically compared.
    An example 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 a particular algorithm using recursion.
    For example, reduce and quick sort functions in a 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 get back 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 construct.

    An example is more complicated, counting the number of elements in a list in a 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 style for functional languages ​​directly on coffee, without the need to use a precompiler.

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

    translates to 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, 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 the
    argument and pass all the missing variables to it. Something 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 up is fact_stub , which can also parse its arguments and
    generate its local branch of the current function. For a factorial, it will look something like this:

    After the parsing tree is built, you can easily generate a module and create it through the same Function.apply.
    The module body should look 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 state 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 requests.

    Only registered users can participate in the survey. Please come in.

    Is the article clear?

    • 54.9% Understand 28
    • 25.4% Don't understand 13
    • 33.3% What is it all about? 17

    Also popular now: