Quelle est la différence entre la programmation procédurale et la programmation fonctionnelle? [fermé]


247

J'ai lu les articles Wikipedia pour la programmation procédurale et la programmation fonctionnelle , mais je suis toujours légèrement confus. Quelqu'un pourrait-il le réduire au cœur?


Wikipedia implique que FP est un sous-ensemble (c'est-à-dire est toujours) de la programmation déclarative, mais ce n'est pas vrai et confond la taxonomie de l'IP contre le DP .
Shelby Moore III

Réponses:


151

Un langage fonctionnel (idéalement) vous permet d'écrire une fonction mathématique, c'est-à-dire une fonction qui prend n arguments et renvoie une valeur. Si le programme est exécuté, cette fonction est évaluée logiquement au besoin. 1

Un langage procédural, d'autre part, effectue une série d' étapes séquentielles . (Il existe un moyen de transformer la logique séquentielle en logique fonctionnelle appelée style de passage de continuation .)

En conséquence, un programme purement fonctionnel donne toujours la même valeur pour une entrée et l'ordre d'évaluation n'est pas bien défini; ce qui signifie que les valeurs incertaines comme les entrées utilisateur ou les valeurs aléatoires sont difficiles à modéliser dans des langages purement fonctionnels.


1 Comme tout le reste dans cette réponse, c'est une généralisation. Cette propriété, qui évalue un calcul lorsque son résultat est nécessaire plutôt que séquentiellement là où il est appelé, est appelée «paresse». Tous les langages fonctionnels ne sont pas universellement paresseux, ni la paresse limitée à la programmation fonctionnelle. La description donnée ici fournit plutôt un «cadre mental» pour penser à différents styles de programmation qui ne sont pas des catégories distinctes et opposées mais plutôt des idées fluides.


9
Les valeurs incertaines telles que les entrées utilisateur ou les valeurs aléatoires sont difficiles à modéliser dans des langages purement fonctionnels, mais c'est un problème résolu. Voir les monades.
Apocalisp

« étapes séquentielles , où le programme fonctionnel serait imbriqué» signifie prévoir une séparation des préoccupations en mettant l'accent sur la composition des fonctions , c'est-à-dire séparer les dépendances entre les sous-calculs d'un calcul déterministe.
Shelby Moore III

cela semble faux - les procédures peuvent également être imbriquées, les procédures peuvent avoir des paramètres
Hurda

1
@Hurda Oui, aurait pu mieux formuler cela. Le fait est que la programmation procédurale se fait pas à pas dans un ordre prédéterminé, tandis que les programmes fonctionnels ne sont pas exécutés pas à pas; les valeurs sont plutôt calculées lorsqu'elles sont nécessaires. Cependant, l'absence d'une définition généralement acceptée de la terminologie de programmation rend ces généralisations presque inutiles. J'ai modifié ma réponse à cet égard.
Konrad Rudolph

97

Fondamentalement, les deux styles sont comme le Yin et le Yang. L'un est organisé, l'autre chaotique. Il y a des situations où la programmation fonctionnelle est le choix évident, et d'autres situations où la programmation procédurale est le meilleur choix. C'est pourquoi il y a au moins deux langues qui ont récemment sorti une nouvelle version, qui embrasse les deux styles de programmation. ( Perl 6 et D 2 )

De procédure:

  • La sortie d'une routine n'a pas toujours une corrélation directe avec l'entrée.
  • Tout se fait dans un ordre précis.
  • L'exécution d'une routine peut avoir des effets secondaires.
  • A tendance à mettre en œuvre des solutions de manière linéaire.

Perl 6

sub factorial ( UInt:D $n is copy ) returns UInt {

  # modify "outside" state
  state $call-count++;
  # in this case it is rather pointless as
  # it can't even be accessed from outside

  my $result = 1;

  loop ( ; $n > 0 ; $n-- ){

    $result *= $n;

  }

  return $result;
}

D 2

int factorial( int n ){

  int result = 1;

  for( ; n > 0 ; n-- ){
    result *= n;
  }

  return result;
}

Fonctionnel:

  • Souvent récursif.
  • Renvoie toujours la même sortie pour une entrée donnée.
  • L'ordre d'évaluation n'est généralement pas défini.
  • Doit être apatride. c'est-à-dire qu'aucune opération ne peut avoir d'effets secondaires.
  • Bon ajustement pour une exécution parallèle
  • A tendance à mettre l'accent sur une approche diviser pour mieux régner.
  • Peut avoir la fonction d'évaluation paresseuse.

Haskell

(copié de Wikipedia );

fac :: Integer -> Integer

fac 0 = 1
fac n | n > 0 = n * fac (n-1)

ou en une seule ligne:

fac n = if n > 0 then n * fac (n-1) else 1

Perl 6

proto sub factorial ( UInt:D $n ) returns UInt {*}

multi sub factorial (  0 ) { 1 }
multi sub factorial ( $n ) { $n * samewith $n-1 } # { $n * factorial $n-1 }

D 2

pure int factorial( invariant int n ){
  if( n <= 1 ){
    return 1;
  }else{
    return n * factorial( n-1 );
  }
}

Note latérale:

Factorial est en fait un exemple courant pour montrer à quel point il est facile de créer de nouveaux opérateurs en Perl 6 de la même manière que vous créeriez un sous-programme. Cette fonctionnalité est tellement enracinée dans Perl 6 que la plupart des opérateurs de l'implémentation Rakudo sont définis de cette façon. Il vous permet également d'ajouter vos propres candidats multiples aux opérateurs existants.

sub postfix:< ! > ( UInt:D $n --> UInt )
  is tighter(&infix:<*>)
  { [*] 2 .. $n }

say 5!; # 120␤

Cet exemple montre également la création de plage ( 2..$n) et le méta-opérateur de réduction de liste ( [ OPERATOR ] LIST) combinés avec l'opérateur de multiplication d'infixe numérique. ( *)
Cela montre également que vous pouvez mettre --> UIntla signature au lieu de l' returns UIntajouter.

(Vous pouvez vous en sortir en commençant la plage avec 2car "multiplier" retournera 1lorsqu'il sera appelé sans aucun argument)


Bonjour, pouvez-vous s'il vous plaît fournir un exemple pour les 2 points suivants mentionnés pour "Procédure" en considérant l'exemple d'implémentation factorielle en Perl 6. 1) La sortie d'une routine n'a pas toujours une corrélation directe avec l'entrée. 2) L'exécution d'une routine peut avoir des effets secondaires.
Naga Kiran

sub postfix:<!> ($n) { [*] 1..$n }
Brad Gilbert

@BradGilbert -Pouvez- No operation can have side effectsvous le développer?
kushalvm

2
Probablement la meilleure réponse que j'ai pu trouver ... Et j'ai fait des recherches sur ces points individuels .. qui m'ont vraiment aidé! :)
Navaneeth

1
@AkashBisariya sub foo( $a, $b ){ ($a,$b).pick }← ne renvoie pas toujours la même sortie pour la même entrée, contrairement à ce qui suitsub foo( $a, $b ){ $a + $b }
Brad Gilbert

70

Je n'ai jamais vu cette définition donnée ailleurs, mais je pense que cela résume assez bien les différences données ici:

La programmation fonctionnelle se concentre sur les expressions

La programmation procédurale se concentre sur les déclarations

Les expressions ont des valeurs. Un programme fonctionnel est une expression dont la valeur est une séquence d'instructions à exécuter par l'ordinateur.

Les instructions n'ont pas de valeurs et modifient à la place l'état d'une machine conceptuelle.

Dans un langage purement fonctionnel, il n'y aurait pas de déclarations, dans le sens où il n'y a aucun moyen de manipuler l'état (elles pourraient toujours avoir une construction syntaxique nommée "déclaration", mais à moins qu'elle ne manipule l'état, je ne l'appellerais pas une déclaration dans ce sens ). Dans un langage purement procédural, il n'y aurait pas d'expressions, tout serait une instruction qui manipule l'état de la machine.

