Функциональное программирование на CoffeeScript с библиотекой f_context

    Тем, кто сталкивался с функциональными языками программирования наверняка знакома такая конструкция:
      fact(0) -> 1
      fact(N) -> N * fact(N - 1)
    

    Это один из классических примеров ФП — вычисление факториала.
    Теперь это можно делать и на CoffeeScript с библиотекой f_context, просто оборачивая код в f_context ->, например:
      f_context ->
        fact(0) -> 1
        fact(N) -> N * fact(N - 1)
    


    Как это работает
    Посмотреть больше примеров

    Что умеет библиотека


    Модули
    Pattern Matching
    Destructuring
    Guards
    Переменная _ и матчинг одинаковых аргументов

    Примеры из реальной жизни


    Модульность:

    По умолчанию f_context возвращает сгенерированный модуль, т.о. его можно положить в какую-то переменную:
    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]
    

    Успользуя директиву module можно сразу положить модуль в глобальное пространство,
    например window или 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

    что это и зачем оно нужно
    Пример:
      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}"
    

    Результат:
      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

    что это и зачем оно нужно
    Примеры:
      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}
    

    Результат:
      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

    что это и зачем оно нужно
    Гварды задаются через директиву where(%condition%).
    В гвардах можно задавать более гибкое сравнение. Пример вычисления ряда Фибоначчи:
      #без гвардов
      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])
    



    Переменная _ и матчинг одинаковых аргументов

    Переменная _ служит для «сбрасывания» аргументов, в случае если их значение не важно, но важно количество аргументов
    Пример реализации функции 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))
    

    Если задать одинаковые названия аргументов, то автоматически будет проведено их сравнение.
    Пример реализации функции range:
    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])
    



    Примеры из жизни

    Порой реализовать тот или иной алгоритм гораздо удобнее с помощью рекурсии.
    Например, функции reduce и quick sort в функциональном стиле:
    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))...
        ]
    

    на мой взляд проще и понятнее своих императивных аналогов.


    Как это работает


    Вернемся к самому первому в этой статье примеру — вычисление факториала.
      fact(0) -> 1
      fact(N) -> N * fact(N - 1)
    

    В CoffeeScript это является абсолютно валидной конструкцией.

    Пример посложнее, подсчет количества элементов в списке в функциональном стиле:
      count(List) ->
        count(List, 0)
    
      count([], Iterator) -> Iterator
      count([Head, List...], Iterator) ->
        count(List, Iterator + 1)
    

    И это тоже валидный CoffeeScript код.
    Таким образом мы можем писать в привычном для функциональных языков стиле прямо на coffee, без надобности использовать прекомпилятор.

    Для наглядности дальше буду рассказывать на примере все того же факториала. Итак,
    этот код:
      fact(0) -> 1
      fact(N) -> N * fact(N - 1)
    

    странслируется в js следующим образом:
      fact(0)(function() {
        return 1;
      });
      fact(N)(function() {
        return N * fact(N - 1);
      });
    

    Его даже можно исполнить, но с ошибками «ReferenceError», про то что fact и N не объявлены.
    Можно завернуть этот код в функцию-обертку, таким образом чтобы он передавался в качестве аргумента
      function_wrapper ->
        fact(0) -> 1
        fact(N) -> N * fact(N - 1)
    

    в js получим следующее:
    function_wrapper(function() {
        fact(0)(function() {
          return 1;
        });
        fact(N)(function() {
          return N * fact(N - 1);
        });
    });
    

    Теперь function_wrapper может проанализировать функцию, передаваемую ей в
    аргументе и передать в нее все недостающие переменные. Примерно вот так:
    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)
    
    }
    

    Теперь код выполняется уже без ошибок, но пока ничего не делает.
    Дальше дело за fact_stub, которая тоже может проанализировать свои аргументы и
    сгенерировать свою локальную ветвь текущей функции. Для факториала это будет выглядеть примерно вот так:

    После того как дерево парсинга построено, можно легко сгеренировать модуль и создать его через все тот же Function.apply
    Тело модуля должно получиться вот таким:
    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
    };
    


    На деле все немного сложнее, но здесь я постарался изложить базовые принципы работы.
    Если статья не понятна — пишите, постараюсь исправить.

    Сама библиотека лежит вот здесь: github.com/nogizhopaboroda/f_context

    Любые комментарии приветствуются. Как и feature-request'ы.

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Понятна ли статья?

    Поделиться публикацией

    Комментарии 25

      +1
      Отсутствие pattern matching — на мой взгляд основной недостаток Coffeescript. Хорошо что он преодолен.
        +5
        я думаю, люди так считают про любой язык где нет pattern matching'a, когда узнают, что это такое
      • НЛО прилетело и опубликовало эту надпись здесь
          +5
          Не имя красит человека)
          +3
          Это очень плохо. Единственный нормальный вариант, как такое можно делать — создавая расширения для Coffee или, например, такими способами как sweetjs.org/. Но не дай бог такое использовать в продакшене.
            0
            Скажите, а чем принципиально в данном случае прекомпилированный код отличается от сгенерированного на лету? И неужели установить расширение для Coffee и затем прекомпилировать файл удобнее и легче чем просто подключить библиотеку (при том что она весит 11к не пожатая)?
              +1
              Достаточно того, что в таком коде теряется доступ к замыканию, остаётся доступ только к глобальным объектам. Производительность с таким подходом оставляет желать лучшего, особенно если подобная функция создаётся неоднократно. Зачем здесь перекомпиляция — вообще не ясно. Библиотека, ссылка на которую чуть ниже, прекрасно обходится и без этого.
                –3
                Простите, а Вы код читали? Он вот здесь github.com/nogizhopaboroda/f_context/blob/master/src/f_context.coffee
                Вас не затруднит показать мне в каком месте теряется доступ к замыканию и где функция создается неоднократно?
                  +2
                  Я читал код из статьи, мне его хватило, что бы пропало всё желание читать код самой библиотеки. Здесь, например, ошибки не видите?

                  var new_function = Function.apply(null, fn_body, /*именованные аргументы*/ 'fact', 'N');
                  

                  Вас не затруднит показать мне в каком месте теряется доступ к замыканию

                  Да без проблем. Добавим к примеру с факториалом применение произвольной функции из замыкания:

                  ( ->
                    mod = (it)-> it | 0
                    O = f_context ->
                      fact(0) -> mod 1
                      fact(N) -> mod N * fact(N - 1)
                    console.log O.fact 10
                  )()
                  

                  Результат — Uncaught ReferenceError: mod is not defined.
              –3
              Такое заявление без аргументации является попыткой набросить. Библиотека работает и решает свою работу, просадки по производительности не создает. Так в чем проблема?
              +1
              Вот вам еще паттерн матчинг для Coffeescript с чуть более приятным конструкциями: github.com/CRogers/pun
              Автозаменяемый на лету код очень сложно дебажить и предсказать его поведение. Если уж хочется функциональщины, то есть множество вариантов вроде Clojurescript, Elm и тд — можно использовать на ура, принеся в жертву незаменимый дебаггер. И не говорите про source maps, их предельный максимум — чтение кода, вменяемую отладку на них никогда не построить.
                0
                вменяемую отладку на них никогда не построить

                Это еще почему?
                  –2
                  А как? На выходе это просто символьная карта, которая при отладке ведет себя непредсказуемо, не связывая цепочку выполнения с оригинальным кодом. Тот же coffeescript не отладить, потому что в половине случаев я просто не могу поставить брейк поинт в нужное мне место. Плюс ко всему dev tools подвисает секунд на 30 при разбивке кода по этим самым source maps.
                    0
                    Вы что-то делает не так. У меня на проекте гораздо более сложные трансформации, чем просто coffee и все отлично дебажится.
                      0
                      Поверю на слово. Мне не так с этим везет, отключаю все map файлы и дебажу чистый js.
                  0
                  Автозаменяемый на лету код очень сложно дебажить и предсказать его поведение

                  Конкретно в случае данной библиотеки — вы заблуждаетесь. И вот почему:

                  1) Вот пример (Вы уж простите за этот преславутый факториал, но он самый короткий и наглядный):
                    f_fact(0) -> 1
                    f_fact(N) ->
                      debugger
                      N * f_fact(N - 1)
                  

                  Сгенерированный библиотекой код:
                  ...
                  var f_fact_local_0 = function(){ 
                        return 1;
                       };
                  var f_fact_local_1 = function(N){ 
                        debugger; //вот ваш дебаггер, все нужные переменные на месте
                        return N * f_fact(N - 1);
                       };
                  var f_fact = function(){
                    if(arguments[0] === 0 && arguments.length === 1){
                  	return f_fact_local_0();
                  }
                  if(arguments.length === 1){
                  	return f_fact_local_1(arguments[0]);
                  }
                  ...
                  

                  Весь код как на ладони.

                  2) Это ведь просто функция, у нее нет состояния, а значит ее результат выполнения зависит только от входных параметров, а значит ее очень легко покрыть тестами и ничего не надо дебажить.

                  Но спасибо за идею, дипишу в ридми про легкость отладки.
                    0
                    Как сгенерированный налету код можно увидеть в отладчике без прописывания debugger? По факту это дополнительный постпроцессинг в в браузере после сборки coffee кода, производительность не измерялась?
                      0
                      Без прописывания debugger никак. Но зачем Вам это? Я не потроллить, мне просто интересно.
                      По производительности — обычная рекурсия. Лупам, конечно, проигрывает, но как раз над этим я сейчас работаю.
                        0
                        Для прозрачности и четкого понимания следования выполнения кода. Не вылажу из дебагера, поэтому не понимаю как работать с «невидимым» кодом при пошаговой отладке. Про производительность выше уже подметили.
                          –2
                          Погодите, то есть Вы перечитываете все скрипты перед запуском или как?
                          Вас, как человека, не вылазящего из дебагера, наверное не должно удивить, что как только, при пошаговой отладке, дебагер дойдет до исполнения функции, сгенерированной данной библиотекой, он откроет код всего сгенерированного модуля в новой вкладке и выставит курсор ровно на строчке с этой самой функцией.
                          Хотите, я для Вас маленький скринкаст запишу раз уж Вы на слово не верите?
                            0
                            Попробовал, не впечатлило. Действительно есть заход в сгенерированный модуль, но как дальше быть с этим самостоятельным островком кода, который ничего не знает о контексте, не понятно. Единственный вариант работы с библиотекой — это придерживаться чистоты функций путем прокидывания в них контекста и прочих параметров, а это уже каноническое ФП и прощайте замыкания. Тот же require придется прокидывать внутрь окружения f_context.
                    0
                    В Clojure pattern matching тоже не встроенный, а сделанный отдельной библиотекой.
                      0
                      Спасибо за ссылку.
                      Но все же синтаксис pattern matching у pun менее понятен.
                      0
                      Благодарю за статью,

                      На мой взгляд реализация функциональных конструкций, особенно pattern matching, получилась громоздкой.
                        0
                        Спасибо за комментарий. Постараюсь сделать лаконичнее в следующем релизе.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое