Чому в Elixir існують два види функцій?


279

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

  • функції, визначені в модулі з def, називаються за допомогоюmyfunction(param1, param2)
  • анонімні функції, визначені за допомогою fn, викликані за допомогоюmyfn.(param1, param2)

Тільки функція другого роду, здається, є об'єктом першого класу і може бути передана як параметр іншим функціям. Функцію, визначену в модулі, потрібно загорнути в a 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, ви отримуєте порожню область змінної. Це важлива різниця між ними. Мені особливо подобається те, що кожна названа функція починається з чистого діапазону, і ви не отримуєте змішаних змінних різних областей, змішаних разом. У вас чітка межа.

Ми могли б отримати названу привіт функцію вище як анонімну функцію. Ви самі це згадали:

other_function(&hello(&1))

І тоді ви запитали, чому я не можу просто передати це так, helloяк в інших мовах? Це тому, що функції в Elixir ідентифікуються за назвою та сутністю. Таким чином, функція, яка очікує двох аргументів, відрізняється від функції, яка очікує на три, навіть якщо вони мали однакову назву. Тож якби ми просто пройшли hello, ми б не мали уявлення, що helloти насправді маєш на увазі. Той, що має два, три чи чотири аргументи? Це точно та сама причина, чому ми не можемо створити анонімну функцію з пропозиціями з різними артуіами.

Оскільки Elixir v0.10.1, у нас є синтаксис для захоплення названих функцій:

&hello/1

Це буде охоплювати локально названу функцію привіт з артістю 1. У всій мові та її документації дуже часто зустрічаються функції ідентифікації цього hello/1синтаксису.

Ось чому Elixir використовує крапку для виклику анонімних функцій. Оскільки ви не можете просто перейти helloяк функцію, замість цього вам потрібно чітко її захопити, існує природне розмежування між іменованими та анонімними функціями, а чіткий синтаксис для виклику кожної робить все трохи більш явним (Лісперс був би знайомий з цим завдяки дискусії Lisp 1 проти Lisp 2).

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


30
Я також вивчаю Еліксир, і це перше питання, з яким я зіткнувся, що дав мені паузу, щось з цього приводу здавалося непослідовним. Чудове пояснення, але щоб було зрозуміло ... це результат питання щодо впровадження чи він відображає більш глибоку мудрість щодо використання та передачі функцій? Оскільки анонімні функції можуть збігатися на основі значень аргументів, здається, було б корисно мати можливість також відповідати кількості аргументів (і відповідати схемі функцій, що відповідають іншим параметрам).
Циклон

17
Це не обмеження в реалізації в тому сенсі, що воно може також працювати як f()(без крапки).
Жозе Валім

6
Можна порівняти кількість аргументів, використовуючи is_function/2захист. is_function(f, 2)перевіряє, що він має сутність 2. :)
Хосе Валім

20
Я б віддав перевагу викликам функції без крапок як для іменованої, так і для анонімної функції. Іноді це робить заплутаним, і ви забуваєте, чи певна функція була анонімною чи названою. Існує також більше шуму, коли справа доходить до currying.
CMCDragonkai

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

17

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

Все в еліксирі - це вираз. Так

MyModule.my_function(foo) 

не є функцією, але виразом, повернутим при виконанні коду в my_function. Насправді існує лише один спосіб отримати "функцію", яку ви можете передати як аргумент, і це використовувати позначення анонімної функції.

Заманливо називати fn або & notation як покажчик функції, але насправді це набагато більше. Це закриття навколишнього середовища.

Якщо ви запитаєте себе:

Чи потрібно мені середовище виконання або значення даних у цьому місці?

І якщо вам потрібно використовувати fn, то більшість труднощів стає набагато зрозумілішими.


2
Зрозуміло, що MyModule.my_function(foo)це вираз, але MyModule.my_function"міг" бути виразом, який повертає функцію "об'єкт". Але оскільки вам потрібно сказати арійство, вам знадобиться щось подібне MyModule.my_function/1. І я здогадуюсь, що вони вирішили, що краще використовувати &MyModule.my_function(&1) синтаксис, який дозволяє висловити суворість (і служить і для інших цілей). Досі незрозуміло, чому існує ()оператор для названих функцій та .()оператор для неназваних функцій
RubenLaguna

Я думаю, що ти хочеш: &MyModule.my_function/1ось як ти можеш передавати це як функцію.
jnmandal

