Пишем на JS в функционально-декларативном стиле

  • Tutorial


Введение


Я люблю функциональные языки за их простоту, ясность и предсказуемость. Пишу в основном на Elixir / Erlang / OTP, пробовал другие языки, но Erlang с его акторами пока мне гораздо ближе чем например Lisp или Haskell. Как известно Erlang == web, а у чего-либо написанного для веба порой бывает клиентский веб-интерфейс: html, css, js — содержимое. Увы js это стандарт современного веба, для него есть библиотеки почти под любую задачу почти на все случаи жизни, да и это более-менее единственное доступное средство что-то выполнить в браузере на стороне клиента. Поэтому нам всё-таки нужен js. Сперва мне подумалось «Лямбды и функции высшего порядка есть, значит писать на js будет просто. Выучу синтаксис и буду писать так же как пишу в Erlang/Lisp/Haskell». Как же я ошибался.


Выбираем язык


Начнём с того что чистый js для написания кода совершенно не годится. От обилия скобочек и точек с запятой рябит в глазах. Слово return написанное где-либо в теле функции совершенно непрозрачно намекает на императивность языка и рушит мою веру в лучшее. Есть много языков, в т.ч. и функциональных (purescript, fay, clojurescript) компилируемых в js. Но для себя я выбрал coffeescript — довольно компромиссный вариант, который могут понимать как функциональщики, так и императивщики. Отчасти этот выбор был обоснован тем что я использую для сборки проектов brunch, который написан на coffeescript. Ну и по сравнению например с fay, накладные расходы для перехода с js на coffeescript почти равны 0.

Выбираем фреймворк


Второй интригующий вопрос — как именно связать наш код и html-страничку. Вариантов реально много. Есть куча огромных фреймворков, которые на самом деле выглядят довольно целостно. Я сам какое-то время пользовался angularjs, но после некоторой практики стали видны очевидные минусы: для того чтобы сделать что-либо, нужны директивы, если их нет — надо писать свои велосипеды, а разобраться во внутреннем устройстве ангуляра сложнее чем кажется, также если посмотреть откровенно — самая часто используемая директива ng-model обеспечивает двустороннее связывание данных и представления, что с точки зрения функиональщика совершенно идеоматически неверно и вообще нарушает инкапсуляцию, кроме того все эти ангуляровские приложения-котроллеры итд итп довольно сильно утяжеляют код. Да, и кстати говоря производительность ангуляра реально так себе. Некоторое время назад я познакомился с react js — и мой выбор пал не него. Идея подкупает своей простотой и более-менее функционально-декларативным стилем. Есть state, который в процессе работы приложения может как-то меняться. Мы просто время от времени передаём его в jreact для отрисовки.

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)


И всё! До безумия просто и эффективно. Об оптимизации отрисовки заботится сам react, мы теперь можем вообще не интересоваться этим вопросом. Остаётся обеспечить изменения объекта state таким образом чтобы это соответствовало функциональной парадигме. И тут начинается самое интересное.

Trouble in the Kitchen


Первая и не очень значительная проблема — мягкие типы js. В Erlang они конечно тоже мягкие, но в js они рельно мягкие как, извините за выражение, говно. Вот например не очень содержательное, но довольно смешное видео на эту тему. Но на практике преобразования типов случаются нечасто (во всяком случае если ваш код хорош) — так что мягкие типы js я принял более-менее как они есть.

Когда я начал понемногу практиковаться в js сперва всё было более-менее хорошо, но в какой-то момент приложения почему-то начинали работать совсем не так как я хотел. Я полез глубже и увидел страшное:

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 } ]


хотя в этом случае я конечно ожидал увидеть

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


Это реально был шок. Бинго, данные в js мутабельны! Причём как оказалось — всё что сложнее чем number, string, null и undefined будет передаваться по ссылке!

Но когда я увидел что

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


то волосы у меня зашевелились в самых разных местах. К такому жизнь меня не готовила. Оказывается типы данных maps и lists в js сравниваются не по значению, а тоже по ссылке.

Я стал думать как быть. Касаемо мутабельности например было решено оборачивать константные данные (напрмиер для инициализации каких-либо значений) в лямбда-функции арности ноль, в таких случаях они действительно оставались в общем-то не мутабельными. Можно просто вызывать их в тех выражениях где они нужны и не бояться что они (данные) при этом изменятся.

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 ]


