Pensée fonctionnelle. Partie 7

Nous continuons notre série d'articles sur la programmation fonctionnelle en F #. Aujourd'hui, nous avons un sujet très intéressant: la définition des fonctions. Y compris, parlons des fonctions anonymes, des fonctions sans paramètres, des fonctions récursives, des combinateurs et bien plus encore. Regardez sous le chat!




Définition de fonction


Nous savons déjà comment créer des fonctions régulières en utilisant la syntaxe "let":


let add xy = x + y 

Dans cet article, nous examinerons d'autres façons de créer des fonctions, ainsi que des conseils pour les définir.


Fonctions anonymes (lambdas)


Si vous connaissez les lambdas dans d'autres langues, les paragraphes suivants vous sembleront familiers. Les fonctions anonymes (ou «expressions lambda») sont définies comme suit:


 fun parameter1 parameter2 etc -> expression 

Par rapport aux lambdas de C #, il existe deux différences:


  • lambdas devrait commencer par le mot-clé fun , qui n'est pas requis en C #
  • une seule flèche -> utilisée -> , au lieu de double => de C #.

Définition lambda de la fonction d'addition:


 let add = fun xy -> x + y 

Même fonction sous forme traditionnelle:


 let add xy = x + y 

Les lambdas sont souvent utilisés sous la forme de petites expressions ou lorsqu'il n'y a pas de désir de définir une fonction distincte pour une expression. Comme vous l'avez déjà vu, lorsque vous travaillez avec des listes, ce n'est pas rare.


 //    let add1 i = i + 1 [1..10] |> List.map add1 //        [1..10] |> List.map (fun i -> i + 1) 

Notez que les parenthèses doivent être utilisées autour des lambdas.


Les lambdas sont également utilisés lorsqu'une fonction clairement différente est nécessaire. Par exemple, le " adderGenerator " discuté précédemment, dont nous avons discuté plus tôt, peut être réécrit en utilisant lambdas.


 //   let adderGenerator x = (+) x //     let adderGenerator x = fun y -> x + y 

La version lambda est un peu plus longue, mais indique immédiatement qu'une fonction intermédiaire sera retournée.


Les lambdas peuvent être imbriqués. Un autre exemple de définition adderGenerator , cette fois uniquement sur lambdas.


 let adderGenerator = fun x -> (fun y -> x + y) 

Êtes-vous clair que les trois définitions sont équivalentes?


 let adderGenerator1 xy = x + y let adderGenerator2 x = fun y -> x + y let adderGenerator3 = fun x -> (fun y -> x + y) 

Sinon, relisez le chapitre sur le curry . C'est très important pour comprendre!


Correspondance de motifs


Lorsqu'une fonction est définie, il est possible de lui passer explicitement des paramètres, comme dans les exemples ci-dessus, mais il est également possible de comparer avec un modèle directement dans la section des paramètres. En d'autres termes, la section des paramètres peut contenir des modèles (modèles correspondants), et pas seulement des identifiants!


L'exemple suivant illustre l'utilisation de modèles dans une définition de fonction:


 type Name = {first:string; last:string} //    let bob = {first="bob"; last="smith"} //   //     let f1 name = //   let {first=f; last=l} = name //     printfn "first=%s; last=%s" fl //   let f2 {first=f; last=l} = //        printfn "first=%s; last=%s" fl //  f1 bob f2 bob 

Ce type de comparaison ne peut se produire que lorsque la correspondance est toujours décidable. Par exemple, vous ne pouvez pas faire correspondre les types d'union et les listes de cette manière, car certains cas ne peuvent pas être mis en correspondance.


 let f3 (x::xs) = //       printfn "first element is=%A" x 

Le compilateur donnera un avertissement sur la correspondance incomplète (une liste vide provoquera une erreur d'exécution à l'entrée de cette fonction).


Erreur courante: tuples vs de nombreux paramètres


