Почему в эликсире есть два вида функций?


279

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

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

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

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

Ответы:


386

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

Начнем со второго 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

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

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

other_function(&hello(&1))

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

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

&hello/1

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

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

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


30
Я также изучаю эликсир, и это первая проблема, с которой я столкнулся, которая заставила меня задуматься, что-то в этом казалось просто непоследовательным. Отличное объяснение, но, чтобы быть ясным… это результат проблемы реализации, или это отражает более глубокую мудрость относительно использования и передачи функций? Поскольку анонимные функции могут сопоставляться на основе значений аргументов, кажется, что было бы полезно иметь возможность сопоставления и по числу аргументов (и в соответствии с сопоставлением с шаблоном функции в другом месте).
Циклон

17
Это не ограничение реализации в том смысле, что оно также может работать f()(без точки).
Хосе Валим

6
Вы можете сопоставить количество аргументов, используя is_function/2в гвардии. is_function(f, 2)проверяет, имеет ли он 2.
Хосе Валим

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

28
В Erlang анонимные вызовы функций и обычные функции уже синтаксически различаются: SomeFun()и some_fun(). В Elixir, если мы удалим точку, они будут одинаковыми some_fun()и some_fun()потому что переменные используют тот же идентификатор, что и имена функций. Отсюда и точка.
Хосе Валим

17

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

Все в эликсире является выражением. Так

MyModule.my_function(foo) 

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

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

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

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

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


2
Понятно, что MyModule.my_function(foo)это выражение, но MyModule.my_function«могло» быть выражением, которое возвращает функцию «объект». Но так как вам нужно рассказать об искусстве, вам нужно что-то вроде MyModule.my_function/1этого. И я думаю, что они решили, что &MyModule.my_function(&1) вместо этого лучше использовать синтаксис, который позволяет выражать арность (и служит другим целям). ().()
До

Я думаю, вы хотите: &MyModule.my_function/1вот как вы можете передать это как функцию.
января

14

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

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

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

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

Для того, чтобы:

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?

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


12

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

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)

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

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

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

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

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

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

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


9

Об этом поведении есть отличный пост в блоге: ссылка

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

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

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).

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


Я не уверен в полезности прямого ответа на вопрос OP, но предоставленная ссылка (включая раздел комментариев) на самом деле является отличным ИМО для чтения для целевой аудитории вопроса, то есть для тех из нас, кто плохо знаком с Elixir + Erlang ,

Ссылка сейчас мертва :(
AnilRedshift

2

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

defmodule Insanity do
  def dive(), do: fn() -> 1 end
end

Insanity.dive
# #Function<0.16121902/0 in Insanity.dive/0>

Insanity.dive() 
# #Function<0.16121902/0 in Insanity.dive/0>

Insanity.dive.()
# 1

Insanity.dive().()
# 1

Не делая различий между двумя типами функций, мы не можем сказать, что Insanity.diveозначает: получить саму функцию, вызвать ее или также вызвать полученную анонимную функцию.


1

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

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

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


0

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

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

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.