web-dev-qa-db-fra.com

Pourquoi existe-t-il deux types de fonctions dans Elixir?

J'apprends Elixir et je me demande pourquoi il existe deux types de définitions de fonctions:

  • fonctions définies dans un module avec def, appelées à l'aide de myfunction(param1, param2)
  • fonctions anonymes définies avec fn, appelées à l'aide de myfn.(param1, param2)

Seul le deuxième type de fonction semble être un objet de première classe et peut être transmis en tant que paramètre à d'autres fonctions. Une fonction définie dans un module doit être encapsulée dans un fn. Il y a un sucre syntaxique qui ressemble à otherfunction(myfunction(&1, &2)) afin de faciliter les choses, mais pourquoi est-ce nécessaire en premier lieu? Pourquoi ne pouvons-nous pas simplement faire otherfunction(myfunction))? Est-ce seulement pour permettre d'appeler des fonctions de module sans parenthèses, comme dans Ruby? Il semble avoir hérité de cette caractéristique d'Erlang, qui possède également des fonctions de module et des fonctions amusantes. Cela vient-il de la façon dont Erlang VM fonctionne en interne?)?

Y at-il un avantage à avoir deux types de fonctions et à passer d’un type à l’autre afin de les passer à d’autres fonctions? Y a-t-il un avantage à avoir deux notations différentes pour appeler des fonctions?

262
Alex Marandon

Juste pour clarifier la dénomination, ce sont les deux fonctions. L'une est une fonction nommée et l'autre est une fonction anonyme. Mais vous avez raison, ils travaillent un peu différemment et je vais expliquer pourquoi ils fonctionnent comme ça.

Commençons par la seconde, fn. fn est une fermeture similaire à un lambda en Ruby. Nous pouvons le créer comme suit:

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

Une fonction peut aussi avoir plusieurs clauses:

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

Maintenant, essayons quelque chose de différent. Essayons de définir différentes clauses attendues avec un nombre d'arguments différent:

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

Oh non! Nous avons une erreur! Nous ne pouvons pas mélanger des clauses qui attendent un nombre différent d'arguments. Une fonction a toujours une arité fixe.

Parlons maintenant des fonctions nommées:

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

Comme prévu, ils ont un nom et peuvent également recevoir des arguments. Cependant, ce ne sont pas des fermetures:

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

La compilation de ce code échouera car chaque fois que vous verrez un def, vous obtenez une étendue de variable vide. C'est une différence importante entre eux. J'aime particulièrement le fait que chaque fonction nommée commence par une table rase et que les variables de différentes portées ne soient pas toutes mélangées. Vous avez une limite claire.

Nous pourrions récupérer la fonction nommée hello ci-dessus en tant que fonction anonyme. Vous en avez parlé vous-même:

other_function(&hello(&1))

Et ensuite, vous avez demandé pourquoi je ne pouvais pas simplement le passer comme hello comme dans d’autres langues? En effet, les fonctions dans Elixir sont identifiées par leur nom et arity. Ainsi, une fonction qui attend deux arguments est une fonction différente de celle qui en attend trois, même s'ils avaient le même nom. Donc, si nous passions simplement hello, nous n'aurions aucune idée de ce que hello voulait réellement dire. Celui avec deux, trois ou quatre arguments? C'est exactement la même raison pour laquelle nous ne pouvons pas créer de fonction anonyme avec des clauses d'arités différentes.

Depuis Elixir v0.10.1, nous avons une syntaxe pour capturer les fonctions nommées:

&hello/1

Cela va capturer la fonction nommée locale hello with arity 1. Tout au long du langage et de sa documentation, il est très courant d’identifier des fonctions dans cette hello/1 syntaxe.

C’est aussi pourquoi Elixir utilise un point pour appeler des fonctions anonymes. Etant donné que vous ne pouvez pas simplement passer hello en tant que fonction, vous devez plutôt la capturer explicitement. Il existe une distinction naturelle entre les fonctions nommées et anonymes et une syntaxe distincte pour les appeler rend chaque chose un peu plus explicite ( Les Lispers le sauraient bien en raison de la discussion entre LISP 1 et LISP 2).

Globalement, ce sont les raisons pour lesquelles nous avons deux fonctions et pourquoi elles se comportent différemment.

361
José Valim

Je ne sais pas à quel point cela sera utile à quiconque, mais j'ai finalement compris que le concept était de réaliser que les fonctions d'élixir n'étaient pas des fonctions.

Tout en élixir est une expression. Alors

MyModule.my_function(foo) 

