Почему в Elixir существуют два вида функций?

Я изучаю Elixir и задаюсь вопросом, почему он имеет два типа определений функций:

  • определенные в модуле с def, вызываемые с помощью myfunction(param1, param2)
  • анонимные функции, определенные с помощью fn, вызываемые с помощью myfn.(param1, param2)

Только второй тип функции кажется первоклассным объектом и может передаваться как параметр другим функциям. Функция, определенная в модуле, должна быть обернута в fn. Там какой-то синтаксический сахар, который выглядит как otherfunction(myfunction(&1, &2)), чтобы сделать это легко, но почему это необходимо в первую очередь? Почему мы не можем просто сделать otherfunction(myfunction))? Нужно ли разрешать вызывать функции модуля без скобок, как в Ruby? Кажется, он унаследовал эту характеристику от Erlang, которая также имеет модульные функции и развлечения, так же как это происходит на самом деле от того, как Erlang VM работает внутри?

Есть ли какое-то преимущество, имеющее два типа функций и преобразование из одного типа в другой, чтобы передать их другим функциям? Есть ли преимущество с двумя различными обозначениями для вызова функций?

Ответ 1

Просто чтобы уточнить наименование, они обе функции. Одна - именованная функция, а другая - анонимная. Но вы правы, они работают несколько иначе, и я собираюсь проиллюстрировать, почему они работают так.

Давайте начнем со второго, fn. fn - это замыкание, похожее на lambda в Ruby. Мы можем создать его следующим образом:

x = 1
fun = fn y -> x + y end
fun.(2) #=> 3

Функция также может иметь несколько предложений:

x = 1
fun = fn
  y when y < 0 -> x - y
  y -> x + y
end
fun.(2) #=> 3
fun.(-2) #=> 3

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

fn
  x, y -> x + y
  x -> x
end
** (SyntaxError) cannot mix clauses with different arities in function definition

о нет! Мы получаем ошибку! Мы не можем смешивать предложения, которые ожидают различное количество аргументов. Функция всегда имеет фиксированную арность.

Теперь поговорим о названных функциях:

def hello(x, y) do
  x + y
end

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

x = 1
def hello(y) do
  x + y
end

Этот код не удастся скомпилировать, потому что каждый раз, когда вы видите def, вы получаете пустую переменную scope. Это важное различие между ними. Мне особенно нравится тот факт, что каждая именованная функция начинается с чистого листа, и вы не смешиваете переменные разных областей. У вас есть четкая граница.

Мы могли бы получить указанную выше функцию hello как анонимную функцию. Вы упомянули это сами:

other_function(&hello(&1))

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

Начиная с Elixir v0.10.1, у нас есть синтаксис для захвата именованных функций:

&hello/1

Это захватит локальную именованную функцию hello с arity 1. Во всем языке и его документации очень часто идентифицируются функции в этом синтаксисе hello/1.

Именно поэтому Elixir использует точку для вызова анонимных функций. Поскольку вы не можете просто передать hello как функцию, вместо этого вам нужно явно захватить ее, есть естественное различие между именованными и анонимными функциями, и отдельный синтаксис для вызова каждой делает все немного более явным (Lispерс был бы знаком с это связано с обсуждением Lisp 1 и Lisp 2).

В целом, это причины, по которым у нас есть две функции и почему они ведут себя по-разному.

Ответ 2

Я не знаю, насколько это полезно для кого-то другого, но способ, которым я, наконец, обернулся вокруг концепции, заключался в том, чтобы понять, что функции эликсира не являются функциями.

Все в эликсире - это выражение. Итак,

MyModule.my_function(foo) 

- это не функция, а выражение, возвращаемое при выполнении кода в my_function. На самом деле существует только один способ получить "функцию", которую вы можете передать как аргумент, и использовать анонимную функцию.

Заманчиво ссылаться на fn и/или нотацию как на указатель функции, но на самом деле это намного больше. Это закрытие окружающей среды.

Если вы спросите себя:

Мне нужна среда выполнения или значение данных в этом месте?

И если вам нужно выполнить использование fn, то большая часть трудностей станет много яснее.

Ответ 3

Я понимаю объяснение, почему требуется & f/1, хотя было бы неплохо иметь способ сказать "все функции f любой арности", а остальные - под крышками.

Даже если параметр & f/1 необходим для ссылки на саму функцию, я не понимаю, почему также необходимо использовать вариацию ". (args)" для вызова этой функции. Это из-за потенциального затенения имени или по другой причине?

Это то, что нужно будет многократно объяснять в течение жизни Эликсира.

http://joearms.github.io/2013/05/31/a-week-with-elixir.html

Ответ 4

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

Arity, очевидно, задействован, но откладывает его на некоторое время и использует функции без аргументов. На языке, таком как javascript, где скобки являются обязательными, легко сделать разницу между передачей функции в качестве аргумента и вызовом функции. Вы вызываете его только при использовании скобок.

my_function // argument
(function() {}) // argument

my_function() // function is called
(function() {})() // function is called

Как вы можете видеть, называть это или нет, не имеет большого значения. Но эликсир и рубин позволяют вам вызывать функции без скобок. Это выбор дизайна, который мне лично нравится, но у него есть этот побочный эффект, который вы не можете использовать только имя без скобок, потому что это может означать, что вы хотите вызвать функцию. Для этого используется &. Если вы оставите arity appart на секунду, добавление имени вашей функции с помощью & означает, что вы явно хотите использовать эту функцию в качестве аргумента, а не то, что возвращает эта функция.

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

