Uniões discriminadas

As uniões discriminadas fornecem suporte para valores que podem ser um dos vários casos nomeados, possivelmente cada um com diferentes valores e tipos. Elas são úteis para dados heterogêneos: dados que podem ter casos especiais, incluindo casos válidos e de erro, dados que variam no tipo de uma instância para outra e como alternativa para hierarquias de objetos pequenos. Além disso, as uniões discriminadas recursivas são usadas para representar estruturas de dados de árvore.

Sintaxe

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

    [ member-list ]

Comentários

As uniões discriminadas são semelhantes aos tipos de união em outras linguagens, mas há diferenças. Assim como acontece com um tipo de união em C++ ou um tipo de variante no Visual Basic, os dados armazenados no valor não são fixos. Ele pode ser uma das várias opções distintas. No entanto, ao contrário das uniões nessas outras linguagens, cada uma das opções possíveis recebe um identificador de caso. Os identificadores de caso são nomes para os vários tipos possíveis de valores que os objetos desse tipo podem ser. Os valores são opcionais. Se os valores não estiverem presentes, o caso será equivalente a um caso de enumeração. Se os valores estiverem presentes, cada valor poderá ser um só valor de um tipo especificado ou uma tupla que agrega vários campos dos mesmos tipos ou de tipos diferentes. Você pode dar um nome a um campo individual, mas o nome é opcional, mesmo que outros campos do mesmo caso sejam nomeados.

A acessibilidade para uniões discriminadas usa public como padrão.

Por exemplo, considere a declaração a seguir de um tipo Shape.

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

O código anterior declara uma Shape de união discriminada, que pode ter valores de um destes três casos: Rectangle, Circle e Prism. Cada caso tem um conjunto diferente de campos. O caso Rectangle tem dois campos nomeados, ambos do tipo float, que têm os nomes largura e comprimento. O caso Circle tem apenas um campo nomeado, raio. O caso Prism tem três campos, dois dos quais (largura e altura) são campos nomeados. Os campos sem nome são chamados de campos anônimos.

Você constrói objetos fornecendo valores para os campos nomeados e anônimos de acordo com os exemplos a seguir.

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

Esse código mostra que você pode usar os campos nomeados na inicialização ou contar com a ordenação dos campos na declaração e apenas fornecer os valores para cada campo por vez. A chamada do construtor a rect no código anterior usa os campos nomeados, mas a chamada do construtor a circ usa a ordenação. Você pode combinar os campos ordenados e os campos nomeados, como na construção de prism.

O tipo option é uma união discriminada simples na biblioteca principal F#. O tipo option é declarado desta maneira.

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

O código anterior especifica que o tipo Option é uma união discriminada que tem dois casos, Some e None. O caso Some tem um valor associado que consiste em um campo anônimo cujo tipo é representado pelo parâmetro de tipo 'a. O caso None não tem nenhum valor associado. Portanto, o tipo option especifica um tipo genérico que tem um valor de algum tipo ou nenhum valor. O tipo Option também tem um alias de tipo de letra minúscula, option, que é mais comumente usado.

Os identificadores de caso podem ser usados como construtores para o tipo de união discriminada. Por exemplo, o código a seguir é usado para criar valores do tipo option.

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

Os identificadores de caso também são usados em expressões de padrões correspondentes. Em uma expressão de padrões correspondentes, os identificadores são fornecidos para os valores associados aos casos individuais. Por exemplo, no código a seguir, x é o identificador dado o valor associado ao caso Some do tipo option.

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

Em expressões de padrões correspondentes, você pode usar campos nomeados para especificar correspondências de união discriminada. Para o tipo Shape declarado anteriormente, use os campos nomeados como mostra o código a seguir para extrair os valores dos campos.

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

Normalmente, os identificadores de caso podem ser usados sem qualificá-los com o nome da união. Caso deseje que o nome seja sempre qualificado com o nome da união, aplique o atributo RequireQualifiedAccess à definição de tipo de união.

Como desencapsular uniões discriminadas

Em F#, as uniões discriminadas costumam ser usadas na modelagem de domínio para encapsular um só tipo. É fácil extrair o valor subjacente por meio de padrões correspondentes também. Você não precisa usar uma expressão de correspondência para um só caso:

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

O exemplo a seguir demonstra este:

type ShaderProgram = | ShaderProgram of id:int

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

Os padrões correspondentes também são permitidos diretamente em parâmetros de função, para que você possa desencapsular um só caso neles:

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

Uniões discriminadas como structs

Você também pode representar uniões discriminadas como structs. Isso é feito com o atributo [<Struct>].

[<Struct>]
type SingleCase = Case of string

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

Como esses são tipos de valor e não tipos de referência, há considerações extras em comparação com uniões discriminadas de referência:

  1. Eles são copiados como tipos de valor e têm uma semântica de tipo de valor.
  2. Não é possível usar uma definição de tipo recursivo com uma união discriminada como struct de multicaso.
  3. Você precisa fornecer nomes de casos exclusivos para uma união discriminada como struct de multicaso.

Como usar uniões discriminadas em vez de hierarquias de objetos

Muitas vezes, você pode usar uma união discriminada como uma alternativa mais simples a uma hierarquia de objetos pequenos. Por exemplo, a união discriminada a seguir pode ser usada em vez de uma classe base Shape que tenha tipos derivados para círculo, quadrado 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

Em vez de um método virtual para calcular uma área ou um perímetro, como você usará em uma implementação orientada a objeto, você poderá usar padrões correspondentes para criar ramificações para as fórmulas apropriadas a fim de calcular essas quantidades. No exemplo a seguir, fórmulas diferentes são usadas para calcular a área, dependendo da 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)

A saída é da seguinte maneira:

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

Como usar uniões discriminadas para estruturas de dados de árvore

As uniões discriminadas podem ser recursivas, o que significa que a própria união pode ser incluída no tipo de um ou mais casos. As uniões discriminadas recursivas podem ser usadas para criar estruturas de árvore, que são usadas para modelar expressões em linguagens de programação. No código a seguir, uma união discriminada recursiva é usada para criar uma estrutura de dados de árvore binária. A união consiste em dois casos: Node, que é um nó com um valor inteiro e subárvores esquerda e direita, e Tip, que encerra a árvore.

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

No código anterior, resultSumTree tem o valor 10. A ilustração a seguir mostra a estrutura da árvore para myTree.

Diagram that shows the tree structure for myTree.

As uniões discriminadas funcionam bem se os nós na árvore são heterogêneos. No código a seguir, o tipo Expression representa a árvore de sintaxe abstrata de uma expressão em uma linguagem de programação simples que dá suporte à adição e à multiplicação de números e variáveis. Alguns dos casos de união não são recursivos e representam números (Number) ou variáveis (Variable). Outros casos são recursivos e representam operações (Add e Multiply), em que os operandos também são expressões. A função Evaluate usa uma expressão de correspondência para processar recursivamente a árvore de sintaxe.

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

Quando esse código é executado, o valor de result é 5.

Membros

É possível definir membros em uniões discriminadas. O seguinte exemplo mostra como definir uma propriedade e implementar uma 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}"

Atributos comuns

Os seguintes atributos são comumente vistos em uniões discriminadas:

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

Confira também