Uniões discriminadas (F#)
As uniões discriminadas fornecem suporte para os valores que podem ser alguns casos nomeados, possivelmente cada um com valores e tipos diferentes. As uniões discriminadas são úteis para dados heterogêneos; os dados que podem ter casos especiais, inlcuindo casos válidos e de erro; dados que variam em tipo de uma instância para outra e como uma alternativa para hierarquias de objetos pequenos. Além disso, as uniões discriminadas recursivas são usadas para representar estruturas de dados de árvore.
type type-name =
| case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 ...]
| case-identifier2 [of [fieldname3 : ]type3 [ * [ fieldname4 : ]type4 ...]
...
Comentários
As uniões discriminadas são semelhantes à tios de união em outros idiomas, mas há diferenças. Tal como um tipo de união no C++ ou um tipo de variante em Visual Basic, os dados armazenados no valor não são corrigidos; pode ser uma das várias opções diferentes. Ao contrário de uniões em outras linguagens, no entanto, cada uma das opções possíveis recebe um identificador de casos. Os identificadores de caso são nomes para os diversos tipos de valores possíveis 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 valor único de um tipo especificado ou um tuple que agregue vários campos do mesmo tipo ou de tipos diferentes. A partir do F# 3.1, você pode dar a um campo individual um nome, mas o nome é opcional, mesmo se outros campos no mesmo caso forem nomeados.
Por exemplo, considere a seguinte declaração de um tipo Forma.
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 forma discriminada de união, que pode ter valores de qualquer um dos três casos: Rectangle, Circle e Prism. Cada caso têm um conjunto diferente de campos. O caso Rectangle tem dois campos nomeados, ambos do tipo float, que têm os nomes width e length. O caso de círculo tem apenas um campo nomeado, raio. O caso Prism têm três campos, dois dos quais são nomeados Unnamed e referidos como campos anônimos.
Você constrói objetos fornecendo valores para os campos nomeados e anônimos de acordo com os seguintes exemplos.
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 você pode confiar na ordem dos campos na declaração e apenas fornecer os valores para cada campo sucessivamente. A chamada do construtor para rect no código anterior usa os campos nomeados, mas a chamada do construtor para circ usa a ordenação. Você pode combinar os campos ordenados e nomeados, como a compilação de prism.
O tipo option é uma união discriminada simples na biblioteca principal F#. O tipo de option é declarado como a seguir.
// The option type is a discriminated union.
type Option<'a> =
| Some of 'a
| None
O código anterior que especifica 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 valor associado. O tipo option especifica um tipo genérico que tem um valor de qualquer tipo ou nenhum valor. O tipo Option também tem um alias de tipo em minúscula, option, que é mais comumente usado.
Os identificadores dos casos podem ser usados como construtores para o tipo discriminado de união. 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 correspondência de padrões. Em uma expressão de correspondência de padrão, 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 correspondência de padrão, você pode usar campos nomeados para especificar correspondências de união discriminadas. Para o tipo Forma declarado anteriormente, você pode usar os campos nomeados como o código a seguir mostra para extrair os valores dos campos.
let getShapeHeight shape =
match shape with
| Rectangle(height = h) -> h
| Circle(radius = r) -> 2. * r
| Prism(height = h) -> h
Normalmente, os identificadores de caso podem ser usados sem qualificá-los com o nome da união. Se você desejar que o nome seja sempre qualificado com um nome da união, poderá aplicar o atributo RequireQualifiedAccess à definição de tipo de união.
Usando uniões discriminadas em vez de hierarquias de objeto
Você geralmente usa uma união discriminada como uma alternativa mais simples para uma pequena hierarquia de objeto. Por exemplo, a seguinte união discriminada poderia ser usada em vez de uma classe base Shape com tipos derivados para círculo, quadrado e assim por diante.
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ê usaria em uma implementação orientada a objeto, será possível usar a correspondência de padrão para ramificar para fórmulas apropriadas para 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 é a seguinte:
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
Usando 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 recursivas discriminadas podem ser usadas para criar as estruturas de árvore, que são usadas para modelar expressões nas 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 finaliza 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 de árvore para myTree.
Estrutura de árvore para o myTree
As uniões discriminadas funcionarão bem se os nós na árvore forem 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), onde 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.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
Quando esse código é executado, o valor de result é 5.