Теперь последняя проблема заключается в том, что иногда вы должны называть их в своем коде, потому что они не всегда используются с функцией итератора, или вы можете сами кодировать итератор. Для маленькой истории, поскольку ruby ​​объектно ориентирован, основной способ сделать это - использовать метод call для объекта. Таким образом, вы можете поддерживать неизменное поведение скобок.

my_lambda.call
my_lambda.call()
my_lambda_with_arguments.call :h2g2, 42
my_lambda_with_arguments.call(:h2g2, 42)

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

my_lambda.()
my_lambda_with_arguments.(:h2g2, 42)

Опять же, это выбор дизайна. Теперь эликсир не является объектно-ориентированным и поэтому не обязательно должен использовать первую форму. Я не могу говорить для José, но похоже, что вторая форма использовалась в эликсире, потому что она по-прежнему выглядит как вызов функции с дополнительным символом. Это достаточно близко к вызову функции.

Я не думал обо всех плюсах и минусах, но похоже, что на обоих языках вы можете уйти с помощью только скобок, пока вы делаете скобки обязательными для анонимных функций. Кажется, что это:

Обязательные скобки VS Немного разные обозначения

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

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

И в эликсире это просто лишняя точка, тогда как в рубине у вас есть блоки поверх этого. Блоки поразительны, и я удивлен, сколько вы можете сделать только с блоками, но они работают только тогда, когда вам нужна только одна анонимная функция, которая является последним аргументом. Затем, поскольку вы должны иметь дело с другими сценариями, здесь возникает весь метод /lambda/proc/block confusion.

В любом случае... это выходит за рамки.

Ответ 5

Я никогда не понимал, почему объяснения этого настолько сложны.

Это действительно просто исключительное маленькое различие в сочетании с реалиями исполнения функций Ruby в стиле без парнеров.

Для сравнения:

def fun1(x, y) do
  x + y
end

To:

fun2 = fn
  x, y -> x + y
end

Хотя оба они являются только идентификаторами...

  • fun1 - это идентификатор, который описывает именованную функцию, определенную с помощью def.
  • fun2 - это идентификатор, описывающий переменную (которая содержит ссылку на функцию).

Считайте, что это означает, когда вы видите fun1 или fun2 в каком-то другом выражении? Когда вы оцениваете это выражение, вызываете ли вы ссылочную функцию или просто ссылаетесь на значение из памяти?

Нет хорошего способа узнать во время компиляции. Ruby обладает роскошью для интроспекции пространства имен переменных, чтобы выяснить, не зацепило ли переменную привязку функцию в какой-то момент времени. Эликсир, будучи скомпилированным, не может этого сделать. То, что делает точка-нотация, сообщает Elixir, что она должна содержать ссылку на функцию и что она должна быть вызвана.

И это действительно сложно. Представьте, что не было точечной нотации. Рассмотрим этот код:

val = 5

if :rand.uniform < 0.5 do
  val = fn -> 5 end
end

IO.puts val     # Does this work?
IO.puts val.()  # Or maybe this?

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

Ответ 6

Там отличное сообщение в блоге об этом поведении: ссылка

Два типа функций

Если модуль содержит это:

fac(0) when N > 0 -> 1;
fac(N)            -> N* fac(N-1).

Вы не можете просто вырезать и вставить это в оболочку и получить тот же результат.

Это потому, что в Erlang есть ошибка. Модули в Erlang - это последовательности ФОРМ. Оболочка Эрланга оценивает последовательность ВЫРАЖЕНИЙ. В Erlang FORMS нет ВЫРАЖЕНИЙ.

double(X) -> 2*X.            in an Erlang module is a FORM

Double = fun(X) -> 2*X end.  in the shell is an EXPRESSION

Два не одинаковы. Эта глупость была Эрлангом навсегда, но мы не заметили этого и научились жить с этим.

Точка в вызове fn

iex> f = fn(x) -> 2 * x end
#Function<erl_eval.6.17052888>
iex> f.(10)
20

В школе я научился вызывать функции, написав f (10), а не f. (10) - это "действительно" функция с именем, подобным Shell.f(10) (это функция, определенная в оболочке). Часть оболочки неявный, поэтому он должен быть назван просто f (10).

Если вы оставите это так, ожидайте, что проведете следующие двадцать лет своей жизни, объясняя, почему.

Ответ 7

Только второй тип функции кажется первоклассным объектом и может передаваться как параметр другим функциям. Функция, определенная в модуле, должна быть завернута в fn. Там какой-то синтаксический сахар, который выглядит как otherfunction(myfunction(&1, &2)), чтобы сделать это легко, но почему это необходимо в первую очередь? Почему мы не можем просто сделать otherfunction(myfunction))?

Вы можете сделать otherfunction(&myfunction/2)

Так как elixir может выполнять функции без скобок (например, myfunction), используя otherfunction(myfunction)), он попытается выполнить myfunction/0.

Итак, вам нужно использовать оператор захвата и указать функцию, включая arity, так как вы можете иметь разные функции с тем же именем. Таким образом, &myfunction/2.

Ответ 8

Синтаксис fn -> предназначен для использования анонимных функций. Выполнение var.() Просто говорит эликсиру, что я хочу, чтобы вы взяли этот var с функцией в нем и запустили вместо того, чтобы ссылаться на var как на нечто, просто удерживающее эту функцию.

У Elixir есть такая общая схема, где вместо логики внутри функции, чтобы увидеть, как что-то должно выполняться, мы сопоставляем различные функции в зависимости от того, какой у нас ввод. Я предполагаю, что именно поэтому мы называем вещи арностью в смысле имя_функции function_name/1.

Довольно странно привыкать к сокращенным определениям функций (func (& 1) и т.д.), Но удобно, когда вы пытаетесь передать или сохранить ваш код лаконичным.