Parcours
/
Julia
Julia
/
Programme
/
Composition de fonctions
Co

Composition de fonctions en Julia

1 exercice

À propos de Composition de fonctions

Julia encourage à mettre autant de code que possible dans des fonctions qui peuvent être compilées à la volée (JIT), et créer de nombreuses petites fonctions est, par conception, performant.

Cela a tendance à laisser beaucoup de petites fonctions simples, qu'il faut ensuite combiner pour accomplir des tâches non triviales.

Une approche évidente consiste à imbriquer les appels de fonctions. L'exemple suivant est très artificiel, mais il illustre le propos.

julia> first.(titlecase.(reverse.(["my", "test", "strings"])))
3-element Vector{Char}:
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

L'inconvénient de cette approche est que la lisibilité chute rapidement à mesure que l'imbrication s'approfondit.

Il nous faut une approche plus simple et plus flexible.

Composition

C'est la technique chère aux mathématiciens, et Julia en reprend la syntaxe mathématique.

Un nombre arbitraire de fonctions peut être composed à l'aide d'opérateurs ∘ (que l'on saisit avec \circ puis tabulation). Le résultat peut être utilisé comme une seule fonction.

julia> compfunc = first ∘ titlecase ∘ reverse
first ∘ titlecase ∘ reverse

julia> compfunc.(["my", "test", "strings"])
3-element Vector{Char}:
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

# alternative syntax, giving the same result
julia> (first ∘ titlecase ∘ reverse).(["my", "test", "strings"])

Quelques points à noter :

  • Les fonctions de départ apparaissent dans le même ordre que lors d'une imbrication, et sont exécutées de droite à gauche.
  • Le broadcasting n'est pas simple à utiliser lors de la composition, mais on peut l'appliquer au moment d'appeler la fonction composée.

Enchaînement avec des pipes

On peut aussi y voir l'approche des programmeurs, plutôt que celle des mathématiciens.

Les Pipelines sont utilisés depuis longtemps dans les scripts shell Unix, et sont devenus plus récemment populaires dans les langages de programmation grand public (on attribue parfois à F# leur adoption pionnière).

Le principe de base est de partir de quelques données, puis de les faire passer au travers d'une séquence de fonctions pour obtenir le résultat.

L'opérateur pipe est |> (comme en F# et dans les versions récentes de R), mais Julia dispose aussi d'une version broadcasting .|>.

julia> ["my", "test", "strings"] .|> reverse .|> titlecase .|> first
3-element Vector{Char}:
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

L'exécution se fait désormais strictement de gauche à droite, la sortie de chaque fonction circulant dans le sens de la flèche pour devenir l'entrée de la fonction suivante.

Limites, contournements et autres options

Ce n'est pas un hasard si les fonctions utilisées pour illustrer la composition et l'enchaînement avec des pipes prennent toutes un seul argument.

Certains langages purement fonctionnels font passer le premier argument dans une fonction, tout en permettant d'y inclure d'autres.

En revanche, Julia n'attend que des noms de fonctions (ou leur équivalent) dans un pipeline, sans aucun argument supplémentaire.

Cela répond à d'importantes raisons techniques (liées au fait que le currying n'est pas un élément standard de la conception du langage). Les nombreuses personnes qui ne connaissent rien au currying doivent simplement accepter que cette limitation n'est pas un oubli négligent, et qu'elle ne devrait pas changer dans les futures versions de Julia.

Contournements

On a besoin de fonctions à un seul argument qui font tout ce qu'il faut. Heureusement, définir de nouvelles fonctions en Julia est facile.

Le plus simple est d'utiliser une anonymous function. Par exemple, si l'on a une seule string en entrée et qu'on veut la découper sur les tirets bas :

julia> "my_test_strings" |> (s -> split(s, '_'))
3-element Vector{SubString{String}}:
 "my"
 "test"
 "strings"

Ce tableau peut ensuite être passé à d'autres fonctions via un pipe, comme précédemment.

Mettre la fonction anonyme entre parenthèses est facultatif dans ce cas, mais c'est de manière plus générale un bon moyen de réduire l'ambiguïté.

De même, on peut créer une fonction nommée, plus tôt dans le programme, et la réutiliser au besoin.

Les Closures dépassent le cadre de ce concept, mais quiconque les connaît d'autres langages reconnaîtra qu'elles offrent un moyen plus flexible de créer des fonctions à un seul argument.

julia> function makesplit(sep)
           fs(str) = split(str, sep)
           fs
       end
makesplit (generic function with 1 method)

julia> f_us = makesplit('_')
(::var"#fs#32"{Char}) (generic function with 1 method)

julia> "my_test_strings" |> f_us
3-element Vector{SubString{String}}:
 "my"
 "test"
 "strings"

# alternatively:
julia> "my_test_strings" |> makesplit('_')
3-element Vector{SubString{String}}:
 "my"
 "test"
 "strings"

Une fois makesplit() définie, elle peut servir avec n'importe quel séparateur. Remarque que makesplit('_') est un appel de fonction qui donne une autre fonction, laquelle reçoit à son tour l'entrée du pipe.

Si cela te semble confus, c'est normal au début (mais ça devient plus clair avec la pratique).

Autres options

Une longue discussion a eu lieu pour rendre les pipes plus polyvalents dans Julia de base, mais les diverses suggestions sont mutuellement incompatibles et aucun accord n'a été trouvé.

En attendant, les utilisateurs et utilisatrices ont opté pour la solution habituelle : créer divers paquets installables qui répondent à des besoins précis. Aucun ne fonctionnera dans Exercism, mais jette un œil à ceux-ci si ça t'intéresse :

Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet

Apprends Composition de fonctions