В принципе, подумалось мне, если рекурсивно оборачивть лямбдами вообще все даннные, если все функции будут принимать лямбды и возвращать лямбды — язык станет действительно функциональным! В принципе — это решение. Нужно просто описать эти лямбда-типы данных на основе обычных типов, написать функции для рекурсивного прямого и обратного преобразования в обычные js типы, а также функции высшего порядка для работы с этими лямбда-типами (map, reduce, filter, zip итд итп). Заодно кстати можно и сделать эти новые типы менее мягкими. Задача в принципе решаемая, но довольно объёмная, отчасти кстати уже реализованная например вот в этой библиотеке. Но такой подход имеет довольно существенные недостатки:

1) Так как наш код как правило не подвешен в воздухе, а имеет зависисимости от других js-библиотек, всякий раз обращаясь к ним надо не забыть сделать преобразование лямбда-типа в обычный тип, и соответсвенно наоборот.
2) Этим подходом мы конечно в какой-то степени обеспечим чистоту функций и иммутабельность данных, но транзакционности всё равно не будет
3) Такой код будет не очень понятен тем кто предпочитает императивный подход

Таким образом от этой идеи я пока отказался (но стоит ей уделить внимание в будущем) и решил сделать что-то менее радикальное, но такое же простое и понятное. Очевидно чтобы локально решить проблему мутабельности и сравнения данных по ссылке мне нужно было научиться рекурсивно копировать и сравнивать по значению любые js-данные.

Пишем функцию clone
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), {}


Пишем функцию equal
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


Оказалось проще чем я думал, единственное «но» — если в данных будут циклические ссылки, конечно получим stack overflow. Для меня это в принципе не проблема, так как я не использую такие абстракции как «циклические ссылки». Мне думается можно как-то и их обрабатывать в процессе клонирования данных, но тогда конечно код не будет таким простым и изящным. В общем я собрал эти и некоторые другие функции в библиотеку и считаю что проблема мутабельности данных в js на какое-то время для меня решена.

Акторы


Теперь поговорим зачем нам транзакционность изенения данных. Допустим есть какой-то более-менее сложный state и мы его меняем в процессе выполнения какой-либо функции.
state.aaa = 20
state.foo = 100
state.bar = state.bar.map (el) -> baz(el, state)


На практике процесс изменения state конечно может быть более сложным и длинным, содержать асинхронные вызовы к внешним api итд итп. Но суть в том что если где-то в процессе изменения state где-то в другом месте вызовется функция func(state) — что произойдёт? Будет ли наполовину изменённый state при этом валидным? А может в силу однопоточности js полу-изменённого state вообще не будет существовать и всё нормально? А если нет? А что делать если мне надо сделать какие-то внешние вызовы и жизненно важно чтобы пока я их делаю state на менялся? Чтобы не ломать голову такими непростыми вопросами сделаем изменения state транзакционными.

Тут думаю многие вспомят о мьютексах. Я тоже вспомнил о мьютексах. И о состояниях гонки. И о дедлоках. И понял что я хочу совсем не этого, поэтому будем писать не мьютекс, а позаимствуем из языка Erlang понятие «актор». В контексте js актор будет просто неким объектом который инициализируется определённым state, и будет делать всего три вещи

1) принимать «сообщения» в виде функций арности 1 или 0 и добавлять их в очередь
2) самостоятельно «разгребать» очередь сообщений, применяя функции арности 1 к своему внутреннему state (функции арности 0 просто вызываются, state не меняется) — всё это делается строго в том порядке в каком были получены сообщения.
3) по требованию вернуть значение своего внутреннего state

Естественно для соблюдения не-мутабельности данных будем каждый раз клонировать state при изменении, и в функции get будем возвращать не сам state, а его копию. Для этого будем использовать библиотеку, написанную ранее. В итоге получим код.

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

* Функции которые кладут лямбду в очередь называются cast и zcast по аналогии с эрланговскими функциями handle_cast

Поскольку не все js-данные клонируемы (вспомним о циклических ссылках и внешних библиотеках), сделаем ещё один вариант конструктора для актора в котором уберём клонирование внутреннего state и соберём всё это дело в библиотеку.

Наслаждаемся:
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' }


Все изменения state осуществляем исключительно через очередь посредствам функции cast. Как мы можем видеть в «чистом» варианте state полностью инкапсулирован внутри актора, что бы мы ни делали с ним после получения из функции get (то же самое верно если мы добавляем что-то из внешнего мира в функции cast). Транзакционность обеспечивается очередью сообщений. Мы получили практически эрланговский код, только на js. Если мы хотим использовать в своём state по какой-то причине не-клонируемые данные, то будем просто использовать «грязный» вариант актора с глобальным state. В принципе даже такой вариант (если менять state строго через актор) приемлим и обеспечивает транзакционность изменения данных. Была ещё мысль сделать изменения данных не просто транзакционными, а в каком-то смысле даже атомарными, передавая не одну лямбду, а три (например на случай каких-то эксепшнов во внешних библиотеках).

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


