Uniones discriminadas

Las uniones discriminadas proporcionan compatibilidad con valores que pueden ser uno de varios casos con nombre, posiblemente cada uno con distintos valores y tipos. Las uniones discriminadas son útiles para datos heterogéneos; datos que pueden tener casos especiales, incluidos los casos válidos y de error; datos que varían en tipo de una instancia a otra; y como alternativa para jerarquías de objetos pequeños. Además, se usan uniones discriminadas recursivas para representar estructuras de datos de árbol.

Sintaxis

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

    [ member-list ]

Comentarios

Las uniones discriminadas son similares a los tipos de unión de otros lenguajes, pero hay diferencias. Al igual que con un tipo de unión en C++ o un tipo variante en Visual Basic, los datos almacenados en el valor no son fijos; puede ser una de varias opciones distintas. Sin embargo, a diferencia de las uniones en estos otros lenguajes, a cada una de las opciones posibles se le da un identificador de caso. Los identificadores de caso son nombres para los distintos tipos posibles de valores que podrían ser objetos de este tipo. los valores son opcionales. Si los valores no están presentes, el caso es equivalente a un caso de enumeración. Si hay valores, cada valor puede ser un valor único de un tipo especificado o una tupla que agrega varios campos del mismo tipo o de distintos tipos. Puede dar un nombre a un campo individual, pero el nombre es opcional, incluso si se denominan otros campos en el mismo caso.

La accesibilidad para uniones discriminadas tiene como valor predeterminado public .

Por ejemplo, considere la siguiente declaración de un tipo Shape.

type Shape =
    | Rectangle of width : float * length : float
    | Circle of radius : float
    | Prism of width : float * float * height : float

El código anterior declara una unión discriminada Shape, que puede tener valores de cualquiera de tres casos: Rectangle, Circle y Prism. Cada caso tiene un conjunto diferente de campos. El caso Rectangle tiene dos campos con nombre, ambos de tipo , que tienen el ancho y la float longitud de los nombres. El caso de círculo tiene solo un campo con nombre, radius. El caso Prism tiene tres campos, dos de los cuales (ancho y alto) se denominan campos. Los campos sin nombre se conocen como campos anónimos.

Los objetos se construyen proporcionando valores para los campos con nombre y anónimos según los ejemplos siguientes.

let rect = Rectangle(length = 1.3, width = 10.0)
let circ = Circle (1.0)
let prism = Prism(5., 2.0, height = 3.0)

Este código muestra que puede usar los campos con nombre en la inicialización o puede basarse en el orden de los campos de la declaración y simplemente proporcionar los valores de cada campo a su vez. La llamada al constructor rect para en el código anterior usa los campos con nombre, pero la llamada del constructor para utiliza la circ ordenación. Puede mezclar los campos ordenados y los campos con nombre, como en la construcción de prism .

El option tipo es una unión discriminada simple en la biblioteca principal de F#. El option tipo se declara como sigue.

// The option type is a discriminated union.
type Option<'a> =
    | Some of 'a
    | None

El código anterior especifica que el tipo Option es una unión discriminada que tiene dos casos, Some y None . El Some caso tiene un valor asociado que consta de un campo anónimo cuyo tipo se representa mediante el parámetro de tipo 'a . El None caso no tiene ningún valor asociado. Por lo option tanto, el tipo especifica un tipo genérico que tiene un valor de algún tipo o ningún valor. El tipo Option también tiene un alias de tipo en minúsculas, , que se usa con más option frecuencia.

Los identificadores de caso se pueden usar como constructores para el tipo de unión discriminada. Por ejemplo, se usa el código siguiente para crear valores del option tipo .

let myOption1 = Some(10.0)
let myOption2 = Some("string")
let myOption3 = None

Los identificadores de caso también se usan en expresiones de coincidencia de patrones. En una expresión de coincidencia de patrones, se proporcionan identificadores para los valores asociados a los casos individuales. Por ejemplo, en el código siguiente, x es el identificador dado el valor asociado al caso del Some option tipo.