Haskell serait un exemple de langage purement fonctionnel car il n'y a aucun moyen de manipuler l'état. Le code machine serait un exemple de langage purement procédural car tout dans un programme est une instruction qui manipule l'état des registres et de la mémoire de la machine.

La partie déroutante est que la grande majorité des langages de programmation contiennent à la fois expressions et des instructions, vous permettant de mélanger les paradigmes. Les langages peuvent être classés comme plus fonctionnels ou plus procéduraux en fonction de la mesure dans laquelle ils encouragent l'utilisation des déclarations par rapport aux expressions.

Par exemple, C serait plus fonctionnel que COBOL car un appel de fonction est une expression, alors que l'appel d'un sous-programme dans COBOL est une instruction (qui manipule l'état des variables partagées et ne renvoie pas de valeur). Python serait plus fonctionnel que C car il vous permet d'exprimer la logique conditionnelle comme une expression en utilisant une évaluation de court-circuit (test && path1 || path2 par opposition aux instructions if). Le schéma serait plus fonctionnel que Python car tout dans le schéma est une expression.

Vous pouvez toujours écrire dans un style fonctionnel dans une langue qui encourage le paradigme procédural et vice versa. Il est juste plus difficile et / ou plus gênant d'écrire dans un paradigme qui n'est pas encouragé par la langue.


2
La meilleure et la plus succincte explication que j'ai vue sur le web, bravo!
2018

47

En informatique, la programmation fonctionnelle est un paradigme de programmation qui traite le calcul comme l'évaluation des fonctions mathématiques et évite les données d'état et mutables. Il met l'accent sur l'application des fonctions, contrairement au style de programmation procédurale qui met l'accent sur les changements d'état.


4
Bien que ce soit l'explication qui m'a le plus aidé, je reste floue sur le concept de programmation fonctionnelle. Je recherche un style de programmation qui ne dépend pas du référencement d'objets externes pour s'exécuter (tout ce que la fonction doit exécuter doit être passé en paramètre). Par exemple, je ne mettrais jamais GetUserContext()la fonction, le contexte utilisateur serait passé. Est-ce une programmation fonctionnelle? Merci d'avance.
Matt Cashatt

26

Je crois que la programmation procédurale / fonctionnelle / objective concerne la façon d'aborder un problème.

Le premier style planifierait tout en étapes et résoudrait le problème en implémentant une étape (une procédure) à la fois. D'un autre côté, la programmation fonctionnelle mettrait l'accent sur l'approche diviser pour mieux régner, où le problème est divisé en sous-problèmes, puis chaque sous-problème est résolu (création d'une fonction pour résoudre ce sous-problème) et les résultats sont combinés pour créer la réponse à l'ensemble du problème. Enfin, la programmation objective imiterait le monde réel en créant un mini-monde à l'intérieur de l'ordinateur avec de nombreux objets, chacun ayant des caractéristiques (quelque peu) uniques, et interagissant avec les autres. De ces interactions, le résultat émergerait.

