mardi 17 janvier 2012

La curryfication en Ruby


Cet article portera sur un point très particulier de Ruby (et traitera d'ailleurs d'un sujet qui n'appartient pas du tout à ce qu'il est indispensable de savoir en Ruby).

Introduction

La curryfication est une opération typique de la programmation fonctionnelle.
Dans cet article, nous allons aborder cette opération au moyen du langage de programmation Ruby, qui entre nous, n'est pas vraiment un langage où ce genre de procédé est important. Il s'agit surtout de curiosité.

 Cet article aura donc pour objectif de répondre à une certaine curiosité même si je doute que la curryfication prenne une place importante dans vos projets (du moins avec Ruby).

Définition & explication

Avant d'aborder l'aspect historique de la curryifcation, il est important de voir une chose. Dans un langage comme Haskell, toutes les fonctions ne prennent qu'un seul et unique argument/paramètre. Ceux qui ne programme pas en Haskell seront outrés ! Un seul argument c'est bien trop peu ! Ceux qui connaissent un petit peu Haskell (du moins de vue), diront que ces faux et ceux qui connaissent diront oui. En effet, en Haskell, les fonctions ne prennent qu'un seul argument et cet argument est une fonction qui prend elle même en unique argument le paramètre suivant et ainsi de suite.

Exemple
Voici une fonction f qui prend (normalement) 4 arguments. Comme elle est découpée ci-dessous, on peut facilement voir qu'elle prend 1 argument qui prend elle même un argument qui est une fonction etc. 

   f(g(h(i(x))))

Dans la programmation fonctionnelle, généralement, utiliser un espace entre 2 choses consiste à y appliquer une fonction, on peut donc considérer un espace comme un genre d'opérateur.
La curryfication est une opération qui permet donc de créer des fonctions pures (qui retournes toujours la même valeur pour les mêmes arguments).

Un peu d'Histoire
Je conclurai cette introduction en parlant de Moses Schönfinkel, l'inventeur de la curryfication et auteur d'un article sur la logique combinatoire, lui ayant attribué ce nom en référence au mathématicien Haskell Curry (partiellement contemporain à Moses Schönfinkel), que certains d'entre vous connaissent pour avoir poser les bases de la programmation fonctionnelle, le paradoxe de Curry (très amusant paradoxe permettant d'arriver à n'importe quelle conclusion à partir d'une phrase auto-référentielle et de quelques règles logiques, par exemple:
"Si cette phrase est vraie, alors le monstre du Memphrémagog existe.") ainsi que la correspondance Curry-Howard, un pont entre la théorie de la démonstration et l'informatique théorique qui a joué un rôle très important dans la logique.

Application & exemple d'utilisations