let printValue opt =
    match opt with
    | Some x -> printfn "%A" x
    | None -> printfn "No value."

En las expresiones de coincidencia de patrones, puede usar campos con nombre para especificar coincidencias de unión discriminadas. Para el tipo de forma que se declaró anteriormente, puede usar los campos con nombre como se muestra en el código siguiente para extraer los valores de los campos.

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

Normalmente, los identificadores de caso se pueden usar sin calificarlos con el nombre de la unión. Si desea que el nombre siempre esté calificado con el nombre de la unión, puede aplicar el atributo RequireQualifiedAccess a la definición del tipo de unión.

Desencapsular uniones discriminadas

En F# las uniones discriminadas se usan a menudo en el modelado de dominios para ajustar un único tipo. También es fácil extraer el valor subyacente mediante la coincidencia de patrones. No es necesario usar una expresión de coincidencia para un solo caso:

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

En el siguiente ejemplo se muestra esto:

type ShaderProgram = | ShaderProgram of id:int

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

La coincidencia de patrones también se permite directamente en los parámetros de función, por lo que puede desencapsular un solo caso:

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

Struct Discriminated Unions

También puede representar uniones discriminadas como estructuras. Esto se hace con el [<Struct>] atributo .

[<Struct>]
type SingleCase = Case of string

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

Dado que se trata de tipos de valor y no tipos de referencia, hay consideraciones adicionales en comparación con las uniones discriminadas de referencia:

  1. Se copian como tipos de valor y tienen semántica de tipo de valor.
  2. No se puede usar una definición de tipo recursivo con una estructura multicase Unión discriminada.
  3. Debe proporcionar nombres de caso únicos para una estructura multicase Unión discriminada.

Usar uniones discriminadas en lugar de jerarquías de objetos

A menudo puede usar una unión discriminada como alternativa más sencilla a una jerarquía de objetos pequeña. Por ejemplo, se podría usar la siguiente unión discriminada en lugar de una clase base que tenga tipos derivados para Shape círculo, cuadrado, y así sucesivamente.

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

En lugar de un método virtual para calcular un área o un perímetro, como se usaría en una implementación orientada a objetos, puede usar la coincidencia de patrones para bifurcar a las fórmulas adecuadas para calcular estas cantidades. En el ejemplo siguiente, se usan fórmulas diferentes para calcular el área, en función de la forma.

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 salida es como sigue:

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

Usar uniones discriminadas para estructuras de datos de árbol

Las uniones discriminadas pueden ser recursivas, lo que significa que la propia unión se puede incluir en el tipo de uno o varios casos. Las uniones discriminadas recursivas se pueden usar para crear estructuras de árbol, que se usan para modelar expresiones en lenguajes de programación. En el código siguiente, se usa una unión discriminada recursiva para crear una estructura de datos de árbol binario. La unión consta de dos casos, , que es un nodo con un valor entero y subárboles izquierdo y derecho, y , que Node Tip finaliza el árbol.

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

En el código anterior, resultSumTree tiene el valor 10. En la ilustración siguiente se muestra la estructura de árbol para myTree .

Diagrama que muestra la estructura de árbol de myTree.

Las uniones discriminadas funcionan bien si los nodos del árbol son heterogéneos. En el código siguiente, el tipo representa el árbol de sintaxis abstracta de una expresión en un lenguaje de programación simple que admite la suma y Expression multiplicación de números y variables. Algunos de los casos de unión no son recursivos y representan números ( Number ) o variables ( Variable ). Otros casos son recursivos y representan operaciones ( y ), donde los Add Multiply operandos también son expresiones. La Evaluate función usa una expresión match para procesar de forma recursiva el árbol de sintaxis.

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

Cuando se ejecuta este código, el valor de result es 5.

Miembros

Es posible definir miembros en uniones discriminadas. En el ejemplo siguiente se muestra cómo definir una propiedad e implementar una interfaz :

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

Atributos comunes

Los siguientes atributos se ven normalmente en uniones discriminadas:

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

Vea también