Chaque style de programmation a ses propres avantages et faiblesses. Par conséquent, faire quelque chose comme de la "programmation pure" (c'est-à-dire purement procédurale - personne ne le fait d'ailleurs, ce qui est un peu bizarre - ou purement fonctionnel ou purement objectif) est très difficile, voire impossible, à l'exception de quelques problèmes élémentaires spécialement conçu pour démontrer l'avantage d'un style de programmation (par conséquent, nous appelons ceux qui aiment la pureté "weenie": D).

Ensuite, à partir de ces styles, nous avons des langages de programmation conçus pour être optimisés pour certains de chaque style. Par exemple, l'assemblage est une question de procédure. D'accord, la plupart des premiers langages sont procéduraux, pas seulement Asm, comme C, Pascal, (et Fortran, j'ai entendu). Ensuite, nous avons tous Java célèbre dans l'école objective (En fait, Java et C # est également dans une classe appelée "orientée vers l'argent", mais qui fait l'objet d'une autre discussion). L'objectif est également Smalltalk. Dans une école fonctionnelle, nous aurions «presque fonctionnel» (certains les considéraient comme impurs) la famille Lisp et la famille ML et de nombreux Haskell, Erlang «purement fonctionnels», etc. Soit dit en passant, il existe de nombreux langages généraux tels que Perl, Python , Ruby.


26

Programmation fonctionnelle

num = 1 
def function_to_add_one(num):
    num += 1
    return num


function_to_add_one(num)
function_to_add_one(num)
function_to_add_one(num)
function_to_add_one(num)
function_to_add_one(num)

#Final Output: 2

Programmation procédurale

num = 1 
def procedure_to_add_one():
    global num
    num += 1
    return num


procedure_to_add_one()
procedure_to_add_one()
procedure_to_add_one()
procedure_to_add_one()
procedure_to_add_one()

#Final Output: 6

function_to_add_one est une fonction

procedure_to_add_one est une procédure

Même si vous exécutez la fonction cinq fois, chaque fois qu'elle reviendra 2

Si vous exécutez la procédure cinq fois, à la fin de la cinquième exécution, cela vous donnera 6 .


5
cet exemple est vraiment simple à comprendre le terme "sans état" et "données immuables" dans la programmation fonctionnelle, la lecture de toutes les définitions et différences énumérées ci-dessus n'a pas effacé ma confusion jusqu'à la lecture de cette réponse. Je vous remercie!
maximus

13

Pour développer le commentaire de Konrad:

En conséquence, un programme purement fonctionnel donne toujours la même valeur pour une entrée et l'ordre d'évaluation n'est pas bien défini;

Pour cette raison, le code fonctionnel est généralement plus facile à paralléliser. Puisqu'il n'y a (généralement) aucun effet secondaire des fonctions, et qu'elles (généralement) agissent simplement sur leurs arguments, beaucoup de problèmes de concurrence disparaissent.

La programmation fonctionnelle est également utilisée lorsque vous devez être en mesure de prouver que votre code est correct. C'est beaucoup plus difficile à faire avec la programmation procédurale (pas facile avec fonctionnel, mais toujours plus facile).

Avertissement: je n'ai pas utilisé de programmation fonctionnelle depuis des années, et je n'ai recommencé à le regarder que récemment, donc je ne suis peut-être pas complètement correct ici. :)


12

Une chose que je n'avais pas vue vraiment soulignée ici est que les langages fonctionnels modernes tels que Haskell sont vraiment plus sur les fonctions de première classe pour le contrôle de flux que la récursivité explicite. Vous n'avez pas besoin de définir récursivement factorielle dans Haskell, comme cela a été fait ci-dessus. Je pense que quelque chose comme

fac n = foldr (*) 1 [1..n]

est une construction parfaitement idiomatique, et beaucoup plus proche dans l'esprit d'utiliser une boucle que d'utiliser une récursivité explicite.


10

Une programmation fonctionnelle est identique à une programmation procédurale dans laquelle les variables globales ne sont pas utilisées.


7

Les langages procéduraux ont tendance à garder une trace de l'état (à l'aide de variables) et ont tendance à s'exécuter comme une séquence d'étapes. Les langages purement fonctionnels ne gardent pas la trace de l'état, utilisent des valeurs immuables et ont tendance à s'exécuter comme une série de dépendances. Dans de nombreux cas, le statut de la pile d'appels contiendra les informations qui seraient équivalentes à celles qui seraient stockées dans des variables d'état dans le code de procédure.

