Pourquoi y a-t-il deux types de fonctions dans Elixir?

279

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

  • fonctions définies dans un module avec def, appeléesmyfunction(param1, param2)
  • fonctions anonymes définies avec fn, appelées usingmyfn.(param1, param2)

Seul le second type de fonction semble être un objet de première classe et peut être passé en paramètre à d'autres fonctions. Une fonction définie dans un module doit être enveloppée dans un fn. Il y a du sucre syntaxique qui ressemble otherfunction(myfunction(&1, &2))pour rendre cela facile, mais pourquoi est-il nécessaire en premier lieu? Pourquoi ne pouvons-nous pas simplement faire otherfunction(myfunction))? Est-ce uniquement 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 a également des fonctions et des amusements de module, ainsi vient-il réellement de la façon dont la VM Erlang fonctionne en interne?

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

Alex Marandon
la source

Réponses:

386

Juste pour clarifier la dénomination, ce sont 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,. fnest une fermeture, semblable à un lambdaRuby. Nous pouvons le créer comme suit:

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

Une fonction peut également 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 en attendant 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 obtenons 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.

Maintenant, parlons 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 parviendra pas à compiler car chaque fois que vous voyez un def, vous obtenez une portée 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 vous ne mélangez pas les variables de différentes étendues. Vous avez une frontière 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 peux pas simplement le passer hellocomme dans d'autres langues? C'est parce que les fonctions dans Elixir sont identifiées par leur nom et leur arité. Ainsi, une fonction qui attend deux arguments est une fonction différente de celle qui attend trois, même si elles avaient le même nom. Donc, si nous passions simplement hello, nous n'aurions aucune idée de ce que hellovous vouliez 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 une 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 capturera la fonction nommée locale bonjour avec arity 1. Dans tout le langage et sa documentation, il est très courant d'identifier les fonctions dans cette hello/1syntaxe.

C'est aussi pourquoi Elixir utilise un point pour appeler des fonctions anonymes. Comme vous ne pouvez pas simplement passer hellocomme une 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 appeler chacune rend tout un peu plus explicite (Lispers serait familier avec cela en raison de la discussion Lisp 1 vs Lisp 2).

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

José Valim
la source
30
J'apprends également Elixir, et c'est le premier problème que j'ai rencontré qui m'a donné une pause, quelque chose à ce sujet semblait juste incohérent. Grande explication, mais pour être clair… est-ce le résultat d'un problème de mise en œuvre, ou reflète-t-il une sagesse plus profonde concernant l'utilisation et le passage des fonctions? Étant donné que les fonctions anonymes peuvent correspondre en fonction des valeurs des arguments, il semble qu'il serait utile de pouvoir également faire correspondre le nombre d'arguments (et cohérent avec le modèle de fonction correspondant ailleurs).
Cyclone
17
Ce n'est pas une contrainte d'implémentation dans le sens où elle pourrait également fonctionner comme f()(sans le point).
José Valim
6
Vous pouvez faire correspondre le nombre d'arguments en utilisant is_function/2dans un garde. is_function(f, 2)vérifie qu'il a une arité de 2. :)
José Valim
20
J'aurais préféré des invocations de fonctions sans points pour les fonctions nommées et anonymes. Cela rend parfois confus et vous oubliez si une fonction particulière était anonyme ou nommée. Il y a aussi plus de bruit quand il s'agit de curry.
CMCDragonkai
28
Dans Erlang, les appels de fonctions anonymes et les fonctions régulières sont déjà syntaxiquement différents: SomeFun()et some_fun(). Dans Elixir, si nous supprimions le point, ils seraient les mêmes some_fun()et some_fun()parce que les variables utilisent le même identifiant que les noms de fonction. D'où le point.
José Valim
17

Je ne sais pas à quel point cela sera utile à quelqu'un d'autre, mais la façon dont j'ai finalement pensé au concept était de réaliser que les fonctions d'élixir ne sont pas des fonctions.

Tout dans l'é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. Il n'y a en fait qu'une seule façon d'obtenir une "fonction" que vous pouvez passer comme argument et qui est d'utiliser la notation de fonction anonyme.

Il est tentant de se référer à la notation fn ou & comme un pointeur de fonction, mais c'est en fait beaucoup plus. C'est une fermeture de l'environnement environnant.

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, alors la plupart des difficultés deviennent beaucoup plus claires.

