Unions discriminées (F#)
Les unions discriminées assurent la prise en charge de valeurs. Celles-ci peuvent correspondre à l'un des nombreux cas nommés, chacun d'eux pouvant avoir des valeurs et des types différents. Les unions discriminées sont utiles pour les données hétérogènes ; les données qui peuvent avoir des cas spéciaux, notamment des cas valides et cas d'erreur ; les données dont le type varie d'une instance à une autre ; et comme alternative pour les petites hiérarchies d'objets. De plus, les unions discriminées récursives sont utilisées pour représenter des structures de données d'arborescence.
type type-name =
| case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 ...]
| case-identifier2 [of [fieldname3 : ]type3 [ * [ fieldname4 : ]type4 ...]
...
Notes
Les unions discriminées sont semblables aux types union dans d'autres langages, mais il existe des différences. Comme avec un type union en C++ ou un type variant en Visual Basic, les données stockées dans la valeur ne sont pas fixes ; elles peuvent revêtir l'une des diverses options distinctes. Contrairement aux unions de ces autres langages, toutefois, chacune des options possibles reçoit un identificateur de cas. Les identificateurs de cas correspondent aux noms des différents types de valeurs que peuvent prendre les objets de ce type ; les valeurs sont facultatives. Si les valeurs sont absentes, le cas équivaut à un cas d'énumération. Si les valeurs sont présentes, chaque valeur peut être une valeur unique d'un type spécifié, ou un tuple qui regroupe plusieurs champs du même type ou de types différents. Avec F# 3,1, vous pouvez donner à un champ individuel un nom, mais le nom est facultatif, même si d'autres champs dans le même cas sont nommés.
Par exemple, considérons la déclaration suivante d'un type Shape.
type Shape =
| Rectangle of width : float * length : float
| Circle of radius : float
| Prism of width : float * float * height : float
Le code précédent déclare une forme d'union discriminée, qui peut comporter des valeurs des trois cas suivants : rectangle, cercle et prisme. Chaque cas possède un ensemble de champs différent. Le rectangle a deux champs nommés, tous les deux de type float, qui portent le nom largeur et longueur. Le cercle a un seul champ nommé, le rayon. La casse de prisme a trois champs, dont deux sont sans nom et constituent des champs anonymes.
Vous construisez les objets en fournissant les valeurs des champs nommés et des champs anonymes conformément aux exemples suivants.
let rect = Rectangle(length = 1.3, width = 10.0)
let circ = Circle (1.0)
let prism = Prism(5., 2.0, height = 3.0)
Ce code indique que vous pouvez utiliser les champs nommés dans l'initialisation, ou que vous pouvez utiliser l'ordre des champs dans la déclaration et fournir uniquement les valeurs pour chaque champ. L'appel du constructeur pour rect dans le code précédent utilise les champs nommés, mais l'appel du constructeur pour circ utilise le classement. Vous pouvez combiner les champs ordonnés et les champs nommés, comme dans la construction de prism.
Le type option est une union discriminée simple de la bibliothèque principale F#. Le type option est déclaré comme suit.
// The option type is a discriminated union.
type Option<'a> =
| Some of 'a
| None
Le code précédent spécifie que le type Option est une union discriminée qui a deux cas, Some et None. La case Some a une valeur associée qui se compose d'un champ anonyme dont le type est représenté par le paramètre de type 'a. Le cas None n'a aucune valeur associée. Donc, le type option spécifie un type générique qui a soit une valeur d'un certain type, soit aucune valeur. Le type Option a également un alias de type en minuscules, option, plus communément utilisé.
Les identificateurs de cas peuvent être utilisés comme constructeurs pour le type union discriminée. Par exemple, le code suivant est utilisé pour créer des valeurs du type option.
let myOption1 = Some(10.0)
let myOption2 = Some("string")
let myOption3 = None
Les identificateurs de cas sont également utilisés dans les expressions à critères spéciaux. Dans une expression à critères spéciaux, les identificateurs sont fournis pour les valeurs associées aux cas individuels. Par exemple, dans le code suivant, x est l'identificateur qui reçoit la valeur associée au cas Some du type option.
let printValue opt =
match opt with
| Some x -> printfn "%A" x
| None -> printfn "No value."
Dans les expressions de critères spéciaux, vous pouvez utiliser des champs nommés pour spécifier des correspondances d'unions discriminées. Pour le type Shape qui a été déclaré précédemment, vous pouvez utiliser les champs nommés, comme l'illustre le code suivant, pour extraire les valeurs des champs.
let getShapeHeight shape =
match shape with
| Rectangle(height = h) -> h
| Circle(radius = r) -> 2. * r
| Prism(height = h) -> h
Normalement, les identificateurs de cas peuvent être utilisés sans être qualifiés avec le nom de l'union. Si vous souhaitez que le nom soit toujours qualifié avec le nom de l'union, vous pouvez appliquer l'attribut RequireQualifiedAccess à la définition de type union.
Utilisation d'unions discriminées à la place de hiérarchies d'objets
Vous pouvez souvent utiliser une union discriminée comme alternative plus simple à une petite hiérarchie d'objets. Par exemple, l'union discriminée suivante pourrait être utilisée à la place d'une classe de base Shape qui a des types dérivés pour cercle, carré, et ainsi de suite.
type Shape =
// The value here is the radius.
| Circle of float
// The value here is the side length.
| EquilateralTriangle of double
// The value here is the side length.
| Square of double
// The values here are the height and width.
| Rectangle of double * double
Au lieu d'une méthode virtuelle pour calculer une zone ou un périmètre, que vous utiliseriez dans une implémentation orientée objet, vous pouvez utiliser les critères spéciaux pour créer une branche vers les formules appropriées pour calculer ces quantités. Dans l'exemple suivant, différentes formules sont utilisées pour calculer la zone, selon la forme.
let pi = 3.141592654
let area myShape =
match myShape with
| Circle radius -> pi * radius * radius
| EquilateralTriangle s -> (sqrt 3.0) / 4.0 * s * s
| Square s -> s * s
| Rectangle (h, w) -> h * w
let radius = 15.0
let myCircle = Circle(radius)
printfn "Area of circle that has radius %f: %f" radius (area myCircle)
let squareSide = 10.0
let mySquare = Square(squareSide)
printfn "Area of square that has side %f: %f" squareSide (area mySquare)
let height, width = 5.0, 10.0
let myRectangle = Rectangle(height, width)
printfn "Area of rectangle that has height %f and width %f is %f" height width (area myRectangle)
La sortie est la suivante :
Area of circle that has radius 15.000000: 706.858347
Area of square that has side 10.000000: 100.000000
Area of rectangle that has height 5.000000 and width 10.000000 is 50.000000
Utilisation d'unions discriminées pour les structures de données d'arborescence
Les unions discriminées peuvent être récursives, ce qui signifie que l'union elle-même peut être incluse dans le type d'un ou de plusieurs cas. Les unions discriminées récursives peuvent être utilisées pour créer des arborescences, utilisées pour modeler des expressions dans les langages de programmation. Dans le code suivant, une union discriminée récursive est utilisée pour créer une structure de données d'arborescence binaire. L'union se compose de deux cas, Node, qui est un nœud avec une valeur entière et des sous-arborescences gauche et droite, et Tip, qui termine l'arborescence.
type Tree =
| Tip
| Node of int * Tree * Tree
let rec sumTree tree =
match tree with
| Tip -> 0
| Node(value, left, right) ->
value + sumTree(left) + sumTree(right)
let myTree = Node(0, Node(1, Node(2, Tip, Tip), Node(3, Tip, Tip)), Node(4, Tip, Tip))
let resultSumTree = sumTree myTree
Dans le code précédent, resultSumTree a la valeur 10. L'illustration suivante montre l'arborescence pour myTree.
Arborescence pour myTree
Les unions discriminées fonctionnent bien si les nœuds de l'arborescence sont hétérogènes. Dans le code suivant, le type Expression représente l'arborescence de syntaxe abstraite d'une expression dans un langage de programmation simple qui prend en charge l'addition et la multiplication de nombres et de variables. Certains des cas d'union ne sont pas récursifs et représentent des nombres (Number) ou des variables (Variable). D'autres cas sont récursifs et représentent des opérations (Add et Multiply), où les opérandes sont également des expressions. La fonction Evaluate utilise une expression de correspondance pour traiter l'arborescence de syntaxe de manière récursive.
type Expression =
| Number of int
| Add of Expression * Expression
| Multiply of Expression * Expression
| Variable of string
let rec Evaluate (env:Map<string,int>) exp =
match exp with
| Number n -> n
| Add (x, y) -> Evaluate env x + Evaluate env y
| Multiply (x, y) -> Evaluate env x * Evaluate env y
| Variable id -> env.[id]
let environment = Map.ofList [ "a", 1 ;
"b", 2 ;
"c", 3 ]
// Create an expression tree that represents
// the expression: a + 2 * b.
let expressionTree1 = Add(Variable "a", Multiply(Number 2, Variable "b"))
// Evaluate the expression a + 2 * b, given the
// table of values for the variables.
let result = Evaluate environment expressionTree1
Lorsque ce code est exécuté, la valeur de result est 5.