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 para datos heterogéneos; datos que pueden tener casos especiales, incluidos casos válidos y de error; datos que varían en el tipo de una instancia a otra; y como alternativa para jerarquías de objetos pequeños. Además, las uniones discriminadas recursivas se usan 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 ]
Observaciones
Las uniones discriminadas son similares a los tipos de unión en otros lenguajes, pero hay diferencias. Al igual que con un tipo de unión en C++ o un tipo variant en Visual Basic, los datos almacenados en el valor no son fijos; puede ser una de varias opciones distintas. A diferencia de las uniones en estos otros lenguajes, a cada una de las opciones posibles, sin embargo, se le asigna un identificador de caso . Los identificadores de caso son nombres para los diferentes tipos posibles de valores que los objetos de este tipo podrían tener; los valores son opcionales. Si los valores no están presentes, el caso es equivalente a un caso de enumeración. Si los valores están presentes, cada valor puede ser un único valor de un tipo especificado o una tupla que agrega varios campos de los mismos tipos o diferentes. Puede asignar un nombre a un campo individual, pero el nombre es opcional, incluso si se denominan otros campos 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, que puede tener valores de cualquiera de los tres casos: Rectángulo, Círculo y Prism. Cada caso tiene un conjunto diferente de campos. El caso Rectángulo tiene dos campos con nombre, ambos de tipo float
, que tienen los nombres ancho y longitud. El caso Circle 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 crean 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 confiar en el orden de los campos de la declaración y simplemente proporcionar los valores de cada campo a su vez. La llamada del constructor para rect
en el código anterior usa los campos con nombre, pero la llamada del constructor para circ
usa la ordenación. Puede mezclar los campos ordenados y los campos con nombre, 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 manera.
// 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 está representado por el parámetro de tipo 'a
. El None
caso no tiene ningún valor asociado. Por lo tanto, el tipo option
especifica un tipo genérico que puede tener un valor de algún tipo o no tener 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 casos se pueden usar como constructores para el tipo de unión discriminada. Por ejemplo, el código siguiente se usa para crear valores del tipo option
.
let myOption1 = Some(10.0)
let myOption2 = Some("string")
let myOption3 = None
Los identificadores de casos 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 con el valor que está asociado al caso Some
del tipo option
.
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 shape declarado 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 de tipo de unión.
Desencapsular uniones discriminadas
En F#, las uniones discriminadas se usan con frecuencia en el modelado de dominios para encapsular un solo tipo. También es 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 solo caso:
let ([UnionCaseIdentifier] [values]) = [UnionValue]
En el ejemplo siguiente se muestra lo siguiente:
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 hace con el atributo [<Struct>]
.
[<Struct>]
type SingleCase = Case of string
[<Struct>]
type Multicase =
| Case1 of string
| Case2 of int
| Case3 of double
Dado que son tipos de valor y no tipos de referencia, hay consideraciones adicionales en comparación con 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 struct de varios casos.
Antes de F# 9, había un requisito para cada caso para especificar un nombre de caso único (dentro de la unión). A partir de F# 9, se levanta la limitación.
Usar 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, la unión discriminada siguiente podría usarse en lugar de una clase base de Shape
que tenga tipos derivados para círculos, cuadrados, 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 un método virtual para calcular un área o perímetro, como usaría en una implementación orientada a objetos, puede usar la coincidencia de patrones para bifurcar 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 la siguiente:
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 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, Node
, que es un nodo con un valor entero y con 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 ilustración siguiente se 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 Expression
representa el árbol de sintaxis abstracta de una expresión en un lenguaje de programación simple que admite la adición y 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 (Add
y Multiply
), donde los operandos también son expresiones. La función Evaluate
usa una expresión de coincidencia para procesar recursivamente 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.
Uniones discriminadas mutuamente recursivas
Las uniones discriminadas en F# pueden ser mutuamente recursivas, lo que significa que varios tipos de unión pueden hacer referencia entre sí de forma recursiva. Esto resulta útil al modelar estructuras jerárquicas o interconectadas. Para definir uniones discriminadas mutuamente recursivas, use la palabra clave and
.
Por ejemplo, considere una representación abstracta del árbol de sintaxis (AST) donde las expresiones pueden incluir instrucciones y las instrucciones pueden contener expresiones:
type Expression =
| Literal of int
| Variable of string
| Operation of string * Expression * Expression
and Statement =
| Assign of string * Expression
| Sequence of Statement list
| IfElse of Expression * Statement * Statement
Miembros
Es posible definir miembros en sindicatos discriminados. 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}"
Propiedades .Is*
en casos
Desde F# 9, las uniones discriminadas exponen propiedades .Is*
generadas automáticamente para cada caso, lo que le permite comprobar si un valor es de un caso determinado.
Así es como se puede usar:
type Contact =
| Email of address: string
| Phone of countryCode: int * number: string
type Person = { name: string; contact: Contact }
let canSendEmailTo person =
person.contact.IsEmail // .IsEmail is auto-generated
Atributos comunes
Los atributos siguientes se ven normalmente en uniones discriminadas:
[<RequireQualifiedAccess>]
[<NoEquality>]
[<NoComparison>]
[<Struct>]