web-dev-qa-db-fra.com

Qu'est-ce que la «correspondance de motifs» dans les langages fonctionnels?

Je lis sur la programmation fonctionnelle et j'ai remarqué que Pattern Matching est mentionné dans de nombreux articles comme l'une des principales caractéristiques des langages fonctionnels.

Quelqu'un peut-il expliquer à un développeur Java/C++/JavaScript ce que cela signifie?

117
Roman

Comprendre la correspondance de motifs nécessite d'expliquer trois parties:

  1. Types de données algébriques.
  2. Qu'est-ce que la correspondance de motifs
  3. Pourquoi c'est génial.

Types de données algébriques en bref

Les langages fonctionnels de type ML vous permettent de définir des types de données simples appelés "unions disjointes" ou "types de données algébriques". Ces structures de données sont de simples conteneurs et peuvent être définies de manière récursive. Par exemple:

type 'a list =
    | Nil
    | Cons of 'a * 'a list

définit une structure de données de type pile. Considérez-le comme équivalent à ce C #:

public abstract class List<T>
{
    public class Nil : List<T> { }
    public class Cons : List<T>
    {
        public readonly T Item1;
        public readonly List<T> Item2;
        public Cons(T item1, List<T> item2)
        {
            this.Item1 = item1;
            this.Item2 = item2;
        }
    }
}

Ainsi, les identificateurs Cons et Nil définissent simple une classe simple, où of x * y * z * ... Définit un constructeur et certains types de données. Les paramètres du constructeur sont sans nom, ils sont identifiés par la position et le type de données.

Vous créez des instances de votre classe a list En tant que telles:

let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))

C'est la même chose que:

Stack<int> x = new Cons(1, new Cons(2, new Cons(3, new Cons(4, new Nil()))));

Correspondance des motifs en un mot

La correspondance de formes est une sorte de test de type. Supposons donc que nous ayons créé un objet pile comme celui ci-dessus, nous pouvons implémenter des méthodes pour jeter un œil et faire apparaître la pile comme suit:

let peek s =
    match s with
    | Cons(hd, tl) -> hd
    | Nil -> failwith "Empty stack"

let pop s =
    match s with
    | Cons(hd, tl) -> tl
    | Nil -> failwith "Empty stack"

Les méthodes ci-dessus sont équivalentes (bien que non implémentées en tant que telles) au C # suivant:

public static T Peek<T>(Stack<T> s)
{
    if (s is Stack<T>.Cons)
    {
        T hd = ((Stack<T>.Cons)s).Item1;
        Stack<T> tl = ((Stack<T>.Cons)s).Item2;
        return hd;
    }
    else if (s is Stack<T>.Nil)
        throw new Exception("Empty stack");
    else
        throw new MatchFailureException();
}

public static Stack<T> Pop<T>(Stack<T> s)
{
    if (s is Stack<T>.Cons)
    {
        T hd = ((Stack<T>.Cons)s).Item1;
        Stack<T> tl = ((Stack<T>.Cons)s).Item2;
        return tl;
    }
    else if (s is Stack<T>.Nil)
        throw new Exception("Empty stack");
    else
        throw new MatchFailureException();
}

