Pourquoi y a-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é en utilisant myfunction(param1, param2)
  • fonctions anonymes définies avec fn , appelées en utilisant myfn.(param1, param2)

Seul le deuxième type de fonction semble être un object de première classe et peut être passé en 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 à une otherfunction(myfunction(&1, &2)) pour rendre cela facile, mais pourquoi est-ce nécessaire en premier lieu? Pourquoi ne pouvons-nous pas simplement faire d’autres otherfunction(myfunction)) ? Est-ce seulement pour permettre les fonctions de module appelant sans parenthèse comme dans Ruby? Il semble avoir hérité de cette caractéristique d’Erlang, qui comporte également des fonctions de module et des fonctionnalités amusantes. Est-ce que cela provient de la manière dont la machine virtuelle Erlang fonctionne en interne?

Y a-t-il un avantage ayant deux types de fonctions et convertissant d’un type à un autre pour les transmettre à d’autres fonctions? Y a-t-il un avantage ayant deux notations différentes pour appeler les fonctions?

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

Commençons par le second, fn . fn est une fermeture, semblable à 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 avoir plusieurs clauses aussi:

 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 prévoyant un nombre différent d’arguments:

 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 valeur 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 

Ce code ne pourra pas être compilé car chaque fois que vous voyez 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 vierge et que vous ne mélangez pas les variables des différentes étendues. Vous avez une limite claire.

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

 other_function(&hello(&1)) 

Et puis vous avez demandé pourquoi je ne pouvais pas simplement le transmettre aussi bien que dans d’autres langues? C’est parce que les fonctions dans Elixir sont identifiées par leur nom et leur nom. Donc, une fonction qui attend deux arguments est une fonction différente de celle qui en attend trois, même si elles portent le même nom. Donc, si nous avons simplement passé hello , nous n’aurions aucune idée de ce que vous vouliez dire. Celui avec deux, trois ou quatre arguments? C’est exactement la même raison pour laquelle nous ne pouvons pas créer une fonction anonyme avec des clauses avec différentes arités.

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 avec arity 1. Dans le langage et sa documentation, il est très courant d’identifier des fonctions dans cette syntaxe hello/1 .

C’est pourquoi Elixir utilise un point pour appeler des fonctions anonymes. Étant donné que vous ne pouvez pas simplement passer hello tant que fonction, vous devez plutôt le capturer explicitement, il y a une distinction naturelle entre les fonctions nommées et anonymes et une syntaxe distincte pour appeler chacune rend un peu plus explicite (Lispers serait familier avec ceci en raison de la discussion entre Lisp 1 et Lisp 2).

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

Je ne sais pas à quel point cela va être utile à quelqu’un d’autre, mais la façon dont j’ai finalement compris ce concept était de réaliser que les fonctions de l’élixir n’étaient pas des fonctions.

Tout dans l’élixir est une expression. Alors

 MyModule.my_function(foo) 

n’est pas une fonction mais l’expression est retournée en exécutant le code dans my_function . Il n’y a en fait qu’une seule façon d’obtenir une “Fonction” que vous pouvez transmettre en argument, à savoir utiliser la notation de fonction anonyme.

Il est tentant de se référer à la notation fn ou & comme un pointeur de fonction, mais en réalité, 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’exécution, fn, alors la plupart des difficultés deviennent beaucoup plus claires.

Je peux me tromper puisque personne ne l’a mentionné, mais j’ai aussi eu l’impression que la raison en était aussi le fait de pouvoir appeler des fonctions sans crochets.

Arity est évidemment impliqué mais laisse de côté pour un moment et utilise des fonctions sans arguments. Dans un langage comme JavaScript où les parenthèses sont obligatoires, il est facile de faire la différence entre passer une fonction en argument et appeler la fonction. Vous ne l’appelez que lorsque vous utilisez les crochets.

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

Comme vous pouvez le voir, le nommer ou non ne fait pas une grande différence. Mais l’élixir et le rbuy 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 que vous ne pouvez pas utiliser uniquement le nom sans les parenthèses car cela pourrait signifier que vous voulez appeler la fonction. C’est pour ça que le & est. Si vous laissez arity appart une seconde, append le nom de la fonction avec & signifie que vous voulez explicitement utiliser cette fonction comme argument, et non pas ce que cette fonction renvoie.