Но я подумал что вот это уже перегиб, тем более что в нынешнем варианте можно просто писать
actor.cast((state) ->
	args = init_args(state)
	try
		do_work(state, args)
	catch error
		rollback(state, args, error))

если вдруг атомарность так жизненно необходима.

Вывод


Js оказался в итоге не так безнадёжен как мне показалось в начале. За его лямбда-функциями скрывается настоящая функциональная мощь и при должном уровне сноровки на нём можно писать в довольно-таки декларативном стиле. И на закуску простой пример с использованием акторов + react + jade + sass + bullet (эрланговские вебсокеты). Stay functional, stay web!

UPD


Жители хабра указали на ряд грубых ошибок в моём коде, спсибо им за это :) Подправил недочёты, а именно:

1) Заменил в функции clone биндинг на более адекватную и очевидную операцию клонирования функции
2) В функции equal добавил в начале довольно очевидную проверку которая должна повысить производительность :)
3) Глупую проверку функций по значению func.toString() я убрал.
4) При сравнении массивов больше не использую возможно переопределяемое поле length, а просто считаю элементы
5) При сранении объектов не использую функцию «Object.keys» а просто собираю все ключи из объекта. В каком-то смысле код стал так даже стройнее и функциональнее.

Ещё раз спасибо всем тем кто способствовал тому что этот код стал лучше. Актуальные версии библиотек можно найти тут и тут. С удовольствием выслушаю ваши пожелания и предложения.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 31

    +4
    Я правильно понимаю, что вы умышленно форсите рендер через каждые 500мс?
    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)
    

    Реакт сам исходя из состояния компонента (state) определяет, нужно ли ре-рендериться, вам не нужно форсить ре-рендер. Всё, что вам нужно — лишь описать зависимость конечного dom дерева компонента от его state. При изменении state (вызов this.setState) реакт будет смотреть, поменялось ли конечное dom дерево и, если поменялось, то найдёт минимально необходимые изменения в dom и произведёт их.
      +6
      Код далеко не оптимален, более того, некорректен.
      if type_a == type_b
              switch type_a
                  when "[object Undefined]" then a == b #можно сразу возвращать true
                  when "[object Boolean]" then a == b 
                  when "[object Number]" then a == b # а вы точно хотите на +0 == -0 получать true? это не сарказм, это кривая система неточного равенства.
                  when "[object String]" then a == b
                  when "[object Function]" then a.toString() == b.toString() #не учитывается возможность существования функторов; не учитывается возможность оверлоада .toString, не учитываются нативные и binded-функции, которые возвращают function () { [native code] }
                  when "[object Null]" then a == b #можно сразу возвращать true
                  when "[object Array]" #не сравниваете свойства у массива. Да, в JS массив тоже объект и может иметь свойства.
                      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() #не учитывается возможность существования defined Properties, сравнение прототипов
                      keys_b = Object.keys(b).sort()
                      if equal(keys_a, keys_b)
                          keys_a.every (k) -> equal(a[k], b[k])
                      else
                          false
      


      Не пишите на «чисто функциональном» стиле на JS. Он не предназначен для этого, вы не сможете получить именно то, чего хотите, без иммутабельности.
        0
        Дело в том что я в своём коде не использую какие-то сложные, неоднозначные и уж тем более ООП стороны js.

        а вы точно хотите на +0 == -0 получать true?
        да :)

        В моём понимании [head, tail...] — это просто список, а не объект и я никогда не переопределю для него поле length, в моём понимании length тут вообще не поле а метод (хотя судя по тому как он «вызывается» — это не так)

        {key: value} — для меня не объект а просто map, хэш-таблица с парами key — value где key и value — любые js-данные, и поэтому я никогда не буду что-то там делать с прототипами

        Насчёт when "[object Function]" then a.toString() == b.toString() — реально очень скользкий момент, но я увы так и не смог придумать как ещё можно сравнить две функции по значению. Разыменовывания указателя в js я так и не нашёл, это решило бы все подобные проблемы :(

        a == b для примитивов у меня написано просто для красоты и однородности :) Да и мало ли что, просто изучать детально и глубоко реализацию всех типов данных в js не было ни времени ни сил ни желания
          0
          >> а вы точно хотите на +0 == -0 получать true?
          > да :)
          Это ваше дело, я просто уточнил, как я и писал. Кстати, == по перфомансу еще и медленнее чем ===

          > В моём понимании [head, tail...] — это просто список, а не объект и я никогда не переопределю для него поле length, в моём понимании length тут вообще не поле а метод (хотя судя по тому как он «вызывается» — это не так)
          > {key: value} — для меня не объект а просто map, хэш-таблица с парами key — value где key и value — любые js-данные, и поэтому я никогда не буду что-то там делать с прототипами

          Вы же планируете работать c DOM API? или Node.js APIs? Или вы делаете чистые функции на JS просто так? Все куда более неоднозначно, чем кажется.
            0
            Кстати насчёт == || === и прочего — в coffeescript многие подобные скользкие моменты решены, например == всегда разворачивается в ===, слова return нет — всегда возвращается результат последнего выражения в функции итп

            Насчёт работы с реальным продакшном у меня сложилась поределённая схема, вот пример:

            1) Вот html-страничка, нас интересует вот этот блок который собственно будет отрисовываться в jreact, там будет происходить весь экшн

            github.com/timCF/megaweb/blob/c90ede0e0c48373712695fb5a3e3b77247c6f6a7/app/index.jade#L12

            2) Вот сам виджет который будет рисоваться пользователю в зависимости от состояния state

            github.com/timCF/megaweb/blob/c90ede0e0c48373712695fb5a3e3b77247c6f6a7/app/widget.jreact

            3) Когда DOM первый раз построился — мы врубаем jreact через актор

            github.com/timCF/megaweb/blob/c90ede0e0c48373712695fb5a3e3b77247c6f6a7/app/scripts/app.coffee#L88-91

            4) А дальше уже всё в абсолютно свободном плавании — коллбэки висящие на кнопках, что-то летящее с сервера в вебсокет — всё это отправляет сообщения актору

            github.com/timCF/megaweb/blob/c90ede0e0c48373712695fb5a3e3b77247c6f6a7/app/scripts/app.coffee#L51

            который строго в порядке очереди их вызывает, обновляя свой внутренний state

            5) Периодически опрашиваем актор — получаем копию его текущего state и передаём в react который рисует пользователю страничку

            github.com/timCF/megaweb/blob/c90ede0e0c48373712695fb5a3e3b77247c6f6a7/app/scripts/app.coffee#L68
              +1
              Да, про == забыл.

              Про
              > Периодически опрашиваем актор — получаем копию его текущего state и передаём в react который рисует пользователю страничку
              Не кажется ли, что реактивный подход был бы лучше?
                0
                скорее всего глобальный state + react будет быстрее, но конечно чистота тут уже нарушится :)
                  0
                  Сейчас использую глобальный state с чистыми функциями. Получается довольно интересно. Использую github.com/Yomguithereal/baobab.

                  Схема работы у меня такая:
                  В корневом виджете создается глобальный стейт (может получаться с помощью DI). Далее мы триггерим action в componentWillMount. Этот action занимается тем, что получает нужные данные с сервера асинхронно и сетит начальный стейт на основе текущих props. Actions представляют собой объект, который тупо собирает несколько функций, пример:
                  function setField(value {
                     state.set(['path_to_field'], value)
                  }
                  
                  export default {
                    setField
                  }
                  
                    0
                    Почему чистота нарушится? Глобальный state — это очень даже чисто. Разумеется, тут понадобятся дополнительные абстракции для работы с ним. Линзы, к примеру.
              0
              Две функции прекрасно сравниваются оператором ===, если очень надо. Все остальные способы будут врать на простейших замыканиях.

              Кстати, в функциональных языках функции вообще нельзя сравнивать!
                0
                смотря в каких языках :)
                в эрланге например можно

                насчёт js — нет, в лоб это не работает

                coffee> f1 = () -> 123
                [Function]
                coffee> f2 = () -> 123
                [Function]
                coffee> f1 == f2
                false
                coffee> f1.toString() == f2.toString()
                true

                можно конечно использовать toString, но как было верно подмечено это костыль :)
                  +2
                  У вас f1 и f2 — это две совершенно разные функции. То, что они выдают одинаковые значения на всей области определения — это просто случайность.
                    +2
                    Вот вам загадка:

                    ff = x -> () -> x;
                    f1 = ff 1
                    f2 = ff 2
                    
                    console.log f1.toString() == f2.toString() // true
                    


                    Но являются ли функции одинаковыми?
                      0
                      Гм, а в каком месте это противоречит предыдущему оратору?
                        +2
                        В том, что вызов .toString() для сравнения — это не просто костыль, а грубая ошибка.
                          0
                          С этим не спорю, просто из вашего предыдущего комментария этого не следует. Ваш пример по функционалу идентичен примеру предыдущего комментатора.
                            0
                            я не хочу холиваров на хабре и вообще хочу конструктивного диалога :)
                            то что в начальном тексте у меня ошибка это ясно, глупо клонировать функцию вот так

                            when "[object Function]" then some.bind({})

                            а потом пытаться сравнивать вот так

                            when "[object Function]" then a.toString() == b.toString()

                            Но вот я поковырялся с интерпретатором, и в итоге прикрепляю скриншот того что получилось. Я вижу что функции тут — клонируемые по значению абстракции, и это парадоксально но похоже что сравниваются они тоже по значению. Прикрепляю скриншот чтобы ты не подумал что я жульничаю и набираю это всё в текстовом редакторе :)

                            Похоже всё работает как надо, но конечно если придерживаться определённых правил.

                              0
                              точнее даже вот так. я думаю идея понятна :)
                              правда не знаю можно ли из этого извлечь что-то для моей задачи, подумаю над этим на досуге

                                0
                                Извиняюсь, но у вас каша в голове. Разумеется, если вы делаете присваивание f1 = f2, то они почему-то оказываются одинаковыми…
                                  0
                                  но почему тогда вот тут

                                  habrastorage.org/files/91d/83a/ad4/91d83aad404b4e6d8d19082e95e98c33.png

                                  после строчки

                                  f1 = () -> 123

                                  f2 тоже не изменилась, если при присваивании функций данные передаются по указателю? Если в таком случае после второй строчки

                                  f2 = new () -> f1

                                  память всё же выделяется под новую функцию — то тогда почему в третьей строчке они равны?
                                    0
                                    Никаких указателей на переменную — присваивание всегда происходит по значению.

                                    PS вы правда не видите, что new () -> f1 — это то же самое, что и просто f1?
                                      0
                                      «Никаких указателей на переменную — присваивание всегда происходит по значению.»

                                      Это конечно прекрасно :)

                                      Но похоже что это не работает для списков и структур

                                        0
                                        В данном случае, само значение является ссылкой. А указателей на переменную как не было, так и нет:

                                        a = {k : 1}
                                        b = a
                                        console.log b // {k : 1}
                                        
                                        a = {k : 2}
                                        console.log b // все еще {k : 1}
                                        
                                          0
                                          Ясно, спасибо
                                          Видимо реально проще использовать fay или purescript :)
                    +1
                    +0 === -0 даже с точным равенством и тут ничего кривого нет, как и в том, что 1/(+0) !== 1/(-0) или NaN !== NaN.
                    0
                    FortranErlang-программист напишет FortranErlang-программу на любом языке программирования? :)
                      0
                      В принципе да :) Такой подход имеет ряд плюсов

                      1) Однородность данных: сервер кидает клиенту через вебсокет сообщения которые содержат какие-то данные. Сервер написан на эрланге. Там нет никаких объектов — есть binaries, integers, floats, lists, maps. Именно в таком виде данные приходят клиенту. Если мы продолжим работать с этими данными именно как с данными а не как с объектами — это здорово снизит накладные расходы на написание всяческого рода классов, конструкторов, и прочих адаптеров erlang_data <=> js_data.

                      То же самое правило я кстати использую и по отношению в взаимодействию сервер <=> бд. То есть называю поля в эрланговских структурах в точности именами столбцов бд. Это реально здорово снижает накладные расходы, количество кода, увеличивает читаемость и прозрачность.

                      2) Мне гораздо проще читать / писать код, если он написан в парадигме моего основного языка.
                        0
                        Это все уже сделано. Называется N2O.
                        Есть даже компилятор из эрланга в JavaScript SHEN.
                    • UFO just landed and posted this here
                        +1
                          +1
                          Спасибо вам за статью. Должен с вами не согласится по поводу выбора coffeescript'а. С виду данный язык хорош но вот когда начинаешь использовать его на настоящих проектах тут и ползут непонятки, Поль там поставил пробел, Жак здесь забыл запятую и пошло поехало. Мне кажется с такими неясностями в дизайне кофий скриптовский на роль функционального языка ну никак не тянет, его можно попрововать притянуть или натянуть но из этого ничего путного не выйдет. Кстати вы не упомятули о FunScript и Scala.js которые не просто подмоножества но они являются настоящими закоренелыми функциональными языками транслируеммыми в js.

                          Only users with full accounts can post comments. Log in, please.