Fred le chien magique
la source
2
Il est clair que MyModule.my_function(foo)c'est une expression mais MyModule.my_function"pourrait" être une expression qui renvoie une fonction "objet". Mais puisque vous devez dire à l'arité, vous auriez besoin de quelque chose comme à la MyModule.my_function/1place. Et je suppose qu'ils ont décidé qu'il valait mieux utiliser une &MyModule.my_function(&1) syntaxe à la place qui permet d'exprimer l'arité (et sert également à d'autres fins). On ne sait toujours pas pourquoi il existe un ()opérateur pour les fonctions nommées et un .()opérateur pour les fonctions non nommées
RubenLaguna
Je pense que vous voulez: &MyModule.my_function/1c'est comment vous pouvez le faire passer en fonction.
jnmandal
14

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

  • fun1est 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 lorsque vous voyez fun1ou fun2dans 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 hors de la mémoire?

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

Et c'est vraiment difficile. Imaginez qu'il n'y ait 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 qu'il est assez clair pourquoi vous devez donner un indice à Elixir. Imaginez si chaque dé-référence variable devait vérifier une fonction? Alternativement, imaginez quelle héroïque serait nécessaire pour toujours déduire que la déréférence variable utilisait une fonction?

Jayson
la source
12

Je peux me tromper car personne ne l'a mentionné, mais j'avais également l'impression que la raison en est également l'héritage rubis de pouvoir appeler des fonctions sans crochets.

Arity est évidemment impliqué mais laisse le mettre de côté pendant un moment et utilise des fonctions sans arguments. Dans un langage comme javascript où les crochets sont obligatoires, il est facile de faire la différence entre passer une fonction en 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 voir, le nommer ou non ne fait pas une grande différence. Mais l'élixir et le rubis 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 crochets, car cela pourrait signifier que vous souhaitez appeler la fonction. C'est pour ça &. Si vous laissez arity appart pendant une seconde, le fait d'ajouter le nom de votre fonction &signifie que vous souhaitez explicitement utiliser cette fonction comme argument, et non ce qu'elle renvoie.

Maintenant, la fonction anonyme est un peu différente en ce qu'elle est principalement utilisée comme argument. Encore une fois, c'est un choix de conception, mais le raisonnement derrière cela est qu'il est principalement utilisé par les itérateurs du type de fonctions qui prennent des fonctions comme arguments. Vous n'avez donc évidemment pas besoin de les utiliser &car 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 itérateur, ou vous pouvez coder vous-même un itérateur. Pour la petite histoire, puisque ruby ​​est orienté objet, la principale façon de le faire était d'utiliser la callméthode sur l'objet. De cette façon, vous pouvez garder le comportement des crochets 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 trouvé un raccourci qui ressemble à une méthode sans nom.

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

Encore une fois, c'est un choix de conception. Maintenant, l'élixir n'est pas orienté objet et donc appelez à ne pas utiliser le premier formulaire à coup sûr. Je ne peux pas parler pour José mais il semble que la deuxième forme ait été utilisée dans l'élixir 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 les inconvénients, mais il semble que dans les deux langues, vous pouvez vous en tirer avec les crochets tant que vous rendez les crochets obligatoires pour les fonctions anonymes. Il semble que ce soit:

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. Puisqu'il y a une différence, vous pourriez aussi bien la rendre évidente et opter pour la notation différente. Les parenthèses obligatoires sembleraient naturelles dans la plupart des cas, mais très déroutantes lorsque les choses ne se déroulent pas comme prévu.

Voici. Maintenant, 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 le rubis, j'aime les appels de fonction sans crochets, 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 qu'en rubis vous avez des blocs en plus. Les blocs sont incroyables et je suis surpris de voir tout ce que 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 ... cela est hors de portée.

Mig R
la source
9

Il y a un excellent article de blog 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 cela dans la coque et obtenir le même résultat.

C'est parce qu'il y a un bug dans Erlang. Les modules d'Erlang sont des séquences de FORMULAIRES . Le shell Erlang évalue une séquence d' EXPRESSIONS . À Erlang, les FORMES 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 toujours été Erlang mais nous ne l'avons pas remarqué et nous avons appris à vivre avec.

Point dans l'appel 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) et non f. (10) - c'est «vraiment» une fonction avec un nom comme Shell.f (10) (c'est une fonction définie dans le shell) La partie shell est implicite donc il devrait juste être appelé f (10).

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

sobolevn
la source
Je ne suis pas sûr de l'utilité de répondre directement à la question OP, mais le lien fourni (y compris la section commentaires) est en fait une excellente lecture IMO pour le public cible de la question, c'est-à-dire ceux d'entre nous qui débutent et apprennent Elixir + Erlang .
Le lien est mort maintenant :(
AnilRedshift
2

Elixir a des accolades optionnelles pour les fonctions, y compris les fonctions avec 0 arité. Voyons un exemple de la raison pour laquelle il est important d'avoir une syntaxe d'appel distincte:

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

Sans faire la différence entre 2 types de fonctions, nous ne pouvons pas dire ce que cela Insanity.divesignifie: obtenir une fonction elle-même, l'appeler ou également appeler la fonction anonyme résultante.

Gramme
la source
1

fn ->la syntaxe permet d'utiliser des fonctions anonymes. Faire var. () Est juste dire à elixir que je veux que vous preniez ce var avec un func dedans et que vous le lanciez au lieu de faire référence au var comme quelque chose qui tient juste cette fonction.

Elixir a ce modèle commun où, au lieu d'avoir une logique à l'intérieur d'une fonction pour voir comment quelque chose doit s'exécuter, nous adaptons différentes fonctions en fonction du type d'entrée que nous avons. Je suppose que c'est pourquoi nous nous référons aux choses par arité dans lefunction_name/1 sens.

C'est un peu bizarre de s'habituer à faire des définitions de fonctions abrégées (func (& 1), etc.), mais pratique lorsque vous essayez de diriger ou de garder votre code concis.

Ian
la source
0

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

Tu peux faire otherfunction(&myfunction/2)

Comme elixir peut exécuter des fonctions sans les crochets (comme myfunction), son utilisation otherfunction(myfunction))tentera de s'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 avec le même nom. Ainsi, &myfunction/2.

ryanwinchester
la source