n'est pas une fonction mais l'expression renvoyée en exécutant le code dans my_function. En fait, il n’ya qu’un moyen d’obtenir une "Fonction" que vous pouvez transmettre sous forme d’argument: utiliser la notation de fonction anonyme.

Il est tentant de faire référence à la notation fn ou & notation en tant que pointeur de fonction, mais c’est beaucoup plus. C'est une fermeture de l'environnement.

Si vous vous demandez:

Ai-je besoin d'un environnement d'exécution ou d'une valeur de données à cet endroit?

Et si vous avez besoin d'une exécution, utilisez fn, la plupart des difficultés deviennent alors beaucoup plus claires.

Je me trompe peut-être puisque personne ne l’a mentionné, mais j’avais aussi l’impression que cela tient aussi à l’héritage Ruby de pouvoir appeler des fonctions sans crochets.

Arity est évidemment impliqué, mais mettons cela de côté et utilisons des fonctions sans arguments. Dans un langage tel que javascript, où les crochets sont obligatoires, il est facile de faire la différence entre passer une fonction en tant qu'argument et appeler la fonction. Vous l'appelez uniquement lorsque vous utilisez les crochets.

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

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

Comme vous pouvez le constater, le nom ou non ne fait pas une grande différence. Mais elixir et Ruby vous permettent d’appeler des fonctions sans les crochets. C'est un choix de conception que j'aime personnellement, mais il a cet effet secondaire: vous ne pouvez pas utiliser uniquement le nom sans les crochets car cela pourrait vouloir dire que vous souhaitez appeler la fonction. C'est à cela que sert le &. Si vous laissez l'arité à part pendant une seconde, l'ajout du nom de votre fonction à l'aide de & Signifie que vous souhaitez explicitement utiliser cette fonction en tant qu'argument, et non ce que cette fonction renvoie.

Maintenant, la fonction anonyme est un peu différente en ce sens qu’elle est principalement utilisée comme argument. Là encore, il s’agit d’un choix de conception, mais la raison en est qu’il est principalement utilisé par les itérateurs, type de fonctions qui prennent des fonctions en tant qu’arguments. Donc, évidemment, vous n'avez pas besoin d'utiliser & Car ils sont déjà considérés comme des arguments par défaut. C'est leur but.

Le dernier problème est que vous devez parfois les appeler dans votre code, car ils ne sont pas toujours utilisés avec une fonction de type itérateur, ou vous pouvez coder vous-même un itérateur. Pour la petite histoire, puisque Ruby est orienté objet, le moyen principal de le faire était d’utiliser la méthode call sur l’objet. De cette façon, vous pouvez conserver le comportement des crochets non obligatoires.

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

Maintenant, quelqu'un a créé un raccourci qui ressemble à une méthode sans nom.

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

Encore une fois, ceci est un choix de conception. Maintenant, elixir n'est pas orienté objet et appelle donc pas la première forme à coup sûr. Je ne peux pas parler pour José mais il semble que la deuxième forme ait été utilisée dans elixir car elle ressemble toujours à un appel de fonction avec un caractère supplémentaire. C'est assez proche d'un appel de fonction.

Je ne pensais pas à tous les avantages et les inconvénients, mais il semble que dans les deux langues, vous pouvez vous en sortir avec les crochets tant que vous les rendez obligatoires pour les fonctions anonymes. Il semble que ce soit le cas:

Crochets obligatoires VS Notation légèrement différente

Dans les deux cas, vous faites une exception car vous faites en sorte que les deux se comportent différemment. Comme il y a une différence, vous pouvez tout aussi bien le rendre évident et opter pour la notation différente. Les crochets obligatoires sembleraient naturels dans la plupart des cas, mais très déroutants lorsque les choses ne se passent pas comme prévu.

Voici. Ce n’est peut-être pas la meilleure explication au monde, car j’ai simplifié la plupart des détails. De plus, la plupart sont des choix de conception et j'ai essayé de leur donner une raison sans les juger. J'aime l'élixir, j'aime Ruby, j'aime les appels de fonctions sans crochets, mais comme vous, je trouve les conséquences assez erronées de temps en temps.

Et en élixir, c’est juste ce point supplémentaire, alors que dans Ruby, vous avez des blocs en plus. Les blocs sont incroyables et je suis surpris de voir combien vous pouvez faire avec seulement des blocs, mais ils ne fonctionnent que lorsque vous avez besoin d’une seule fonction anonyme qui est le dernier argument. Ensuite, puisque vous devriez être capable de gérer d’autres scénarios, voici toute la confusion méthode/lambda/proc/block.

