Unions discriminées

Les unions discriminées prennent en charge les valeurs qui peuvent être un cas nommé parmi de nombreux autres, éventuellement chacune avec 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, y compris les cas valides et les cas d’erreur, les données dont le type varie d’une instance à l’autre, et comme alternative aux petites hiérarchies d’objets. Par ailleurs, les unions discriminées récursives sont utilisées pour représenter des structures de données en arborescence.

Syntaxe

[ attributes ]
type [accessibility-modifier] type-name =
    | case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 ...]
    | case-identifier2 [of [fieldname3 : ]type3 [ * [ fieldname4 : ]type4 ...]

    [ member-list ]

Notes

Les unions discriminées sont similaires aux types union dans d’autres langages, mais il existe des différences. Comme avec un type union en C++ ou un type variante en Visual Basic, les données stockées dans la valeur ne sont pas fixes, il y a plusieurs options distinctes. Toutefois, contrairement aux unions dans ces autres langages, chacune des options possibles reçoit un identificateur de cas. Les identificateurs de cas sont des noms pour les différents types de valeurs possibles que peuvent avoir les objets de ce type. Les valeurs sont facultatives. S’il n’y a pas de valeurs, le cas équivaut à un cas d’énumération. S’il y a des valeurs, chaque valeur peut être une seule valeur d’un type spécifié ou un tuple qui agrège plusieurs champs de types identiques ou différents. Vous pouvez donner un nom à un champ individuel, mais le nom est facultatif, même si d’autres champs dans le même cas sont nommés.

L’accessibilité des unions discriminées est définie par défaut sur public.

Par exemple, prenons 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 union discriminée Shape, qui peut avoir des valeurs d’un des trois cas : Rectangle, Circle et Prism. Chaque cas a un ensemble de champs différent. Le cas Rectangle a deux champs nommés, tous deux de type float, dont les noms sont width et length. Le cas Circle a un seul champ nommé, radius. Le cas Prism a trois champs, dont deux (width et height) sont des champs nommés. Les champs sans nom sont appelés champs anonymes.

Vous construisez des objets en fournissant des valeurs pour les champs nommés et anonymes en fonction des 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 montre que vous pouvez utiliser les champs nommés dans l’initialisation, ou vous pouvez vous appuyer sur l’ordre des champs dans la déclaration et simplement fournir les valeurs de chaque champ à tour de rôle. L’appel de constructeur pour rect dans le code précédent utilise les champs nommés, mais l’appel de constructeur pour circ utilise l’ordre. Vous pouvez combiner les champs triés et les champs nommés, comme dans la construction de prism.

Le type option est une union discriminée simple dans la bibliothèque principale F#. Le type option est déclaré de la façon suivante.

// 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. Le cas 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 pas de valeur associée. Ainsi, le type option spécifie un type générique qui a une valeur d’un certain type ou aucune valeur. Le type Option a également un alias de type en minuscules, option, qui est plus couramment utilisé.

Les identificateurs de cas peuvent être utilisés comme constructeurs pour le type d’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 de critères spéciaux. Dans une expression de 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 en fonction de 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 les correspondances d’union discriminée. Pour le type Shape qui a été déclaré précédemment, vous pouvez utiliser les champs nommés comme le montre le code suivant pour extraire les valeurs des champs.

let getShapeWidth shape =
    match shape with
    | Rectangle(width = w) -> w
    | Circle(radius = r) -> 2. * r
    | Prism(width = w) -> w

Normalement, les identificateurs de cas peuvent être utilisés sans être qualifiés avec le nom de l’union. Si vous voulez que le nom soit toujours qualifié avec le nom de l’union, vous pouvez appliquer l’attribut RequireQualifiedAccess à la définition du type union.

Désenvelopper les unions discriminées

Dans F#, les unions discriminées sont souvent utilisées dans la modélisation de domaine pour l’enveloppement d’un seul type. Il est également facile d’extraire la valeur sous-jacente en utilisant des critères spéciaux. Vous n’avez pas besoin d’utiliser une expression de correspondance pour un seul cas :

let ([UnionCaseIdentifier] [values]) = [UnionValue]

Cela est illustré par l'exemple suivant :

type ShaderProgram = | ShaderProgram of id:int

let someFunctionUsingShaderProgram shaderProgram =
    let (ShaderProgram id) = shaderProgram
    // Use the unwrapped value
    ...

Les critères spéciaux sont également autorisés directement dans les paramètres de fonction. Vous pouvez donc désenvelopper un seul cas ici :

let someFunctionUsingShaderProgram (ShaderProgram id) =
    // Use the unwrapped value
    ...

Unions discriminées de struct

Vous pouvez également représenter des unions discriminées comme des structs. Pour cela, vous utilisez l’attribut [<Struct>].

[<Struct>]
type SingleCase = Case of string

[<Struct>]
type Multicase =
    | Case1 of Case1 : string
    | Case2 of Case2 : int
    | Case3 of Case3 : double

Comme ce sont des types valeur et non des types référence, il y a des points supplémentaires à prendre en considération par rapport aux unions discriminées de référence :

  1. Ils sont copiés sous forme de types valeur et ont une sémantique de type valeur.
  2. Vous ne pouvez pas utiliser une définition de type récursive avec une union discriminée de struct multicas.
  3. Vous devez fournir des noms de cas uniques pour une union discriminée de struct multicas.

Utilisation d’unions discriminées au lieu 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 peut être utilisée à la place d’une classe de base Shape qui a des types dérivés pour circle, square, etc.

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 aire ou un périmètre, comme dans une implémentation orientée objet, vous pouvez utiliser des critères spéciaux pour bifurquer vers des formules appropriées pour calculer ces quantités. Dans l’exemple suivant, différentes formules sont utilisées pour calculer l’aire, en fonction de 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 se présente comme suit :

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 en 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 plusieurs cas. Les unions discriminées récursives peuvent être utilisées pour créer des arborescences, qui sont utilisées pour modéliser 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 en arbre 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 de myTree.

Diagram that shows the tree structure for 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 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 de manière récursive l’arborescence de syntaxe.

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 [ "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

Quand ce code est exécuté, la valeur de result est 5.

Membres

Vous pouvez définir des membres sur des unions discriminées. L’exemple suivant montre comment définir une propriété et implémenter une interface :

open System

type IPrintable =
    abstract Print: unit -> unit

type Shape =
    | Circle of float
    | EquilateralTriangle of float
    | Square of float
    | Rectangle of float * float

    member this.Area =
        match this with
        | Circle r -> Math.PI * (r ** 2.0)
        | EquilateralTriangle s -> s * s * sqrt 3.0 / 4.0
        | Square s -> s * s
        | Rectangle(l, w) -> l * w

    interface IPrintable with
        member this.Print () =
            match this with
            | Circle r -> printfn $"Circle with radius %f{r}"
            | EquilateralTriangle s -> printfn $"Equilateral Triangle of side %f{s}"
            | Square s -> printfn $"Square with side %f{s}"
            | Rectangle(l, w) -> printfn $"Rectangle with length %f{l} and width %f{w}"

Attributs courants

Les attributs suivants sont couramment vus dans les unions discriminées :

  • [<RequireQualifiedAccess>]
  • [<NoEquality>]
  • [<NoComparison>]
  • [<Struct>]

Voir aussi