Uniones discriminadas
Las uniones discriminadas proporcionan compatibilidad con valores que pueden ser diferentes casos con nombre, cada uno con valores y tipos posiblemente diferentes. Las uniones discriminadas son útiles con datos heterogéneos, con datos que pueden tener casos especiales (incluidos casos válidos y con error), con datos que varían en cuanto a tipo de una instancia a otra y, asimismo, como alternativa en jerarquías de objetos pequeñas. Además, también 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 en otros lenguajes, pero hay diferencias. Al igual que sucede con los tipos de unión en C++ o con los tipos de variante en Visual Basic, los datos almacenados en el valor no son fijos, sino que pueden ser una de varias opciones distintas. Con todo, y a diferencia de las uniones de estos otros lenguajes, a cada una de las opciones posibles se le asigna 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 no hay valores presentes, el caso equivale a un caso de enumeración. Si hay valores presentes, cada valor puede ser un valor único de un tipo especificado, o una tupla que agrega varios campos de los mismos tipos o tipos diferentes. Se puede asignar un nombre a un campo individual, pero el nombre es opcional, incluso si hay otros campos con nombre en el mismo caso.
La accesibilidad de las uniones discriminadas es public
de forma predeterminada.
Por ejemplo, consideremos 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 forma de unión discriminada Shape, que puede tener valores de tres casos posibles: Rectangle, Circle y Prism. Cada caso tiene un conjunto de campos diferente. El caso Rectangle tiene dos campos con nombre, ambos de tipo float
, que tienen los nombres 'width' y 'length'. El caso Circle tiene solo un campo con nombre, 'radius'. El caso Prism tiene tres campos, dos de los cuales ('width' y 'height') son campos con nombre. Los campos sin nombre se conocen como campos anónimos.
Los objetos se crean proporcionando valores en los campos con nombre y anónimos, como en 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 bien puede basarse en el orden de los campos de la declaración y proporcionar a cambio solo los valores de cada campo. La llamada del constructor de rect
en el código anterior usa los campos con nombre, pero la llamada del constructor de circ
usa la ordenación. Los campos ordenados y los campos con nombre se pueden mezclar, como en la construcción de prism
.
El tipo option
es una unión discriminada sencilla en la biblioteca principal de F#. El tipo option
se declara de la siguiente forma.
// 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 caso Some
tiene un valor asociado que consta de un campo anónimo cuyo tipo está representado por el parámetro de tipo 'a
. El caso None
no tiene ningún valor asociado. Por lo tanto, el tipo option
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, option
, que se usa con más frecuencia.
Los identificadores de caso se pueden usar como constructores en el tipo de unión discriminada. Por ejemplo, el siguiente código se usa para crear valores de tipo option
.
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 de los valores asociados a los casos individuales. Por ejemplo, en el siguiente código, x
es el identificador dado al valor asociado al caso Some
de tipo option
.
let printValue opt =
match opt with
| Some x -> printfn "%A" x
| None -> printfn "No value."
En las expresiones de coincidencia de patrones se pueden usar campos con nombre para especificar coincidencias de unión discriminadas. En cuanto al tipo Shape declarado anteriormente, se pueden usar los campos con nombre como se indica 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.
Desencapsulamiento de uniones discriminadas
En F#, las uniones discriminadas se usan con frecuencia en el modelado de dominios para encapsular un solo tipo. También resulta fácil extraer el valor subyacente a través de la coincidencia de patrones. No es necesario usar una expresión de coincidencia para un único 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 puede usar directamente en los parámetros de función, por lo que se puede desencapsular un único caso:
let someFunctionUsingShaderProgram (ShaderProgram id) =
// Use the unwrapped value
...
Uniones discriminadas como estructuras
Las uniones discriminadas también se pueden representar como estructuras. Esto se realiza a través del atributo [<Struct>]
.
[<Struct>]
type SingleCase = Case of string
[<Struct>]
type Multicase =
| Case1 of Case1 : string
| Case2 of Case2 : int
| Case3 of Case3 : double
Dado que son tipos de valor y no tipos de referencia, existen consideraciones adicionales que deben tenerse en cuenta en comparación con las uniones discriminadas de referencia:
- Se copian como tipos de valor y tienen la semántica de los tipos de valor.
- No se puede usar una definición de tipo recursivo con una unión discriminada de estructura de tipo Multicase.
- En una unión discriminada de estructura de tipo Multicase se deben proporcionar nombres de caso únicos.
Uso de uniones discriminadas en lugar de jerarquías de objetos
A menudo, puede usar una unión discriminada como una alternativa más sencilla a una jerarquía de objetos pequeña. Por ejemplo, podríamos usar la siguiente unión discriminada en lugar de una clase base Shape
que tenga tipos derivados para círculo, cuadrado, 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
En lugar de usar un método virtual para calcular un área o perímetro, como haríamos en una implementación orientada a objetos, se puede usar la coincidencia de patrones para crear una rama a las fórmulas adecuadas para calcular estas cantidades. En el ejemplo siguiente se usan diferentes fórmulas 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
Uso de uniones discriminadas en estructuras de datos de árbol
Las uniones discriminadas pueden ser recursivas, lo que significa que la unión en sí se puede incluir en el tipo de uno o varios casos. Las uniones discriminadas recursivas se pueden usar para crear estructuras de árbol, que sirven para modelar expresiones en lenguajes de programación. En el siguiente código se usa una unión discriminada recursiva para crear una estructura de datos de árbol binaria. La unión consta de dos casos: Node
(que es un nodo con un valor entero y subárboles izquierdo y derecho) y Tip
(que 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 siguiente ilustración se muestra la estructura de árbol de myTree
.
Las uniones discriminadas funcionan bien si los nodos del árbol son heterogéneos. En el siguiente código, el tipo Expression
representa el árbol de sintaxis abstracta de una expresión en un lenguaje de programación simple que admite la suma y multiplicación de números y variables. Algunos de los casos de unión no son recursivos y representan o números (Number
) o variables (Variable
). Otros casos son recursivos y representan operaciones (Add
y Multiply
), donde los operandos también son expresiones. La función Evaluate
usa una expresión de coincidencia para procesar el árbol de sintaxis recursivamente.
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 este código se ejecuta, el valor de result
es 5.
Miembros
Es posible definir miembros en uniones discriminadas. En el siguiente ejemplo 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 suelen ser habituales en las uniones discriminadas:
[<RequireQualifiedAccess>]
[<NoEquality>]
[<NoComparison>]
[<Struct>]