La récursivité est un exemple classique de programmation de style fonctionnel.


1
Après avoir lu cette page, je pensais à la même chose -> "La récursivité est un exemple classique de programmation de style fonctionnel", et vous l'avez effacé. Merci, maintenant je pense que je reçois quelque chose.
Mudassir Hussain

6

Konrad a déclaré:

En conséquence, un programme purement fonctionnel donne toujours la même valeur pour une entrée et l'ordre d'évaluation n'est pas bien défini; ce qui signifie que les valeurs incertaines comme les entrées utilisateur ou les valeurs aléatoires sont difficiles à modéliser dans des langages purement fonctionnels.

L'ordre d'évaluation dans un programme purement fonctionnel peut être difficile (euh) à raisonner (en particulier avec la paresse) ou même sans importance, mais je pense que dire qu'il n'est pas bien défini donne l'impression que vous ne pouvez pas dire si votre programme va travailler du tout!

Une meilleure explication serait peut-être que le flux de contrôle dans les programmes fonctionnels est basé sur le moment où la valeur des arguments d'une fonction est nécessaire. La bonne chose à ce sujet dans les programmes bien écrits, l'état devient explicite: chaque fonction répertorie ses entrées en tant que paramètres au lieu de fusionner arbitrairement l'état global. Donc, à un certain niveau, il est plus facile de raisonner sur l'ordre d'évaluation par rapport à une fonction à la fois . Chaque fonction peut ignorer le reste de l'univers et se concentrer sur ce qu'elle doit faire. Lorsqu'elles sont combinées, les fonctions sont garanties de fonctionner de la même façon [1] qu'elles le feraient isolément.