Maintenant, la fonction anonyme est légèrement différente en ce sens qu’elle est principalement utilisée comme argument. Encore une fois, c’est un choix de conception, mais la raison derrière cela est qu’il est principalement utilisé par les iterators de type de fonctions qui prennent des fonctions comme arguments. Donc, évidemment, vous n’avez pas besoin d’utiliser & parce qu’ils sont déjà considérés comme des arguments par défaut. C’est leur but.

Maintenant, le dernier problème est que, parfois, vous devez les appeler dans votre code, car ils ne sont pas toujours utilisés avec une fonction de type iterator, ou vous pouvez vous-même coder un iterator. Pour la petite histoire, puisque ruby ​​est orienté object, la principale méthode consiste à utiliser la méthode call sur l’object. De cette façon, vous pouvez garder le comportement des parenthèses non obligatoires cohérent.

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

Maintenant, quelqu’un a proposé 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é object et appelle donc pas utiliser le premier formulaire à coup sûr. Je ne peux pas parler pour José mais il semble que la deuxième forme a é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 n’ai pas pensé à tous les avantages et inconvénients, mais il semble que dans les deux langues, vous pourriez vous contenter des parenthèses à condition de rendre les parenthèses obligatoires pour les fonctions anonymes. Il semble que ce soit:

Parenthèses obligatoires VS notation légèrement différente

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

Voici. Maintenant, cela pourrait ne pas être la meilleure explication au monde car j’ai simplifié la plupart des détails. La plupart sont des choix de conception et j’ai essayé de les expliquer sans les juger. J’adore l’élixir, j’aime le rbuy, j’aime les appels à la fonction sans parenthèses, mais comme vous, je trouve les conséquences assez erronées de temps en temps.

Et dans l’é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 des blocs, mais ils ne fonctionnent que lorsque vous avez besoin d’une seule fonction anonyme, qui est le dernier argument. Puis, comme vous devriez être capable de gérer d’autres scénarios, voici la méthode entière / lambda / proc / block confusion.

En tout cas … c’est hors de scope.

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 “l’exécution de fonctions sans parens” de style Ruby.

Comparer:

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

À:

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

Bien que ces deux éléments 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 à la fonction).

Considérez ce que cela signifie quand vous voyez fun1 ou fun2 dans une autre expression? Lors de l’évaluation de cette expression, appelez-vous la fonction référencée ou référencez-vous simplement une valeur hors mémoire?

Il n’y a aucun moyen de savoir au moment de la compilation. Ruby a le luxe d’introspectionner l’espace de noms des variables pour savoir si une liaison de variable a masqué une fonction à un moment donné. Elixir, étant compilé, ne peut pas vraiment faire cela. C’est ce que fait la notation par points, elle indique à 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 avait pas de notation par points. Considérez 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 c’est assez clair pourquoi vous devez donner le conseil à Elixir. Imaginez si chaque variable de référence devait vérifier une fonction? Sinon, imaginez ce que les héroïques seraient nécessaires pour toujours déduire que le déréférencement variable utilisait une fonction?

Il existe un excellent article sur ce comportement: lien

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 le shell et obtenir le même résultat.

C’est parce qu’il y a un bug dans Erlang. Les modules à Erlang sont des séquences de FORMS . Le shell Erlang évalue une séquence d’ EXPRESSIONS . Dans Erlang, FORMS ne sont pas des 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. Cette bêtise a été Erlang pour toujours mais nous ne l’avons pas remarqué et nous avons appris à vivre avec.

Dot en appelant fn

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

A l’école, j’ai appris à appeler des fonctions en écrivant f (10) et non pas (10) – c’est vraiment une fonction avec un nom comme Shell.f (10) (c’est une fonction définie dans le shell). implicite donc il faut juste l’appeler f (10).

Si vous le quittez, attendez-vous à passer les vingt prochaines années de votre vie à expliquer pourquoi.

Seul le deuxième type de fonction semble être un object de première classe et peut être passé en paramètre à d’autres fonctions. Une fonction définie dans un module doit être encapsulée dans un fichier fn. Il y a un sucre syntaxique qui ressemble à une otherfunction(myfunction(&1, &2)) pour rendre cela facile, mais pourquoi est-ce nécessaire en premier lieu? Pourquoi ne pouvons-nous pas simplement faire d’autres otherfunction(myfunction)) ?

Vous pouvez faire autre otherfunction(&myfunction/2)

Comme elixir peut exécuter des fonctions sans les parenthèses (comme myfunction ), en utilisant d’autres 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 arity, car vous pouvez avoir différentes fonctions avec le même nom. Ainsi, &myfunction/2 .