14

Я ніколи не розумів, чому пояснення цього настільки складні.

Це справді лише винятково невелика відмінність у поєднанні з реаліями стилю Ruby "виконання функції без паронів".

Порівняйте:

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

До:

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

Хоча обидва це лише ідентифікатори ...

  • fun1це ідентифікатор, який описує іменовану функцію, визначену за допомогою def.
  • fun2 - це ідентифікатор, який описує змінну (яка містить посилання на функцію).

Поміркуйте, що це означає, коли ви бачите fun1чи fun2в якомусь іншому виразі? Оцінюючи цей вираз, ви викликаєте функцію, на яку посилаєтесь, чи ви просто посилаєтесь на значення, що не в пам'яті?

Немає хорошого способу дізнатися під час компіляції. У Ruby є розкіш самоаналізу простору імен змінної, щоб дізнатись, чи прив'язка змінної затіняє функцію в якийсь момент часу. Еліксир, будучи складеним, не може цього реально зробити. Ось що робить точкове позначення, воно говорить Еліксиру, що він повинен містити посилання на функцію і що його слід викликати.

І це справді важко. Уявіть, що не було точкових позначень. Розглянемо цей код:

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

Я можу помилятися, оскільки про це ніхто не згадував, але я також мав враження, що причиною цього є також рубінова спадщина того, що можна викликати функції без дужок.

Arity, очевидно, задіяний, але дозволяє відкласти його на деякий час і використовувати функції без аргументів. У такій мові, як javascript, де дужки є обов'язковими, легко змінити різницю між передачею функції в якості аргументу та викликом функції. Ви називаєте це лише тоді, коли використовуєте дужки.

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

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

Як бачите, називати його чи ні не має великого значення. Але еліксир і рубін дозволяють викликати функції без дужок. Це особистий вибір, який мені особисто подобається, але він має цей побічний ефект, ви не можете використовувати лише ім'я без дужок, оскільки це може означати, що ви хочете викликати функцію. Це для чого &. Якщо ви залишаєте arity appart на секунду, попередньо додавши ім'я функції, &означає, що ви явно хочете використовувати цю функцію як аргумент, а не те, що ця функція повертає.

Тепер анонімна функція трохи відрізняється тим, що вона в основному використовується як аргумент. Знову ж це вибір дизайну, але раціональним за ним є те, що він в основному використовується ітераторами типу функцій, які приймають функції як аргументи. Тож очевидно, що вам не потрібно використовувати, &оскільки вони вже вважаються аргументами за замовчуванням. Це їх призначення.

Тепер остання проблема полягає в тому, що іноді доводиться викликати їх у своєму коді, оскільки вони не завжди використовуються з функцією ітератора, або ви можете самі кодувати ітератор. Невелика історія, оскільки рубін орієнтований на об'єкт, основним способом це було використання 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)

Знову ж таки, це вибір дизайну. Тепер еліксир не орієнтований на об'єкти, тому не вимагайте використання першої форми точно. Я не можу говорити за Хосе, але схоже, що друга форма була використана в еліксирі, оскільки вона все ще схожа на виклик функції з додатковим символом. Це досить близько до виклику функції.

Я не замислювався над усіма плюсами та мінусами, але схоже, що в обох мовах ви можете уникнути лише дужок, доки ви зробите дужки обов’язковими для анонімних функцій. Схоже, це так:

Обов’язкові дужки VS Трохи інші позначення

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

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

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

У всякому разі ... це поза сферою.


9

Є чудова публікація в блозі про таку поведінку: посилання

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

Якщо модуль містить це:

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

Ви не можете просто вирізати та вставити це в оболонку і отримати такий же результат.

Це тому, що в Ерланге є помилка. Модулі в Ерланге - це послідовності ФОРМ . Оболонка Erlang оцінює послідовність ЕКСПРЕСІЙ . У Ерланге ФОРМИ - це НЕ ВИЗНАЧЕННЯ .

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

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


Я не впевнений у корисності прямого відповіді на питання про ОП, але надане посилання (включаючи розділ коментарів) насправді є чудовим IMO для читання цільової аудиторії питання, тобто для тих, хто з нас не в курсі 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)

Оскільки еліксир може виконувати функції без дужок (наприклад myfunction), використовуючи otherfunction(myfunction))його, спробуємо виконати myfunction/0.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.