Pull to refresh

Comments 19

Я вижу, Вы неплохо знакомы с эликсиром и наверное сможете ответить на мой вопрос.
После беглого изучения меня в ступор ввёл один факт, почему в стандартно библиотеке все вызовы функций привели к стандартному виду, что объект изменения (список, рекорд или другой контейнер) находится на первом месте? Это же очевидно неудобно при написании кода, и обычно делают наоборот.
Например, я совершенно не понимаю как писать List.foldl([5,5], 10, fn (x, acc) -> x + acc end)
На мой взгляд, удобнее как раз использовать первый аргумент для субъекта, так как:
1) Если использовать последний аргумент, то это может вводить в замешательство, особенно при использовании с необязательными аргументами. Или, например в таких случаях, как в эрланге: element(N, Tuple) и setelement(Index, Tuple1, Value), где в первой функции субъект идёт последним аргументом, а во второй идёт по середине(?!).
2) В библиотеках при работе с файлами, соединениями часто субъект идёт первым аргументом. Например, мы не пишем:
file:open(Options, Filename) (в эрланге)
Хотя здесь и очевидно, что имя файла является субъектом.
2) И на мой взгляд, удобнее писать, когда функция идёт последним аргументом(хоть и не привычно), особенно когда тело функции является более длинным, чем половина строки(что очень часто встречается), пример:
:lists.foldl(fn({a, b}, acc) ->
     c = case a > b do
              true -> a * 2
              false -> b * 3
      end
      acc + c + 1
end, 0, [{1,2},{3, 4}])

Если посмотреть на функцию, то неудобно, что между самой функцией, названием и аргументами более 5 строк, а тот же самый вариант с эликсировской стандартной библиотекой выглядит так:
List.foldl([{1, 2}, {3, 4}], 0, fn({a, b}, acc) ->
   c = case a > b do
             true -> a * 2
             false -> b * 3
   end
   acc + c + 1
end)

Мы видим в одной строчке название функции и аргументы в одной строке.

Ваш пример можно написать так:
List.foldl([5,5], 10, fn (x, acc) -> 
   x + acc 
end)

Или так с использованием partials:
List.foldl([5,5], 10, &1 + &2) 

На ваш вопрос дает лаконичный ответ создатель языка:

A couple notes:

1) The majority of erlang modules (array, binary, re, gen_*, file, diagraph, ets, etc) actually expect the subject as first argument. lists, *dict and string are the exception and not the rule. Since Elixir provides a better alternative for those (Enum which supports any Enumerable instead of lists, faster dicts, proper utf-8 strings, etc), you won't use the Erlang ones. So it is very likely that whatever module you use, in Erlang or Elixir, you will have the subject as first argument convention.

2) Leaving the subject as last argument may make optional arguments more confusing since the last argument position changes based on the arity. One similar example of this oddity is element and setelement in Erlang stdlib, where element writes like element(index, tuple) and setelement(index, tuple, value). So it is the last except when it isn't?

3) Having the subject as the first argument also works nicely with protocols which detect the target implementation using the first argument. Protocols would be *very* confusing if the target was based on the last argument.

4) Finally, I personally prefer the function to come as last argument for readability issues. The example below reads as «I want to map this collection by calling do_something on each item»:

Enum.map collection, fn(x) -> do_something(x) end

However, by changing the order, you get «I want to map by calling do something on each item this collection»:

Enum.map fn(x) -> do_something(x) end, collection

It feels unnatural imo. Also, the variable name may give good hints on what you are enumerating, which comes too late when it is the last argument.

Besides, having the function as last argument reads better for multiline functions:

Enum.map collection, fn(x) ->
do_something(x)
end

So if we are calling it a design mistake, I would say it was very premeditated one. :)
Ко всему вышесказанному, стандартный макрос |> передает результат как первый аргумент.
Ну ок, понятие удобности очень субъективно, не хочу тут спорить.
Думал, может есть ещё какие-нибудь причины.
Хочу лишь сказать, что большинстве функциональных языков (хаскель, окамль, лисп) объект изменения обычно идет последним аргументом и думаю, это не зря. Поэтому факт того, что в элексире сделали наоборот очень смутил и лично меня оттолкнул.
В функциональных языках это обеспечивает возможность point-free код. Композиция функций как базовая идея значительно выигрывает за счет того, что объект выполнения дейсвтия «откладывается на потом» (haskell):

sum = foldl + 0 


Попробуйте написать это если "+" должен идти на последнем месте. На мой взгляд, это серьезная ошибка в дизайне языка Elixir, которая значительно ограничивает возможности написания выразительного кода.
На мой взгляд, это не ошибка в дизайне, а наоборот правильное решение, по причинам описанным выше.

Я думаю сравнение строготипизированного haskell-я(и других функциональных языков строгой типизации) с языком с динамической типизации elixir-ом(построенным над BeamVM изначально с динамической типизацией) бессмысленно.