... les valeurs incertaines comme les entrées utilisateur ou les valeurs aléatoires sont difficiles à modéliser dans des langages purement fonctionnels.

La solution au problème d'entrée dans les programmes purement fonctionnels est d'incorporer un langage impératif en tant que DSL en utilisant une abstraction suffisamment puissante . Dans les langages impératifs (ou fonctionnels non purs), cela n'est pas nécessaire car vous pouvez "tricher" et passer l'état implicitement et l'ordre d'évaluation est explicite (que cela vous plaise ou non). En raison de cette "tricherie" et de l'évaluation forcée de tous les paramètres de chaque fonction, dans les langages impératifs 1) vous perdez la possibilité de créer vos propres mécanismes de flux de contrôle (sans macros), 2) le code n'est pas intrinsèquement sûr pour les threads et / ou parallélisable par défaut, 3) et la mise en œuvre de quelque chose comme annuler (voyage dans le temps) nécessite un travail minutieux (le programmeur doit impérativement stocker une recette pour récupérer les anciennes valeurs!), Tandis que la programmation fonctionnelle pure vous achète toutes ces choses - et quelques autres je peux ont oublié - "gratuitement".

J'espère que cela ne ressemble pas à du fanatisme, je voulais juste ajouter un peu de perspective. La programmation impérative et en particulier la programmation de paradigmes mixtes dans des langages puissants comme C # 3.0 sont toujours des moyens totalement efficaces pour faire avancer les choses et il n'y a pas de solution miracle .

[1] ... sauf éventuellement en ce qui concerne l'utilisation de la mémoire (cf. foldl et foldl 'dans Haskell).


5

Pour développer le commentaire de Konrad:

et l'ordre d'évaluation n'est pas bien défini

Certains langages fonctionnels ont ce qu'on appelle l'évaluation paresseuse. Ce qui signifie qu'une fonction n'est pas exécutée tant que la valeur n'est pas requise. Jusqu'à ce moment, c'est la fonction elle-même qui est transmise.

Les langues procédurales sont l'étape 1 étape 2 étape 3 ... si à l'étape 2 vous dites ajouter 2 + 2, il le fait tout de suite. Dans une évaluation paresseuse, vous diriez ajouter 2 + 2, mais si le résultat n'est jamais utilisé, il ne fait jamais l'ajout.


4

Si vous en avez l'occasion, je vous recommande d'obtenir une copie de Lisp / Scheme et de faire quelques projets dedans. La plupart des idées qui sont devenues ces derniers temps des bandwagons ont été exprimées en Lisp il y a des décennies: programmation fonctionnelle, continuations (comme fermetures), garbage collection, même XML.

Ce serait donc un bon moyen de prendre une longueur d'avance sur toutes ces idées actuelles, et quelques autres en plus, comme le calcul symbolique.

Vous devez savoir à quoi sert la programmation fonctionnelle et à quoi elle ne l'est pas. Ce n'est pas bon pour tout. Certains problèmes sont mieux exprimés en termes d'effets secondaires, où la même question donne des réponses différentes selon le moment où elle est posée.


3

@Creighton:

Dans Haskell, il existe une fonction de bibliothèque appelée produit :

prouduct list = foldr 1 (*) list

ou simplement:

product = foldr 1 (*)

donc la factorielle "idiomatique"

fac n = foldr 1 (*)  [1..n]

serait tout simplement

fac n = product [1..n]

Cela ne fournit pas de réponse à la question. Pour critiquer ou demander des éclaircissements à un auteur, laissez un commentaire sous son article.
Nick Kitto

Je crois que cela a été publié il y a de nombreuses années, avant l'ajout du système de commentaires, si vous pouvez le croire: stackoverflow.com/help/badges/30/beta?userid=2543
Jared Updike

2

La programmation procédurale divise les séquences d'instructions et les constructions conditionnelles en blocs séparés appelés procédures paramétrées sur des arguments qui sont des valeurs (non fonctionnelles).

La programmation fonctionnelle est la même, sauf que les fonctions sont des valeurs de première classe, elles peuvent donc être passées comme arguments à d'autres fonctions et renvoyées comme résultats d'appels de fonction.