Quoi qu'il en soit ... c'est hors de portée.

11
Mig R

Je n'ai jamais compris pourquoi les explications sont si compliquées.

C'est vraiment juste une distinction exceptionnellement petite combinée avec les réalités de la "fonction d'exécution sans parens" à la Ruby.

Comparer:

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

À:

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

Bien que les deux ne soient que des identifiants ...

  • fun1 est un identifiant qui décrit une fonction nommée définie avec def.
  • fun2 est un identifiant qui décrit une variable (qui contient une référence à une fonction).

Considérez ce que cela signifie quand vous voyez fun1 ou fun2 dans une autre expression? Lorsque vous évaluez cette expression, appelez-vous la fonction référencée ou faites-vous simplement référence à une valeur insuffisante en mémoire?

Il n'y a pas de bon moyen de savoir au moment de la compilation. Ruby a le luxe d’introspectionner l’espace de nom de variable pour savoir si une liaison variable a occulté une fonction à un moment donné. Elixir, en cours de compilation, ne peut pas vraiment le faire. C’est ce que le La notation par points, elle dit à Elixir qu'elle devrait contenir une référence de fonction et qu'elle devrait être appelée.

Et c'est vraiment difficile. Imaginez qu'il n'y ait pas de notation par points. Considérons ce code:

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?

Compte tenu du code ci-dessus, je pense que la raison pour laquelle vous devez donner à Elixir une indication claire est assez claire. Imaginez si chaque variable de référence de référence devait vérifier une fonction? Alternativement, imaginez ce que les héros seraient nécessaires pour toujours déduire que le déréférencement variable utilisait une fonction?

10
Jayson

Il existe un excellent article de blog à propos de ce comportement: link

Deux types de fonctions

Si un module contient ceci:

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

Vous ne pouvez pas simplement couper et coller ceci dans Shell et obtenir le même résultat.

C’est parce qu’il ya un bug à Erlang. Les modules dans Erlang sont des séquences de formes [~ # ~] [~ # ~] . Erlang Shell évalue une séquence d'expressions [~ # ~] [~ # ~] . En Erlang [~ # ~] les formes [~ # ~] ne sont pas [~ # ~] les expressions [~ # ~] .

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

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

Les deux ne sont pas les mêmes. Erlang a toujours eu cette bêtise, mais nous ne l’avons pas remarquée et nous avons appris à vivre avec.

Point en appelant fn

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

À l’école, j’ai appris à appeler des fonctions en écrivant f(10) pas f. (10) - c’est vraiment une fonction avec un nom comme Shell.f (10) (c’est une fonction défini dans le shell) La partie shell étant implicite, il convient de l’appeler f (10).

Si vous le laissez comme ça, attendez-vous à expliquer pourquoi pendant les vingt prochaines années de votre vie.

8
sobolevn

fn -> _ La syntaxe sert à utiliser des fonctions anonymes. Faire var. (), C'est simplement dire à élixir que je veux que vous preniez cette var avec une fonction dans le programme et l'exécutiez au lieu de faire référence à var comme quelque chose qui ne tient que cette fonction.

Elixir a ce modèle commun dans lequel, au lieu d’avoir la logique dans une fonction pour voir comment quelque chose doit s’exécuter, nous modélisons différentes fonctions en fonction du type d’entrée que nous avons. Je suppose que c’est la raison pour laquelle nous nous référons aux choses par arité dans le function_name/1 sens.

C'est un peu étrange de s'habituer à la définition abrégée de fonctions (func (& 1), etc.), mais pratique lorsque vous essayez de canaliser votre code ou de le garder concis.

0
Ian

Seul le deuxième type de fonction semble être un objet de première classe et peut être transmis en tant que paramètre à d'autres fonctions. Une fonction définie dans un module doit être encapsulée dans un fn. Il y a un sucre syntaxique qui ressemble à otherfunction(myfunction(&1, &2)) afin de faciliter les choses, mais pourquoi est-ce nécessaire en premier lieu? Pourquoi ne pouvons-nous pas simplement faire otherfunction(myfunction))?

Vous pouvez faire otherfunction(&myfunction/2)

Comme elixir peut exécuter des fonctions sans les crochets (comme myfunction), en utilisant otherfunction(myfunction)), il essaiera d’exécuter myfunction/0.

Vous devez donc utiliser l'opérateur de capture et spécifier la fonction, y compris l'arity, car vous pouvez avoir différentes fonctions du même nom. Ainsi, &myfunction/2.

0
ryanwinchester