sum = foldl + 0

Такой код, и подобный стиль в elixir или erlang будет вести к множеству runtime ошибок, так как если в haskell компилятор будет всё проверять вовремя компиляции, то под BeamVM этого не делается. Строить строготипизированный язык поверх BeamVM тоже не самое благоразумное решение. Строгая типизация усложняет многие механизмы, заложенные в BeamVM, такие как hot code loading и многие другие. Скорее это уже будет новая VM(когда-нибудь).

Вторая проблема, это функции, с разным кол-вом аргументов, например:
def mysum(list, acc // 0) do 
   .... 
end

Создаёт две функции: mysum/1 и mysum/2 и тогда curring без указания arity функции не имеет смысла, т.е. так или иначе нужно указать аргументы, которые ожидает функция.
Тогда, в elixir-е тоже самое мы можем написать так:
sum = List.foldl(&1, 0, &1 + &2)
sum.([1,2,3,4,5])

Либо вот так, если curring идёт последним аргументом:
sum = :lists.foldl(&1 + &2 0, &1)
sum.([1,2,3,4,5])


И в данном случае, практически не имеет значения, будем ли делать curry первым или вторым способом.
А из-за причин указанных выше(стандартизации не только для структур данных, но и для много другого, как соединения, файлы, где тоже можно делать pipelining), то я думаю, что в случае elixir-а — это очень правильное решение, так как не теряя качеств в связи с особенностями BeamVM это даёт описанные выше преимущества.

В любом случае, не считаю поводом отказываться от эликсира, лишь потому что другие языки делают это по-другому, не пытаясь разобраться в деталях, почему. Возможно и я ошибаюсь в разнице между строготипизированными и динамическими функциональными языками в данном случае, если это так, то поправьте меня. Но если мои рассуждения верны, то это никак не ошибка в дизайне elixir.
консоли elixir-а можно создавать модули и кортежи
Кортежи? Может рекорды?

Enum.map(list, fn({a, x}) -> {a, x * 2} end)

dict = HashDict.New(list)

Enum.map(list, fn({a, x}) -> {a, x * 2} end)
Правильно понимаю, что в последней строчке не map(list, а map(dict?

А как определяется где искать реализацию протокола для данной структуры? По названию рекорда?
Насчет протоколов — реализовать то же самое на Erlang вполне можно без каких-либо хаков, просто не принято видимо.

Раздел «Everything is an expression» что-то не понял вообще.
Да, рекорды, только я не знал, как они правильно переводятся(при написании была путанница с переводом типов данных на русский язык). Наверное проще было везде оставить английские названия. Исправил строчку с Enum-ом.

defimpl — макро, которое генерирует модуль: . (т.е. в нашем случае beam файл: Elixir.Access.Tree.beam )
Если создать свой протокол, то можно создать имплементации для всех встроенных типов, например списков, кортежей, цифр и так далее. Реализовать протоколоподобную систему можно и в erlang-е, как и метапрограммирование, например с помощью parse transform. В elixir в принципе нет ничего того, что нельзя делать в erlang-е, вопрос в том, сколько усилий в конечном счёте — это занимает. И как показывает практика, никто этого не делает в elrang-е.

«Раздел «Everything is an expression» что-то не понял вообще.» =>
Я завтра дополню раздел, поподробнее прокоментирую каждый кусок кода и напишу, что происходит, и покажу возможности на чуть более интересном примере. Спасибо за комментарий, буду исправлять.
Дополнил раздел по метапрограммированию («Everything is an expression»), добавил комментарии и добавил ещё пример с регулярными выражениями. Сейчас, надеюсь, раздел более понятен?
Дмитрий, я давно ожидал свежую публикацию об Elixir на Хабре, так что огромное вам спасибо за нее!
Пожалуйста. Я бы мог ещё сделать туториал для начинающих или намного более подробный туториал по метапрограммированию в эликсире.
Это так здорово! Мне интереснее туториал для начинающих. Если получится сразу в видео формате, будет вообще замечательно :)
Дмитрий, краткое руководство для начинающих уже было опубликовано на днях:
learnxinyminutes.com/docs/elixir/

Вы какой-то принципиально другой туториал пишете в настоящий момент?
По мне, так руководство на официальном сайте (elixir-lang.com) за всё время было лучшим руководством по Elixir. Так что, для тех, кто читает по-английски ничего писать не надо. Я максимум думал над русской версией.
Спасибо, за информацию, исправил в статье.
Дмитрий, не поможете написать на Elixir код для классической задачи «Roman Numerals» без использования if-else, а то у меня пока получается как-то не особенно красиво:
gist.github.com/AlbertMoscow/5781821
?
Sign up to leave a comment.

Articles

Change theme settings