Notez que la programmation fonctionnelle est une généralisation de la programmation procédurale dans cette interprétation. Cependant, une minorité interprète la «programmation fonctionnelle» comme signifiant sans effets secondaires, ce qui est assez différent mais sans pertinence pour tous les principaux langages fonctionnels, sauf Haskell.


1

Pour comprendre la différence, il faut comprendre que le paradigme "le parrain" de la programmation procédurale et fonctionnelle est la programmation impérative .

Fondamentalement, la programmation procédurale n'est qu'un moyen de structurer des programmes impératifs dans lesquels la principale méthode d'abstraction est la «procédure». (ou "fonction" dans certains langages de programmation). Même la programmation orientée objet est juste une autre façon de structurer un programme impératif, où l'état est encapsulé dans des objets, devenant un objet avec un "état actuel", plus cet objet a un ensemble de fonctions, de méthodes et d'autres choses qui vous permettent de le programmeur manipule ou met à jour l'état.

Maintenant, en ce qui concerne la programmation fonctionnelle, l' essentiel dans son approche est qu'il identifie les valeurs à prendre et comment ces valeurs doivent être transférées. (il n'y a donc pas d'état et pas de données mutables car il prend les fonctions comme valeurs de première classe et les transmet comme paramètres à d'autres fonctions).

PS: comprendre chaque paradigme de programmation utilisé devrait clarifier les différences entre chacun d'eux.

PSS: En fin de compte, les paradigmes de programmation ne sont que des approches différentes pour résoudre les problèmes.

PSS: cette réponse quora a une grande explication.


0

Aucune des réponses ici ne montre une programmation fonctionnelle idiomatique. La réponse factorielle récursive est idéale pour représenter la récursivité en FP, mais la majorité du code n'est pas récursif, donc je ne pense pas que cette réponse soit pleinement représentative.

Supposons que vous ayez un tableau de chaînes et que chaque chaîne représente un entier comme "5" ou "-200". Vous souhaitez vérifier ce tableau d'entrée de chaînes par rapport à votre scénario de test interne (à l'aide de la comparaison d'entiers). Les deux solutions sont présentées ci-dessous

De procédure

arr_equal(a : [Int], b : [Str]) -> Bool {
    if(a.len != b.len) {
        return false;
    }

    bool ret = true;
    for( int i = 0; i < a.len /* Optimized with && ret*/; i++ ) {
        int a_int = a[i];
        int b_int = parseInt(b[i]);
        ret &= a_int == b_int;  
    }
    return ret;
}

Fonctionnel

eq = i, j => i == j # This is usually a built-in
toInt = i => parseInt(i) # Of course, parseInt === toInt here, but this is for visualization

arr_equal(a : [Int], b : [Str]) -> Bool =
    zip(a, b.map(toInt)) # Combines into [Int, Int]
   .map(eq)
   .reduce(true, (i, j) => i && j) # Start with true, and continuously && it with each value

Alors que les langages fonctionnels purs sont généralement des langages de recherche (comme le monde réel aime les effets secondaires gratuits), les langages procéduraux du monde réel utiliseront la syntaxe fonctionnelle beaucoup plus simple, le cas échéant.

Ceci est généralement implémenté avec une bibliothèque externe comme Lodash , ou disponible intégré avec des langages plus récents comme Rust . La levée de la programmation fonctionnelle lourde se fait avec des fonctions / concepts tels que map, filter, reduce, currying,partial , les trois derniers dont vous pouvez consulter pour une meilleure compréhension.

Addenda

Pour être utilisé dans la nature, le compilateur devra normalement déterminer comment convertir la version fonctionnelle en version procédurale en interne, car la surcharge d'appel de fonction est trop élevée. Les cas récursifs tels que la factorielle illustrée utiliseront des astuces comme l' appel de queue pour supprimer l'utilisation de la mémoire O (n). Le fait qu'il n'y ait pas d'effets secondaires permet aux compilateurs fonctionnels d'implémenter l' && retoptimisation même lorsque la .reducedernière est effectuée. L'utilisation de Lodash dans JS ne permet évidemment aucune optimisation, c'est donc un coup sûr pour les performances (ce qui n'est généralement pas un problème avec le développement web). Des langages comme Rust optimiseront en interne (et auront des fonctions telles que l' try_foldassistance à l' && retoptimisation).

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.