J'apprends Elixir et je me demande pourquoi il existe deux types de définitions de fonctions:
def
, appelées à l'aide de myfunction(param1, param2)
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?
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.
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.
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?
Il existe un excellent article de blog à propos de ce comportement: link
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.
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.
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.
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 faireotherfunction(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
.