Si vous venez d'un langage de type C, le tuple utilisé comme seul argument de la fonction peut douloureusement ressembler à une fonction multi-paramètres. Mais ce n'est pas la même chose! Comme je l'ai noté plus tôt, si vous voyez une virgule, c'est probablement un tuple. Les paramètres sont séparés par des espaces.


Exemple de confusion:


 //      let addTwoParams xy = x + y //      -  let addTuple aTuple = let (x,y) = aTuple x + y //         //        let addConfusingTuple (x,y) = x + y 

  • La première définition, " addTwoParams ", prend deux paramètres, séparés par un espace.
  • La deuxième définition, " addTuple ", prend un paramètre. Ce paramètre lie "x" et "y" à partir du tuple et les additionne.
  • La troisième définition, " addConfusingTuple ", prend un seul paramètre comme " addTuple ", mais l'astuce est que ce tuple est décompressé (adapté au modèle) et lié dans le cadre de la définition du paramètre à l'aide de la correspondance de modèle. Dans les coulisses, tout se passe exactement de la même manière que dans addTuple .

Regardons les signatures (regardez-les toujours si vous n'êtes pas sûr de quelque chose).


 val addTwoParams : int -> int -> int //   val addTuple : int * int -> int // tuple->int val addConfusingTuple : int * int -> int // tuple->int 

Et maintenant ici:


 // addTwoParams 1 2 // ok --      addTwoParams (1,2) // error -     // => error FS0001: This expression was expected to have type // int but here has type 'a * 'b 

Ici, nous voyons une erreur dans le deuxième appel.


Premièrement, le compilateur traite (1,2) comme un tuple généralisé de la forme ('a * 'b) , qu'il essaie de passer comme premier paramètre à addTwoParams . Après quoi, il se plaint que le premier paramètre addTwoParams pas int , mais une tentative a été faite pour passer un tuple.


Pour faire un tuple, utilisez une virgule!


 addTuple (1,2) // ok addConfusingTuple (1,2) // ok let x = (1,2) addTuple x // ok let y = 1,2 //  , //  ! addTuple y // ok addConfusingTuple y // ok 

Et vice versa, si vous passez plusieurs arguments à une fonction en attente d'un tuple, vous obtenez également une erreur incompréhensible.


 addConfusingTuple 1 2 // error --          // => error FS0003: This value is not a function and // cannot be applied 

Cette fois, le compilateur a décidé qu'une fois deux arguments addConfusingTuple , addConfusingTuple devrait être curry. Et l'entrée " addConfusingTuple 1 " est une application partielle et doit renvoyer une fonction intermédiaire. Tenter d'appeler cette fonction intermédiaire avec le paramètre "2" générera une erreur, car il n'y a pas de fonction intermédiaire! Nous voyons la même erreur que dans le chapitre sur le curry, où nous avons discuté des problèmes avec trop de paramètres.


Pourquoi ne pas utiliser des tuples comme paramètres?


La discussion des tuples ci-dessus montre une autre façon de définir des fonctions avec de nombreux paramètres: au lieu de les passer séparément, tous les paramètres peuvent être assemblés en une seule structure. Dans l'exemple ci-dessous, la fonction prend un seul paramètre - un tuple de trois éléments.


 let f (x,y,z) = x + y * z //  - int * int * int -> int //  f (1,2,3) 

Il est à noter que la signature est différente de la signature d'une fonction à trois paramètres. Il n'y a qu'une flèche, un paramètre et des astérisques pointant vers le tuple (int*int*int) .


Quand il est nécessaire de soumettre des arguments avec des paramètres séparés, et quand un tuple?


  • Quand les tuples sont importants en eux-mêmes. Par exemple, pour des opérations dans un espace tridimensionnel, les triples tuples seront plus pratiques que trois coordonnées séparément.
  • Parfois, les tuples sont utilisés pour combiner des données qui doivent être stockées ensemble dans une seule structure. Par exemple, les méthodes TryParse de la bibliothèque .NET renvoient le résultat et une variable booléenne en tant que tuple. Mais pour stocker une grande quantité de données liées, il est préférable de définir une classe ou un enregistrement ( record .

Cas particulier: Tuples et fonctions de la bibliothèque .NET


Lors de l'appel de bibliothèques .NET, les virgules sont très courantes!


Ils acceptent tous les tuples, et les appels se ressemblent comme en C #:


 //  System.String.Compare("a","b") //   System.String.Compare "a" "b" 

La raison en est que les fonctions du .NET classique ne sont pas analysées et ne peuvent pas être partiellement appliquées. Tous les paramètres doivent toujours être transmis immédiatement, et la manière la plus évidente est d'utiliser un tuple.


Notez que ces appels ne ressemblent qu'à transférer des tuples, mais c'est en fait un cas spécial. Vous ne pouvez pas passer de vrais tuples dans de telles fonctions:


 let tuple = ("a","b") System.String.Compare tuple // error System.String.Compare "a","b" // error 

Si vous souhaitez appliquer partiellement les fonctions .NET, écrivez simplement des wrappers dessus, comme cela a été fait précédemment , ou comme indiqué ci-dessous:


 //    let strCompare xy = System.String.Compare(x,y) //    let strCompareWithB = strCompare "B" //      ["A";"B";"C"] |> List.map strCompareWithB 

Guide de sélection des paramètres individuels et groupés


La discussion des tuples mène à un sujet plus général: quand les paramètres doivent-ils être séparés et quand ils sont regroupés?


Vous devez faire attention à la façon dont F # diffère de C # à cet égard. En C #, tous les paramètres sont toujours passés, donc cette question ne se pose même pas là! En F #, en raison d'une application partielle, seuls certains paramètres peuvent être représentés, il est donc nécessaire de faire la distinction entre le cas où les paramètres doivent être combinés et le cas où ils sont indépendants.


Recommandations générales sur la façon de structurer les paramètres lors de la conception de vos propres fonctions.


  • Dans le cas général, il est toujours préférable d'utiliser des paramètres séparés au lieu de passer une structure, que ce soit un tuple ou un enregistrement. Cela permet un comportement plus flexible, comme une application partielle.
  • Mais, lorsqu'un groupe de paramètres doit être transmis à la fois, une sorte de mécanisme de regroupement doit être utilisé.

En d'autres termes, lorsque vous développez une fonction, demandez-vous: «Puis-je fournir ce paramètre séparément?» Si la réponse est non, les paramètres doivent être regroupés.


Regardons quelques exemples:


 //     . //      ,       let add xy = x + y //         //      ,    let locateOnMap (xCoord,yCoord) = //  //      //      -     type CustomerName = {First:string; Last:string} let setCustomerName aCustomerName = //  let setCustomerName first last = //   //     //     //    ,     let setCustomerName myCredentials aName = // 

Enfin, assurez-vous que l'ordre des paramètres aidera dans une application partielle (voir le manuel ici ). Par exemple, pourquoi ai-je mis myCredentials avant aName dans la dernière fonction?


Fonctions sans paramètres


Parfois, vous pouvez avoir besoin d'une fonction qui n'accepte aucun paramètre. Par exemple, vous avez besoin de la fonction "hello world" qui peut être appelée plusieurs fois. Comme indiqué dans la section précédente, la définition naïve ne fonctionne pas.


 let sayHello = printfn "Hello World!" //      

Mais cela peut être corrigé en ajoutant un paramètre d'unité à la fonction ou en utilisant un lambda.


 let sayHello() = printfn "Hello World!" //  let sayHello = fun () -> printfn "Hello World!" //  

Après cela, la fonction doit toujours être appelée avec l'argument unit :


 //  sayHello() 

Ce qui se produit assez souvent lors de l'interaction avec les bibliothèques .NET:


 Console.ReadLine() System.Environment.GetCommandLineArgs() System.IO.Directory.GetCurrentDirectory() 

N'oubliez pas, appelez-les avec les paramètres de l' unit !


Définir de nouveaux opérateurs


Vous pouvez définir des fonctions en utilisant un ou plusieurs caractères d'opérateur (voir la documentation pour une liste de caractères):


 //  let (.*%) xy = x + y + 1 

Vous devez utiliser des parenthèses autour des caractères pour définir la fonction.


Les opérateurs commençant par * nécessitent un espace entre les parenthèses et * , car en F # (* agit comme le début d'un commentaire (comme /*...*/ en C #):


 let ( *+* ) xy = x + y + 1 

Une fois définie, une nouvelle fonction peut être utilisée de la manière habituelle si elle est placée entre crochets:


 let result = (.*%) 2 3 

Si la fonction est utilisée avec deux paramètres, vous pouvez utiliser l'enregistrement d'opérateur d'infixe sans parenthèses.


 let result = 2 .*% 3 

Vous pouvez également définir des opérateurs de préfixe commençant par ! ou ~ (avec quelques restrictions, voir la documentation )


 let (~%%) (s:string) = s.ToCharArray() // let result = %% "hello" 

En F #, la définition des instructions est une opération assez courante, et de nombreuses bibliothèques exportent des instructions avec des noms comme >=> et <*> .


Style sans point


Nous avons déjà vu de nombreux exemples de fonctions qui n'avaient pas les derniers paramètres pour réduire le niveau de chaos. Ce style est appelé style sans point ou programmation tacite .


Voici quelques exemples:


 let add xy = x + y //  let add x = (+) x // point free let add1Times2 x = (x + 1) * 2 //  let add1Times2 = (+) 1 >> (*) 2 // point free let sum list = List.reduce (fun sum e -> sum+e) list //  let sum = List.reduce (+) // point free 

Ce style a ses avantages et ses inconvénients.


L'un des avantages est que l'accent est mis sur la composition de fonctions d'ordre supérieur au lieu de s'occuper d'objets de bas niveau. Par exemple, " (+) 1 >> (*) 2 " est un ajout explicite suivi d'une multiplication. Et " List.reduce (+) " indique clairement que l'opération d'ajout est importante, quelles que soient les informations de la liste.


Un style inutile vous permet de vous concentrer sur l'algorithme de base et d'identifier les caractéristiques communes dans le code. La fonction " reduce " utilisée ci-dessus en est un bon exemple. Ce sujet sera abordé dans une série prévue sur le traitement des listes.


D'un autre côté, une utilisation excessive d'un tel style peut rendre le code obscur. Les paramètres explicites font office de documentation et leurs noms (tels que "liste") facilitent la compréhension de ce que fait la fonction.


Comme tout dans la programmation, la meilleure recommandation est de préférer l'approche qui offre le plus de clarté.


Combinateurs


Les " combinateurs " sont appelés fonctions dont le résultat ne dépend que de leurs paramètres. Cela signifie qu'il n'y a pas de dépendance vis-à-vis du monde extérieur et, en particulier, qu'aucune autre fonction ou valeur globale ne peut les affecter.


En pratique, cela signifie que les fonctions combinatoires sont limitées par une combinaison de leurs paramètres de diverses manières.


Nous avons déjà vu plusieurs combinateurs: un tuyau et un opérateur de composition. Si vous regardez leurs définitions, il est clair qu'elles ne font que réorganiser les paramètres de différentes manières.


 let (|>) xf = fx //  pipe let (<|) fx = fx //  pipe let (>>) fgx = g (fx) //   let (<<) gfx = g (fx) //   

D'un autre côté, des fonctions comme "printf", bien que primitives, ne sont pas des combinateurs car elles dépendent du monde extérieur (E / S).


Oiseaux combinatoires


Les combinateurs sont à la base de toute une section de la logique (naturellement appelée "logique combinatoire"), qui a été inventée de nombreuses années avant les ordinateurs et les langages de programmation. La logique combinatoire a une très grande influence sur la programmation fonctionnelle.


Pour en savoir plus sur les combinateurs et la logique combinatoire, je recommande le livre de Raymond Smullyan "To Mock a Mockingbird". Dans ce document, il explique d'autres combinateurs et leur donne de manière fantaisiste des noms d'oiseaux . Voici quelques exemples de combinateurs standard et leurs noms d'oiseaux:


 let I x = x //  ,  Idiot bird let K xy = x // the Kestrel let M x = x >> x // the Mockingbird let T xy = yx // the Thrush ( !) let Q xyz = y (xz) // the Queer bird ( !) let S xyz = xz (yz) // The Starling //   ... let rec Y fx = f (Y f) x // Y-,  Sage bird 

Les noms de lettres sont assez standard, vous pouvez donc vous référer au combinateur K à toute personne connaissant cette terminologie.


Il s'avère que de nombreux modèles de programmation courants peuvent être représentés par ces combinateurs standard. Par exemple, Kestrel est un modèle régulier dans l'interface fluide où vous faites quelque chose mais retournez l'objet d'origine. Le muguet est une pipe, Queer est une composition directe et le combinateur Y fait un excellent travail de création de fonctions récursives.


En fait, il existe un théorème bien connu selon lequel toute fonction calculable peut être construite en utilisant seulement deux combinateurs de base, Kestrel et Starling.


Bibliothèques combinatoires


Les bibliothèques combinatoires sont des bibliothèques qui exportent de nombreuses fonctions combinatoires conçues pour être partagées. Un utilisateur d'une telle bibliothèque peut facilement combiner des fonctions pour obtenir des fonctions encore plus grandes et plus complexes, comme des cubes facilement.


Une bibliothèque de combinateurs bien conçue vous permet de vous concentrer sur les fonctions de haut niveau et de masquer le «bruit» de bas niveau. Nous avons déjà vu leur puissance dans plusieurs exemples de la série "pourquoi utiliser F #", et le module List est plein de telles fonctions, " fold " et " map " sont également des combinateurs si vous y pensez.


Un autre avantage des combinateurs est qu'ils sont le type de fonction le plus sûr. Parce que ils n'ont aucune dépendance vis-à-vis du monde extérieur, ils ne peuvent pas changer lorsque l'environnement mondial change. Une fonction qui lit une valeur globale ou utilise des fonctions de bibliothèque peut se rompre ou changer entre les appels si le contexte change. Cela n'arrivera jamais aux combinateurs.


En F #, les bibliothèques de combinateur sont disponibles pour l'analyse (FParsec), la création de HTML, les frameworks de test, etc. Nous discuterons et utiliserons les combinateurs plus tard dans la prochaine série.


Fonctions récursives


Souvent, une fonction doit se référer à elle-même à partir de son corps. Un exemple classique est la fonction de Fibonacci.


 let fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2) 

Malheureusement, cette fonction ne pourra pas compiler:


 error FS0039: The value or constructor 'fib' is not defined 

Vous devez indiquer au compilateur qu'il s'agit d'une fonction récursive utilisant le mot clé rec .


 let rec fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2) 

Les fonctions récursives et les structures de données sont très courantes dans la programmation fonctionnelle, et j'espère consacrer toute une série à ce sujet plus tard.


Ressources supplémentaires


Il existe de nombreux didacticiels pour F #, y compris des documents pour ceux qui viennent avec une expérience C # ou Java. Les liens suivants peuvent être utiles pour approfondir F #:



Plusieurs autres façons de commencer à apprendre le F # sont également décrites.


Enfin, la communauté F # est très conviviale pour les débutants. Il y a un chat très actif chez Slack, soutenu par la F # Software Foundation, avec des salles pour débutants que vous pouvez rejoindre librement . Nous vous recommandons fortement de le faire!


N'oubliez pas de visiter le site de la communauté russophone F # ! Si vous avez des questions sur l'apprentissage d'une langue, nous serons heureux d'en discuter dans les salles de chat:



À propos des auteurs de traduction


Traduit par @kleidemos
La traduction et les modifications éditoriales ont été apportées par les efforts de la communauté russophone des développeurs F # . Nous remercions également @schvepsss et @shwars d' avoir préparé cet article pour publication.

Source: https://habr.com/ru/post/fr433398/


All Articles