Compartir a través de


Uniones discriminadas (F#)

Las uniones discriminadas proporcionan compatibilidad con valores que pueden ser uno de los diversos casos con nombre, posiblemente cada uno con valores y tipos diferentes. Las uniones discriminadas son útiles para los datos heterogéneos; datos que pueden tener casos especiales, incluidos casos válidos y casos de error; datos cuyo tipo varía de una instancia a otra; y como alternativa a las jerarquías de objetos de tamaño reducido. Además, las uniones discriminadas se utilizan para representar estructuras de datos en forma de árbol.

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

Comentarios

Las uniones discriminadas son similares a los tipos de unión de otros lenguajes, pero hay diferencias. Al igual que los tipos de unión en C++ o los tipos variantes en Visual Basic, los datos almacenados en el valor no son datos fijos sino que pueden ser una de varias posibles opciones. A diferencia de las uniones en estos otros lenguajes, se asigna un identificador de caso a cada una de las posibles opciones. Los identificadores de caso son nombres para los diversos posibles tipos de valor que pueden tener los objetos de este tipo; los valores en sí son opcionales. Si los valores no están presentes, el caso equivale a un caso de enumeración. Si los valores están presentes, cada valor puede ser un solo valor del tipo especificado o una tupla que agrega varios campos de los mismos tipos o de tipos diferentes. A partir de F# 3.1, puede dar a un campo individual un nombre, pero el nombre es opcional, aunque otros campos del mismo caso tengan nombre.

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 Forma de unión discriminada, que puede tener valores de cualquiera de los tres casos: Rectángulo, Círculo y Prisma. Cada caso tiene un conjunto de campos diferente. El caso del Rectángulo tiene dos campos con nombre, ambos del tipo float, que tienen los nombres ancho y longitud. El caso Circle solo tiene un campo con nombre: radio. El caso Prisma tiene tres campos, dos de los cuales se denominan Sin nombre y se conocen como campos anónimos.

Para construir objetos, proporcione 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 utilizar los campos con nombre en la inicialización o puede confiar en la ordenación de los campos en la declaración y simplemente proporcionar valores para cada campo cada vez. La llamada de constructor para rect en el código anterior utiliza los campos con nombre, pero la llamada de constructor para circ usa el orden. 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 simple de la biblioteca básica 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

En el código anterior, se 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 viene está representado por el parámetro de tipo 'a. El caso None no tiene ningún valor asociado. Por consiguiente, option especifica un tipo genérico que tiene un valor de algún tipo o no tiene ningún valor. El tipo Option también tiene un alias en minúsculas, option, que se utiliza con más frecuencia.

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

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

Los identificadores de caso también se utilizan en las expresiones de coincidencia de modelos. En estas expresiones, se proporcionan identificadores para los valores asociados a los casos individuales. Por ejemplo, en el siguiente código, x es el identificador que se proporciona para el valor 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 utilizar campos con nombre para especificar coincidencias de unión discriminadas. Para el tipo Shape que declaró anteriormente, puede utilizar los campos denominados como muestra el siguiente código para extraer los valores de los campos.

let getShapeHeight shape =
    match shape with
    | Rectangle(height = h) -> h
    | Circle(radius = r) -> 2. * r
    | Prism(height = h) -> h

Normalmente, los identificadores de caso se pueden utilizar sin especificar el nombre de la unión. Si desea que se especifique siempre el nombre de la unión, aplique el atributo RequireQualifiedAccess a la definición del tipo de unión.

Usar uniones discriminadas en lugar de jerarquías de objetos

En muchas ocasiones, se puede utilizar una unión discriminada como alternativa más sencilla a una jerarquía de objetos de tamaño reducido. Por ejemplo, se puede usar la siguiente unión discriminada en lugar de una clase base Shape que tiene tipos derivados para el círculo, el 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, tal y como haría en una implementación orientada a objetos, puede utilizar la coincidencia de modelos a fin de diversificar las fórmulas adecuadas para calcular estas cantidades. En el siguiente ejemplo, según la forma, se usan diferentes fórmulas para calcular el área.

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

Usar uniones discriminadas para las estructuras de datos en árbol

Las uniones discriminadas pueden ser recursivas, lo que significa que la propia unión puede estar incluida en el tipo de uno o varios casos. Las uniones discriminadas recursivas pueden utilizarse para crear estructuras de árbol, que se emplean para modelar las expresiones en los lenguajes de programación. En el siguiente código, se utiliza una unión discriminada recursiva para crear una estructura de datos en forma de árbol binario. La unión consta de dos casos, Node, que es un nodo con un valor entero y subárboles a la izquierda y derecha, y Tip, que termina 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.

Estructura de árbol de myTree

Diagrama de árbol para uniones discriminadas

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 de un lenguaje de programación simple que admite la suma y la 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 utiliza una expresión de coincidencia para procesar el árbol de sintaxis de forma recursiva.

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

Cuando se ejecute este código, el valor de result será 5.

Vea también

Otros recursos

Referencia del lenguaje F#