La troisième partie d'une série d'articles sur la programmation fonctionnelle s'est arrêtée. Aujourd'hui, nous allons parler de tous les types de ce paradigme et montrer des exemples de leur utilisation. Plus d'informations sur les types primitifs, les types généralisés et bien plus encore sous la coupe!

Maintenant que nous avons une certaine compréhension des fonctions, nous allons voir comment les types interagissent avec des fonctions telles que le domaine et la plage. Cet article n'est qu'une revue. Pour une immersion plus profonde dans les types, il existe une série de "compréhension des types F #" .
Pour commencer, nous avons besoin d'une meilleure compréhension de la notation de type. Nous avons vu la flèche " ->
" séparant le domaine et la plage. Ainsi, la signature de fonction ressemble toujours à ceci:
val functionName : domain -> range
Quelques autres exemples de fonctions:
let intToString x = sprintf "x is %i" x // int string let stringToInt x = System.Int32.Parse(x)
Si vous exécutez ce code dans une fenêtre interactive , vous pouvez voir les signatures suivantes:
val intToString : int -> string val stringToInt : string -> int
Ils signifient:
intToString
a un domaine de type int
, qui correspond à la plage de type string
.stringToInt
possède un domaine de type string
, qui correspond à une plage de type int
.
Types primitifs
Il existe des types primitifs attendus: string, int, float, bool, char, byte, etc., ainsi que de nombreux autres dérivés du système de type .NET.
Quelques autres exemples de fonctions avec des types primitifs:
let intToFloat x = float x // "float" - int float let intToBool x = (x = 2) // true x 2 let stringToString x = x + " world"
et leurs signatures:
val intToFloat : int -> float val intToBool : int -> bool val stringToString : string -> string
Type d'annotation
Dans les exemples précédents, le compilateur F # a correctement défini les types de paramètres et de résultats. Mais cela ne se produit pas toujours. Si vous essayez d'exécuter le code suivant, vous obtiendrez une erreur de compilation:
let stringLength x = x.Length => error FS0072: Lookup on object of indeterminate type
Le compilateur ne connaît pas le type de l'argument "x", et pour cette raison, il ne sait pas si la "Longueur" est une méthode valide. Dans la plupart des cas, cela peut être résolu en passant l '"annotation de type" au compilateur F #. Il saura ensuite quel type utiliser. Dans la version fixe, nous indiquons que le type "x" est une chaîne.
let stringLength (x:string) = x.Length
Les accolades autour du paramètre x:string
sont importantes. S'ils sont ignorés, le compilateur décidera que la chaîne est la valeur de retour! Autrement dit, un signe deux-points ouvert est utilisé pour indiquer le type de valeur de retour, comme indiqué dans l'exemple suivant.
let stringLengthAsInt (x:string) :int = x.Length
Nous indiquons que le paramètre x
est une chaîne et que la valeur de retour est un entier.
Types de fonctions comme paramètres
Une fonction qui prend d'autres fonctions comme paramètres ou renvoie une fonction est appelée fonction d'ordre supérieur (la fonction d'ordre supérieur est parfois raccourcie en HOF). Ils sont utilisés comme abstraction pour définir un comportement aussi général que possible. Ce type de fonction est très courant en F #, la plupart des bibliothèques standard les utilisent.
Considérez la fonction evalWith5ThenAdd2
, qui prend une fonction comme paramètre, puis calcule cette fonction à partir de 5 et ajoute 2 au résultat:
let evalWith5ThenAdd2 fn = fn 5 + 2 // , fn(5) + 2
La signature de cette fonction ressemble à ceci:
val evalWith5ThenAdd2 : (int -> int) -> int
Vous pouvez voir que le domaine est (int->int)
et la plage est int
. Qu'est-ce que cela signifie? Cela signifie que le paramètre d'entrée n'est pas une valeur simple, mais une fonction de nombreuses fonctions de int
à int
. La valeur de sortie n'est pas une fonction, mais juste un int
.
Essayons:
let add1 x = x + 1 // - (int -> int) evalWith5ThenAdd2 add1 //
et obtenez:
val add1 : int -> int val it : int = 8
" add1
" est une fonction qui mappe int
à int
, comme on le voit dans la signature. Il s'agit d'un paramètre valide pour evalWith5ThenAdd2
et son résultat est 8.
Soit dit en passant, le mot spécial " it
" est utilisé pour désigner la dernière valeur calculée, dans ce cas, c'est le résultat que nous attendions. Ce n'est pas un mot-clé, c'est juste une convention de dénomination.
Un autre cas:
let times3 x = x * 3 // - (int -> int) evalWith5ThenAdd2 times3 //
donne:
val times3 : int -> int val it : int = 17
" times3
" est également une fonction qui mappe int
à int
, comme on peut le voir sur la signature. Il s'agit également d'un paramètre valide pour evalWith5ThenAdd2
. Le résultat des calculs est 17.
Veuillez noter que les données d'entrée sont sensibles au type. Si la fonction passée utilise un float
, pas un int
, alors rien ne fonctionnera. Par exemple, si nous avons:
let times3float x = x * 3.0 // - (float->float) evalWith5ThenAdd2 times3float
Le compilateur, en essayant de compiler, retournera une erreur:
error FS0001: Type mismatch. Expecting a int -> int but given a float -> float
signalant que la fonction d'entrée doit être une fonction de type int->int
.
Fonctionne comme sortie
Les fonctions de valeur peuvent également être le résultat de fonctions. Par exemple, la fonction suivante générera une fonction «additionneur» qui ajoutera une valeur d'entrée.
let adderGenerator numberToAdd = (+) numberToAdd
Sa signature:
val adderGenerator : int -> (int -> int)
signifie que le générateur prend un int
et crée une fonction ("additionneur") qui mappe les ints
en ints
. Voyons comment cela fonctionne:
let add1 = adderGenerator 1 let add2 = adderGenerator 2
Deux fonctions d'addition sont créées. Le premier crée une fonction qui ajoute 1 à l'entrée, le second en ajoute 2. Notez que les signatures sont exactement ce que nous attendions.
val add1 : (int -> int) val add2 : (int -> int)
Vous pouvez maintenant utiliser les fonctions générées comme d'habitude, elles ne sont pas différentes des fonctions définies explicitement:
add1 5 // val it : int = 6 add2 5 // val it : int = 7
Utilisation d'annotations de type pour restreindre les types de fonction
Dans le premier exemple, nous avons examiné une fonction:
let evalWith5ThenAdd2 fn = fn 5 +2 > val evalWith5ThenAdd2 : (int -> int) -> int
Dans cet exemple, F # peut conclure que " fn
" convertit int
en int
, donc sa signature sera int->int
.
Mais quelle est la signature de "fn" dans le cas suivant?
let evalWith5 fn = fn 5
Il est clair que " fn
" est une sorte de fonction qui prend un int
, mais que renvoie-t-elle? Le compilateur ne peut pas répondre à cette question. Dans de tels cas, s'il devient nécessaire d'indiquer le type de fonction, vous pouvez ajouter un type d'annotation pour les paramètres de fonction, ainsi que pour les types primitifs.
let evalWith5AsInt (fn:int->int) = fn 5 let evalWith5AsFloat (fn:int->float) = fn 5
De plus, vous pouvez déterminer le type de retour.
let evalWith5AsString fn :string = fn 5
Parce que la fonction principale renvoie une string
, la fonction " fn
" est également forcée de renvoyer une string
. Ainsi, il n'est pas nécessaire de spécifier explicitement le type " fn
".
Tapez "unité"
Dans le processus de programmation, nous voulons parfois qu'une fonction fasse quelque chose sans rien retourner. Considérez la fonction " printInt
". La fonction ne renvoie vraiment rien. Il imprime simplement la chaîne à la console comme effet secondaire de l'exécution.
let printInt x = printf "x is %i" x //
Quelle est sa signature?
val printInt : int -> unit
Qu'est-ce qu'une " unit
"?
Même si la fonction ne renvoie pas de valeurs, elle a toujours besoin d'une plage. Il n'y a pas de fonctions "nulles" dans le monde des mathématiques. Chaque fonction doit renvoyer quelque chose, car la fonction est un mappage, et le mappage doit afficher quelque chose!

Ainsi, en F #, des fonctions comme celle-ci renvoient un type spécial de résultat appelé " unit
". Il ne contient qu'une seule valeur, notée " ()
". Vous pourriez penser que unit
et ()
sont quelque chose comme "void" et "null" de C #, respectivement. Mais contrairement à eux, l' unit
est le type réel et ()
valeur réelle. Pour le vérifier, faites simplement:
let whatIsThis = ()
La signature suivante sera reçue:
val whatIsThis : unit = ()
Ce qui indique que l'étiquette " whatIsThis
" est de type unit
et est associée à une valeur ()
.
Maintenant, revenant à la signature " printInt
", nous pouvons comprendre la signification de cette entrée:
val printInt : int -> unit
Cette signature indique que printInt
a un domaine d' int
, ce qui se traduit par quelque chose qui ne nous intéresse pas.
Fonctions sans paramètres
Maintenant que nous comprenons l' unit
, pouvons-nous prédire son apparition dans un contexte différent? Par exemple, essayez de créer une fonction réutilisable "hello world". Puisqu'il n'y a pas d'entrée ou de sortie, nous pouvons nous attendre à l' unit -> unit
signature unit -> unit
. Voyons voir:
let printHello = printf "hello world" //
Résultat:
hello world val printHello : unit = ()
Pas tout à fait ce que nous attendions. "Hello world" a été sorti immédiatement, et le résultat n'était pas une fonction, mais une simple valeur de type unité. On peut dire que c'est une valeur simple, car, comme nous l'avons vu précédemment, elle a une signature de la forme:
val aName: type = constant
Dans cet exemple, nous voyons que printHello
vraiment une simple valeur ()
. Ce n'est pas une fonction que nous pourrons appeler plus tard.
Quelle est la différence entre printInt
et printHello
? Dans le cas de printInt
valeur ne peut pas être déterminée tant que nous ne connaissons pas la valeur du paramètre x
, donc la définition était une fonction. Dans le cas de printHello
il n'y a pas de paramètres, donc le côté droit peut être défini en place. Et c'était égal à ()
avec un effet secondaire sous forme de sortie vers la console.
Vous pouvez créer une véritable fonction réutilisable sans paramètres, forçant la définition à avoir un argument unit
:
let printHelloFn () = printf "hello world" //
Maintenant, sa signature est égale à:
val printHelloFn : unit -> unit
et pour l'appeler, nous devons passer ()
comme paramètre:
printHelloFn ()
Renforcement des types d'unités avec la fonction Ignorer
Dans certains cas, le compilateur nécessite un type d' unit
et se plaint. Par exemple, les deux cas suivants provoqueront une erreur de compilation:
do 1+1 // => FS0020: This expression should have type 'unit' let something = 2+2 // => FS0020: This expression should have type 'unit' "hello"
Pour aider dans ces situations, il existe une fonction spéciale ignore
qui prend n'importe quoi et renvoie l' unit
. La version correcte de ce code pourrait être la suivante:
do (1+1 |> ignore) // ok let something = 2+2 |> ignore // ok "hello"
Types génériques
Dans la plupart des cas, si le type d'un paramètre de fonction peut être de n'importe quel type, nous devons en dire quelque chose. F # utilise des génériques .NET pour de telles situations.
Par exemple, la fonction suivante convertit un paramètre en chaîne en ajoutant du texte:
let onAStick x = x.ToString() + " on a stick"
Quel que soit le type de paramètre, tous les objets peuvent faire dans ToString()
.
Signature:
val onAStick : 'a -> string
Quel type 'a
? En F #, c'est une façon d'indiquer un type générique inconnu au moment de la compilation. Une apostrophe avant «a» signifie que le type est générique. Équivalent à cette signature en C #:
string onAStick<a>(); // string OnAStick<TObject>(); // F#- 'a // C#'- "TObject"
Il faut comprendre que cette fonction F # a toujours un typage fort même avec des types génériques. Il n'accepte pas un paramètre de type Object
. Une frappe forte est bonne car elle vous permet de maintenir la sécurité de votre type lors de la composition de fonctions.
La même fonction est utilisée pour int
, float
et string
.
onAStick 22 onAStick 3.14159 onAStick "hello"
S'il y a deux paramètres généralisés, alors le compilateur leur donnera deux noms différents: 'a
pour le premier, 'b
pour le second, etc. Par exemple:
let concatString xy = x.ToString() + y.ToString()
Il y aura deux types génériques dans cette signature: 'a
et 'b
:
val concatString : 'a -> 'b -> string
D'un autre côté, le compilateur reconnaît quand un seul type générique est requis. Dans l'exemple suivant, x
et y
doivent être du même type:
let isEqual xy = (x=y)
Ainsi, une signature de fonction a le même type générique pour les deux paramètres:
val isEqual : 'a -> 'a -> bool
Les paramètres généralisés sont également très importants en ce qui concerne les listes et autres structures abstraites, et nous en verrons beaucoup dans les exemples suivants.
Autres types
Jusqu'à présent, seuls les types de base ont été discutés. Ces types peuvent être combinés de différentes manières en des types plus complexes. Leur analyse complète sera plus tard dans une autre série , mais en attendant, et ici nous les analyserons brièvement, afin que vous puissiez les reconnaître dans les signatures de fonctions.
- Tuples Il s'agit d'une paire, d'un triple, etc., composée d'autres types. Par exemple,
("hello", 1)
est un tuple basé sur string
et int
. Une virgule est une caractéristique des tuples; si une virgule est vue quelque part en F #, il est presque garanti qu'elle fait partie du tuple.
Dans les signatures de fonction, les tuples sont écrits comme des «produits» des deux types impliqués. Dans ce cas, le tuple sera du type:
string * int // ("hello", 1)
- Collections . Les plus courants sont list (liste), seq (séquence) et array. Les listes et les tableaux sont de taille fixe, tandis que les séquences sont potentiellement infinies (en arrière-plan, les séquences sont les mêmes
IEnumrable
). Dans les signatures de fonction, ils ont leurs propres mots-clés: " list
", " seq
" et " []
" pour les tableaux.
int list // List type [1;2;3] string list // List type ["a";"b";"c"] seq<int> // Seq type seq{1..10} int [] // Array type [|1;2;3|]
- Option (type optionnel) . Il s'agit d'un simple wrapper sur des objets qui peuvent être manquants. Il existe deux options:
Some
(lorsque la valeur existe) et None
(lorsque la valeur n'existe pas). Dans les signatures de fonction, ils ont leur propre mot-clé " option
":
int option // Some 1
- L'association marquée (union discriminée) . Ils sont construits à partir de nombreuses variantes d'autres types. Nous avons vu quelques exemples dans "Pourquoi utiliser F #?" . Dans les signatures de fonction, elles sont référencées par nom de type; elles n'ont pas de mot-clé spécial.
- Type d'enregistrement (enregistrements) . Des types comme des structures ou des lignes de base de données, un ensemble de valeurs nommées. Nous avons également vu quelques exemples dans "Pourquoi utiliser F #?" . Dans les signatures de fonction, elles sont appelées par nom de type et n'ont pas non plus leur propre mot-clé.
Testez votre compréhension des types
Voici quelques expressions pour tester votre compréhension des signatures de fonction. Pour vérifier, il suffit de les exécuter dans une fenêtre interactive!
let testA = float 2 let testB x = float 2 let testC x = float 2 + x let testD x = x.ToString().Length let testE (x:float) = x.ToString().Length let testF x = printfn "%s" x let testG x = printfn "%f" x let testH = 2 * 2 |> ignore let testI x = 2 * 2 |> ignore let testJ (x:int) = 2 * 2 |> ignore let testK = "hello" let testL() = "hello" let testM x = x=x let testN x = x 1 // : x? let testO x:string = x 1 // : :string ?
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.