(Presque toujours, les langages ML implémentent la correspondance de modèles sans les tests de type au moment de l'exécution ou les transtypages, donc le code C # est quelque peu trompeur. Mettons de côté les détails de l'implémentation en agitant la main s'il vous plaît :))

Décomposition de la structure des données en bref

Ok, revenons à la méthode peek:

let peek s =
    match s with
    | Cons(hd, tl) -> hd
    | Nil -> failwith "Empty stack"

L'astuce consiste à comprendre que les identifiants hd et tl sont des variables (errm ... car ils sont immuables, ce ne sont pas vraiment des "variables", mais des "valeurs";)). Si s a le type Cons, alors nous allons extraire ses valeurs du constructeur et les lier aux variables nommées hd et tl .

La correspondance de motifs est utile car elle nous permet de décomposer une structure de données par son forme au lieu de son contenu. Imaginez donc si nous définissons un arbre binaire comme suit:

type 'a tree =
    | Node of 'a tree * 'a * 'a tree
    | Nil

Nous pouvons définir quelques rotation des arbres comme suit:

let rotateLeft = function
    | Node(a, p, Node(b, q, c)) -> Node(Node(a, p, b), q, c)
    | x -> x

let rotateRight = function
    | Node(Node(a, p, b), q, c) -> Node(a, p, Node(b, q, c))
    | x -> x

(Le constructeur let rotateRight = function Est le sucre de syntaxe pour let rotateRight s = match s with ....)

Ainsi, en plus de lier la structure des données aux variables, nous pouvons également l'explorer. Disons que nous avons un nœud let x = Node(Nil, 1, Nil). Si nous appelons rotateLeft x, Nous testons x par rapport au premier modèle, qui ne correspond pas car le bon enfant a le type Nil au lieu de Node. Il passera au modèle suivant, x -> x, Qui correspondra à n'importe quelle entrée et la renverra sans modification.

Pour comparaison, nous écririons les méthodes ci-dessus en C # comme:

public abstract class Tree<T>
{
    public abstract U Match<U>(Func<U> nilFunc, Func<Tree<T>, T, Tree<T>, U> nodeFunc);

    public class Nil : Tree<T>
    {
        public override U Match<U>(Func<U> nilFunc, Func<Tree<T>, T, Tree<T>, U> nodeFunc)
        {
            return nilFunc();
        }
    }

    public class Node : Tree<T>
    {
        readonly Tree<T> Left;
        readonly T Value;
        readonly Tree<T> Right;

        public Node(Tree<T> left, T value, Tree<T> right)
        {
            this.Left = left;
            this.Value = value;
            this.Right = right;
        }

        public override U Match<U>(Func<U> nilFunc, Func<Tree<T>, T, Tree<T>, U> nodeFunc)
        {
            return nodeFunc(Left, Value, Right);
        }
    }

    public static Tree<T> RotateLeft(Tree<T> t)
    {
        return t.Match(
            () => t,
            (l, x, r) => r.Match(
                () => t,
                (rl, rx, rr) => new Node(new Node(l, x, rl), rx, rr))));
    }

    public static Tree<T> RotateRight(Tree<T> t)
    {
        return t.Match(
            () => t,
            (l, x, r) => l.Match(
                () => t,
                (ll, lx, lr) => new Node(ll, lx, new Node(lr, x, r))));
    }
}

Pour sérieusement.

La correspondance des motifs est impressionnante

Vous pouvez implémenter quelque chose similaire à la correspondance de modèles en C # en utilisant modèle visiteur , mais ce n'est pas aussi flexible car vous ne pouvez pas décomposer efficacement les structures de données complexes. De plus, si vous utilisez la correspondance de motifs, le compilateur vous dira si vous avez omis un cas. C'est génial?

Pensez à la façon dont vous implémenteriez des fonctionnalités similaires en C # ou dans des langages sans correspondance de modèle. Réfléchissez à la façon dont vous le feriez sans test-tests et transtypages lors de l'exécution. Ce n'est certainement pas difficile, juste encombrant et encombrant. Et vous n'avez pas le compilateur qui vérifie que vous avez couvert tous les cas.

Ainsi, la correspondance de motifs vous aide à décomposer et à naviguer dans les structures de données dans une syntaxe très pratique et compacte, elle permet au compilateur de vérifier la logique de votre code, au moins un peu. C'est vraiment c'est une fonctionnalité qui tue.

129
Juliet

Réponse courte: L'appariement de motifs se produit parce que les langages fonctionnels traitent le signe égal comme une affirmation d'équivalence au lieu d'une affectation.

Réponse longue: La correspondance de motifs est une forme de répartition basée sur la "forme" de la valeur qui lui est donnée. Dans un langage fonctionnel, les types de données que vous définissez sont généralement appelés unions discriminées ou types de données algébriques. Par exemple, qu'est-ce qu'une liste (liée)? Une liste chaînée List de choses d'un certain type a est soit la liste vide Nil soit un élément de type aConsed sur un List a (Une liste de as). En Haskell (le langage fonctionnel que je connais le mieux), nous écrivons ceci

data List a = Nil
            | Cons a (List a)