Tout ceci peut paraitre fort inutile mais nous allons voir des exemples très pratique d'utilisation. Exemples qui prouvent que la curryfication peut être un moyen très élégant pour résoudre certains cas très spécifique.
Prenons par exemple, une fonction dont le rôle est de multiplier 2 entiers entre eux (je suis d'accord que cet exemple n'est pas très original et je m'en excuse). Voici une solution OCaML:

 let multiplier x y = 
      x * y;;

Cette fonction est relativement simple et se contente de prendre 2 arguments et d'en retourner leur produit.
Cette petite fonction nous amène à une des utilisation les plus courante (pour ne pas dire la seule) de la curryfication, l'application partielle ! Posons le cas très simple où, possédant une fonction multiplier, j'ai envie d'écrire une fonction multiplierPar2. Rien de plus simple, il suffit de l'écrire, identique à la précédente, sauf quelle ne prend qu'un seul argument et qu'elle retourne x*2:

 let multiplier x  = 
      x * 2;;


Cependant, ce ne serait pas très intéressant !
Comme il a été expliqué dans l'introduction, une fonction peut prendre en argument une autre fonction et ainsi de suite. Donc il est possible d'appliquer partiellement nos arguments ! Exemple avec multiplier:

let multiplier x y = 
      x * y;;
let multiplierPar2
      multiplier 2;;

Je viens de faire une application partielle de mes argument. En effet "multiplier 2" va se contenter d'attendre les arguments manquants.
D'ailleurs, en testant "multiplierPar2 6" le résultat sera bel et bien 12.
Dans le cas des fonctions d'ordres supérieures (fonctions qui prennent d'autres fonctions en arguments et qui dans certains cas retournent d'autres fonctions), ce genre d'opérations est vraiment (en plus d'être élégant) puissant.
Avec un peu d'imagination, il serait envisageable de définir une fonction avec moins d'arguments que ce dont elle a besoin et d'utiliser la curryfication pour palier ce manque explicite.

Ce qu'il faut retenir
Tout ce petit blabla peut se résumer en une phrase... "En appellant une fonction avec trop peux de paramètres; on crée, en quelque sorte, des fonctions à la volée.".
Retenez aussi que la programmation fonctionnelle est plus adaptée que les autres paradigmes car beaucoup de langage fonctionnels admettent la curryfication nativement (comme Haskell).


La curryfication appliquée a Ruby

Appliquer des arguments de manière partielle à une fonction écrite en Ruby est envisageable sous plusieurs forme. La plus instinctive serait de jouer avec le passage d'argument sous forme de tableau:


def ma_methode(*arguments)

Ou encore en utilisant le passage d'arguments sous forme de Hash (Dictionnaire pour les Pythoneux)

def ma_methode(hash)
    argument1 = (hash[:argument1] || valeur_par_défaut)
    argument2 = (hash[:argument2] || valeur_par_défaut)
end

Rappelons que la forme x || y vérifie si le membre x et null et attribue si oui, x si non.
Ce genre de méthode est assez peu formelle et nous verrons qu'il existe une autre méthode.

La curryfication, c'est une fonction qui prend comme argument une autre fonction etc.
J'ai souvent répété cette phrase et elle devrait faire tiquer certaines personnes. En effet, quel objet/élément de Ruby est parfaitement adapté à cette situation? Oui, c'est la Lambda/Proc.
En effet, en lisant la description de la classe Proc on peut découvrir une méthode associée à Proc qui est curry, les Procs permettent donc nativement de gerer la curryfication. Exemple:


Comme on peut le voir, il est impossible d'appeler la fonction au moyen de call avec un seul argument (alors que notre lambda en requiert 2). Essayons avec notre fonction curry.


Cela fonctionne parfaitement, notre multiplier.curry retourne bien une nouvelle fonction qui attend un argument en plus. Essayons notre argumentation partielles en créant, sur base de multiplier, une fonction multiplier_par_2. (Curry se contente de retourner un Proc/Lambda avec comme nombre d'arguments les arguments manquants donc multiplier.curry.call(x) revient au même que multiplier.curry[x] voir même multiplier.curry.(x)). Voici la fonction multiplier_par_2 obtenue grâce à une curryfication de multiplier:


Et voila, voici donc un Proc curryfié. Dans le cadre des fonctions d'ordre supérieur, ça peut être très pratique. Cependant, je m'en sert rarement (mais je ne fais pas énormément de Ruby :) ).

Conclusion

Je conclus mon premier article sur FunkyWork ! Je dois avouer que son intérêt (pour Ruby) est assez limité mais je trouvais qu'il s'agissait tout de même d'un aspect amusant et intéressant à développer. 
Lié à certaines fonctions récursives, ou autre traitements spécifiques, la curryfication peut être un apport à la programmation Ruby. J'espère avoir au moins eu l'occasion de vous faire découvrir (ou redécouvrir) quelque chose d'amusant.
Merci beaucoup d'avoir lu cet article et je vous souhaites à tous, une très bonne année 2012 !

Pierre Ruyter.

6 commentaires:

  1. Très instructif, je ne sais pas encore si je l'utiliserai un jour ^^

    RépondreSupprimer
  2. Bonjour

    C'est intéressant mais je n'en saisit pas l'utilité, du moins dans le cadre de ruby.
    J'ai l'impression que cela sert juste à appeller une fonction avec un paramètre par défaut.
    on pourrait alors écrire : def multiplier(x, y = 2)

    Certes, la curryfication permet de mettre autant de valeur par défault que possible (mon exemple ci dessus ne permet pas de gérer par exemple Multiplier_par_6

    Mais, si je suis capable d'écrire dans mon code : Multiplier_par_2[10]
    Pourquoi ne pas écrire directement : Multiplier[10,2]

    Peut-être par souci de clarté.
    Alors dans ce cas, je pourrais aussi utiliser un method missing comme ceci :

    def method_missing( method_name, *args, &block )
    case method_name.to_s when /^Multiplier_par_(\d+)$/
    return multiplier args[0], $1
    end
    end


    Pour moi, la curryfication dans le cadre de ruby n'est pas très claire, dans un cadre général non plus d'ailleurs. Est-ce uniquement un moyen de créer de beaux noms de fonctions ? Ou je manque quelque chose ?

    En tout cas, merci pour cet article qui éveille ma curiosité.

    RépondreSupprimer
    Réponses
    1. Je me permet de répondre. En Ruby la curryfication n'a clairement rien de fascinant ... d'ailleurs il dit que ce n'est que par curiosité qu'il développe ça.
      Dans le cadre de la programmation fonctionnelle (et tout son contexte, variables souvent non-mutables, mise en avant de la récursivité, tout-est-fonction) l’argumentation partielle est vraiment bénéfique notamment dans des cas fonctions d'ordres supérieur, par exemple imaginons un "map" qui accumule des arguments :)
      Merci d'avoir lu.

      Supprimer
  3. J'ajoute que l'argument peut être attendu. Il est possible de l'accumuler progressivement.

    RépondreSupprimer
  4. Proc#curry ne prend pas une « Table » comme tu l’indiques, mais retourne un nouveau Proc. Et Proc#[] est un alias pour Proc#call. Il y a aussi la possibilité d’ajouter un point avant les parenthèses (alors obligatoires) de l’appel de méthode : multiplier_par_2.(10)
    Ça par contre c’est pas une vraie méthode, mais purement du sucre syntaxique pour l’utilisation de call.
    > Object.new.()
    NoMethodError: undefined method `call' for #

    RépondreSupprimer
  5. Oké, je vois :)
    Je suis désolé pour ces imprécisions.
    Je corrige.

    RépondreSupprimer