Tous les syndicats discriminés sont définis de cette façon: un type unique a un nombre fixe de manières différentes de le créer; les créateurs, comme Nil et Cons ici, sont appelés constructeurs. Cela signifie qu'une valeur du type List a Aurait pu être créée avec deux constructeurs différents - elle pourrait avoir deux formes différentes. Supposons donc que nous voulons écrire une fonction head pour obtenir le premier élément de la liste. Dans Haskell, nous écririons ceci comme

-- `head` is a function from a `List a` to an `a`.
head :: List a -> a
-- An empty list has no first item, so we raise an error.
head Nil        = error "empty list"
-- If we are given a `Cons`, we only want the first part; that's the list's head.
head (Cons h _) = h

Étant donné que les valeurs de List a Peuvent être de deux types différents, nous devons traiter chacune séparément; c'est la correspondance des motifs. Dans head x, Si x correspond au modèle Nil, alors nous exécutons le premier cas; s'il correspond au modèle Cons h _, nous exécutons le second.

Réponse courte, expliquée: Je pense que l'une des meilleures façons de penser à ce comportement est de changer la façon dont vous pensez du signe égal. Dans les langues entre accolades, dans l'ensemble, = Indique une affectation: a = b Signifie "faire a en b". Dans de nombreux langages fonctionnels , cependant, = dénote une affirmation d'égalité: let Cons a (Cons b Nil) = frob x affirme que la chose à gauche, Cons a (Cons b Nil), est équivalent à la chose à droite, frob x; de plus, toutes les variables utilisées à gauche deviennent visibles. C'est aussi ce qui se passe avec les arguments de fonction: nous affirmons que le premier argument ressemble à Nil, et si ce n'est pas le cas, nous continuons de vérifier.

30

Cela signifie qu'au lieu d'écrire

double f(int x, int y) {
  if (y == 0) {
    if (x == 0)
      return NaN;
    else if (x > 0)
      return Infinity;
    else
      return -Infinity;
  } else
     return (double)x / y;
}

Tu peux écrire

f(0, 0) = NaN;
f(x, 0) | x > 0 = Infinity;
        | else  = -Infinity;
f(x, y) = (double)x / y;

Hé, C++ prend également en charge la correspondance de modèles.

static const int PositiveInfinity = -1;
static const int NegativeInfinity = -2;
static const int NaN = -3;

template <int x, int y> struct Divide {
  enum { value = x / y };
};
template <bool x_gt_0> struct aux { enum { value = PositiveInfinity }; };
template <> struct aux<false> { enum { value = NegativeInfinity }; };
template <int x> struct Divide<x, 0> {
  enum { value = aux<(x>0)>::value };
};
template <> struct Divide<0, 0> {
  enum { value = NaN };
};

#include <cstdio>

int main () {
    printf("%d %d %d %d\n", Divide<7,2>::value, Divide<1,0>::value, Divide<0,0>::value, Divide<-1,0>::value);
    return 0;
};
19
kennytm

La correspondance de modèles vous permet de faire correspondre une valeur (ou un objet) à certains modèles pour sélectionner une branche du code. Du point de vue C++, cela peut sembler un peu similaire à l'instruction switch. Dans les langages fonctionnels, la correspondance de motifs peut être utilisée pour faire correspondre des valeurs primitives standard telles que des entiers. Cependant, il est plus utile pour les types composés.

Tout d'abord, démontrons la correspondance des modèles sur les valeurs primitives (en utilisant le pseudo-C++ switch) étendu:

switch(num) {
  case 1: 
    // runs this when num == 1
  case n when n > 10: 
    // runs this when num > 10
  case _: 
    // runs this for all other cases (underscore means 'match all')
}

La deuxième utilisation concerne les types de données fonctionnels tels que tuples (qui vous permettent de stocker plusieurs objets dans une seule valeur) et nions discriminées qui vous permettent de créer un type qui peut contiennent l'une des nombreuses options. Cela ressemble un peu à enum sauf que chaque étiquette peut également contenir des valeurs. Dans une syntaxe pseudo-C++:

enum Shape { 
  Rectangle of { int left, int top, int width, int height }
  Circle of { int x, int y, int radius }
}

Une valeur de type Shape peut désormais contenir soit Rectangle avec toutes les coordonnées, soit un Circle avec le centre et le rayon. La correspondance de motifs vous permet d'écrire une fonction pour travailler avec le type Shape:

switch(shape) { 
  case Rectangle(l, t, w, h): 
    // declares variables l, t, w, h and assigns properties
    // of the rectangle value to the new variables
  case Circle(x, y, r):
    // this branch is run for circles (properties are assigned to variables)
}

Enfin, vous pouvez également utiliser motifs imbriqués qui combinent les deux fonctionnalités. Par exemple, vous pouvez utiliser Circle(0, 0, radius) pour faire correspondre toutes les formes qui ont le centre au point [0, 0] et qui ont n'importe quel rayon (la valeur du rayon sera affectée à la nouvelle variable radius).

Cela peut sembler un peu inconnu du point de vue C++, mais j'espère que mon pseudo-C++ clarifiera l'explication. La programmation fonctionnelle est basée sur des concepts très différents, donc c'est plus logique dans un langage fonctionnel!

7
Tomas Petricek

La correspondance de formes est un peu comme des méthodes surchargées sur les stéroïdes. Le cas le plus simple serait à peu près le même que celui que vous avez vu en Java, les arguments sont une liste de types avec des noms. La méthode correcte à appeler est basée sur les arguments transmis et elle se double d'une affectation de ces arguments au nom du paramètre.

Les modèles vont juste plus loin et peuvent détruire encore plus les arguments transmis. Il peut également potentiellement utiliser des gardes pour correspondre en fonction de la valeur de l'argument. Pour démontrer, je vais faire comme si JavaScript avait une correspondance de motifs.

function foo(a,b,c){} //no pattern matching, just a list of arguments

function foo2([a],{prop1:d,prop2:e}, 35){} //invented pattern matching in JavaScript

Dans foo2, il attend que a soit un tableau, il sépare le deuxième argument, attend un objet avec deux accessoires (prop1, prop2) et attribue les valeurs de ces propriétés aux variables d et e, puis attend que le troisième argument soit 35.

Contrairement à JavaScript, les langages avec correspondance de modèles autorisent généralement plusieurs fonctions portant le même nom, mais des modèles différents. De cette façon, c'est comme une surcharge de méthode. Je vais donner un exemple en erlang:

fibo(0) -> 0 ;
fibo(1) -> 1 ;
fibo(N) when N > 0 -> fibo(N-1) + fibo(N-2) .

Brouillez vos yeux un peu et vous pouvez imaginer cela en javascript. Quelque chose comme ça peut-être:

function fibo(0){return 0;}
function fibo(1){return 1;}
function fibo(N) when N > 0 {return fibo(N-1) + fibo(N-2);}

Le fait étant que lorsque vous appelez fibo, l'implémentation qu'il utilise est basée sur les arguments, mais où Java est limité aux types comme seul moyen de surcharge, la correspondance de modèle peut faire plus.

Au-delà de la surcharge de fonctions comme illustré ici, le même principe peut être appliqué à d'autres endroits, tels que les déclarations de cas ou les évaluations de déstructuration. JavaScript a même ceci en 1.7 .

7
Russell Leggett

La correspondance de modèle est l'endroit où l'interpréteur de votre langue choisira une fonction particulière en fonction de la structure et du contenu des arguments que vous lui donnez.

Ce n'est pas seulement une fonction de langage fonctionnel, mais il est disponible pour de nombreuses langues différentes.

La première fois que j'ai rencontré l'idée, c'est quand j'ai appris le prologue où il est vraiment au cœur de la langue.

par exemple.

last ([LastItem], LastItem).

last ([Head | Tail], LastItem): - last (Tail, LastItem).

Le code ci-dessus donnera le dernier élément d'une liste. L'argument d'entrée est le premier et le résultat est le second.

S'il n'y a qu'un seul élément dans la liste, l'interpréteur choisira la première version et le deuxième argument sera réglé pour égaler le premier, c'est-à-dire qu'une valeur sera affectée au résultat.

Si la liste a à la fois une tête et une queue, l'interprète choisira la deuxième version et répétera jusqu'à ce qu'il ne reste plus qu'un élément dans la liste.

4
charlieb

Pour beaucoup de gens, choisir un nouveau concept est plus facile si quelques exemples simples sont fournis, alors c'est parti:

Disons que vous avez une liste de trois entiers et que vous vouliez ajouter le premier et le troisième élément. Sans correspondance de modèle, vous pouvez le faire comme ceci (exemples en Haskell):

Prelude> let is = [1,2,3]
Prelude> head is + is !! 2
4

Maintenant, bien que ce soit un exemple de jouet, imaginez que nous aimerions lier le premier et le troisième entier aux variables et les additionner:

addFirstAndThird is =
    let first = head is
        third = is !! 3
    in first + third

Cette extraction de valeurs à partir d'une structure de données est ce que fait la correspondance de modèles. Vous "reflétez" essentiellement la structure de quelque chose, en donnant des variables à lier pour les lieux d'intérêt:

addFirstAndThird [first,_,third] = first + third

Lorsque vous appelez cette fonction avec [1,2,3] comme argument, [1,2,3] sera unifié avec [first, _, troisième], liant d'abord à 1, troisième à 3 et rejetant 2 (_ est un espace réservé pour des choses qui ne vous intéressent pas).

Maintenant, si vous vouliez seulement faire correspondre les listes avec 2 comme deuxième élément, vous pouvez le faire comme ceci:

addFirstAndThird [first,2,third] = first + third

Cela ne fonctionnera que pour les listes avec 2 comme deuxième élément et lèvera une exception dans le cas contraire, car aucune définition pour addFirstAndThird n'est donnée pour les listes non correspondantes.

Jusqu'à présent, nous utilisions la correspondance de motifs uniquement pour la liaison de déstructuration. Au-dessus de cela, vous pouvez donner plusieurs définitions de la même fonction, où la première définition correspondante est utilisée, ainsi, la correspondance de modèle est un peu comme "une instruction switch sur les stéréoïdes":

addFirstAndThird [first,2,third] = first + third
addFirstAndThird _ = 0

addFirstAndThird ajoutera volontiers le premier et le troisième élément des listes avec 2 comme deuxième élément, et sinon "tomber" et "retourner" 0. Cette fonctionnalité "de type interrupteur" ne peut pas seulement être utilisée dans les définitions de fonction, par exemple:

Prelude> case [1,3,3] of [a,2,c] -> a+c; _ -> 0
0
Prelude> case [1,2,3] of [a,2,c] -> a+c; _ -> 0
4

De plus, il n'est pas limité aux listes, mais peut également être utilisé avec d'autres types, par exemple en faisant correspondre les constructeurs de valeur Just and Nothing du type Maybe afin de "déballer" la valeur:

Prelude> case (Just 1) of (Just x) -> succ x; Nothing -> 0
2
Prelude> case Nothing of (Just x) -> succ x; Nothing -> 0
0

Bien sûr, ce ne sont que de simples exemples de jouets, et je n'ai même pas essayé de donner une explication formelle ou exhaustive, mais ils devraient suffire à saisir le concept de base.

3
danlei

Vous devriez commencer par page Wikipedia qui donne une assez bonne explication. Ensuite, lisez le chapitre correspondant du wikibook Haskell .

Ceci est une belle définition du wikibook ci-dessus:

La correspondance de motifs est donc un moyen d'attribuer des noms aux choses (ou de lier ces noms à ces choses), et éventuellement de décomposer les expressions en sous-expressions en même temps (comme nous l'avons fait avec la liste dans la définition de la carte).

3
Eli Bendersky

Voici un exemple très court qui montre l'utilité de l'appariement de motifs:

Disons que vous souhaitez trier un élément dans une liste:

["Venice","Paris","New York","Amsterdam"] 

à (j'ai trié "New York")

["Venice","New York","Paris","Amsterdam"] 

dans une langue plus impérative, vous écririez:

function up(city, cities){  
    for(var i = 0; i < cities.length; i++){
        if(cities[i] === city && i > 0){
            var prev = cities[i-1];
            cities[i-1] = city;
            cities[i] = prev;
        }
    }
    return cities;
}

Dans un langage fonctionnel, vous écririez plutôt:

let up list value =  
    match list with
        | [] -> []
        | previous::current::tail when current = value ->  current::previous::tail
        | current::tail -> current::(up tail value)

Comme vous pouvez voir que la solution à motif assorti a moins de bruit, vous pouvez clairement voir quels sont les différents cas et à quel point il est facile de voyager et de déstructurer notre liste.

J'ai écrit un article de blog plus détaillé à ce sujet ici .

2
foobarcode