F # 8: Uniões Discriminadas

Portanto, nossa jornada F # continua. Analisamos alguns dos tipos básicos de blocos de construção, como registros / tuplas, agora é hora de dar uma olhada nas associações marcadas.

As uniões rotuladas fornecem suporte para valores, que podem ser um dos vários valores possíveis. Os possíveis valores são conhecidos como "casos combinados" e assumem a forma mostrada abaixo:

case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 …] 

Não se preocupe se essa sintaxe parecer intimidadora, então o que realmente se resume é a presença de um rótulo para que cada caso possa ser reconhecido (distinto) dos outros e o tipo para o caso de combinação. O nome tem certas regras, como

  • Deve começar com uma letra maiúscula
  • Pode ser um identificador, incluindo o nome do próprio tipo de união. Isso pode ser um pouco confuso, mas é útil descrever o caso de mesclagem.

Aqui está um exemplo de uma identificação incorreta

imagem

E aqui está como algo assim pode parecer ao usar um identificador de marca que corresponde ao registro de união, que, como mencionado anteriormente, é perfeitamente válido

 type LabelUnionType = Int of int | String of string 

Construindo associações marcadas


Então, como construir um caso de unificação? Bem, existem diferentes maneiras, você pode usar uma das seguintes abordagens:

 let currentLabelUnionType1 = 13 printfn "let currentLabelUnionType1 = 13" printfn "%O" currentLabelUnionType1 let currentLabelUnionType2 = Int 23 printfn "let currentLabelUnionType2 = Int 23" printfn "%O" currentLabelUnionType2 printfn "%A" currentLabelUnionType2 let currentLabelUnionType3 = "Cat" printfn "let currentLabelUnionType3 = \"Cat\"" printfn "%O" currentLabelUnionType3 printfn "%A" currentLabelUnionType3 let currentLabelUnionType4 = String "Cat" printfn "let currentLabelUnionType4 = String \"Cat\"" printfn "%O" currentLabelUnionType4 printfn "%A" currentLabelUnionType4 

Que na inicialização pode fornecer os seguintes resultados quando iniciado pela função printfn (eu uso o formatador% A ou% O printfn abaixo):

imagem

Você pode usar qualquer tipo de combinação de casos como

  • Tuple
  • a documentação
  • Outros tipos

A única regra é que o tipo deve ser definido antes que seu caso de união possa usá-lo.

Aqui está um exemplo que usa um tipo de tupla em casos de união:

 type unionUsingTuples = CCY of (int * String) | Rates of (int * decimal) ..... ..... let tupledUnion = (12, "GBP") 

Uniões vazias


Você também pode usar uniões vazias. Quais são aqueles em que você não especifica um tipo. Isso os torna muito mais semelhantes aos valores de enumeração .NET padrão. Aqui está um exemplo:

 type Player = Cross | Nought .... .... let emptyUnion = Cross 

Que tal casos semelhantes por tipo


Um olho de águia, como você, pode ver o problema. O que aconteceria se tivéssemos algo assim:

 type PurchaseOrders = Orders of (string * int) | Empty type ClientOrders = Orders of (string * int) | Empty 

Isso está nos causando problemas, não é? Como distinguiríamos esses tipos de alianças demarcadas? Felizmente, podemos adotar uma abordagem totalmente qualificada para isso, para que possamos fazê-lo e tudo funcionará conforme o esperado. Deve-se observar que você pode dar um passo adiante e incluir o nome do módulo, se o módulo estiver envolvido (aprenderemos mais sobre isso mais adiante no próximo artigo):

 let purchaseOrders = PurchaseOrders.Orders ("box of 100 scrubbing brushes", 1) let clientOrders = ClientOrders.Orders ("scrubbing brush", 23) 

Comparação


Como em muitos tipos de F #, as junções delimitadas são consideradas iguais apenas se

  • O comprimento do estojo combinado é o mesmo.
  • Os tipos de seus casos de união correspondem.
  • Os valores de seus casos de associação são os mesmos.

Diferente


Aqui está um exemplo em que a igualdade não é respeitada:

 let purchaseOrders1 = PurchaseOrders.Orders ("box of 100 scrubbing brushes", 1) let purchaseOrders2 = PurchaseOrders.Orders ("10 pack of disks", 1) printfn "purchaseOrders1 = purchaseOrders2 %A" (purchaseOrders1 = purchaseOrders2) 

imagem

Igual


Aqui está um exemplo de igualdade. Isso é algo como código .NET normal, você sabe, se os membros são iguais, têm os mesmos valores e o número correto, então é quase o mesmo (se ignorarmos os códigos hash):

 let purchaseOrders1 = PurchaseOrders.Orders ("box of 100 scrubbing brushes", 1) let purchaseOrders2 = PurchaseOrders.Orders ("box of 100 scrubbing brushes", 1) printfn "purchaseOrders1 = purchaseOrders2 %A" (purchaseOrders1 = purchaseOrders2) 

imagem

Deve-se notar que não podemos usar a igualdade quando precisamos qualificar totalmente os tipos de sindicatos, pois são tipos diferentes, portanto, isso não funcionará:

imagem

Padrões de comparação


Abaixo está uma pequena função que pega uma união de cartão e exibe os casos da união com a qual foi chamada e simplesmente retorna Unit (vazio, se você se lembrar disso dos artigos anteriores desta série):

 type Card = ValueCard of int | Jack | Queen | King | Ace .... .... let cardFunction card = match card with | ValueCard i -> printfn "its a value card of %A" i | Jack -> printfn "its a Jack" | Queen -> printfn "its a Jack" | King -> printfn "its a Jack" | Ace -> printfn "its a Ace" () //return unit //shows you how to pass it in without a Let binding do cardFunction (Card.ValueCard 8) //or you could use explicit Let binding if you do desire let aceCard = Ace do cardFunction aceCard 

imagem

Então, exatamente o que acontece nos bastidores


Então agora vimos alguns exemplos de como as associações rotuladas funcionam. Então, o que, na sua opinião, poderia acontecer se tivéssemos uma biblioteca F # que usasse junções marcadas e decidíssemos usá-la em C # / VB.NET. Você acha que isso vai funcionar? Resposta: Tenho certeza de que será. Farei um post inteiro sobre a Interop em algum lugar no futuro, mas acho que talvez seja interessante considerar um pouco disso agora para junções rotuladas, pois são muito diferentes de tudo o que vemos na programação .NET padrão.

Então, vamos pegar o cartão acima, que era esse código:

 type Card = ValueCard of int | Jack | Queen | King | Ace 

E execute-o através de um descompilador como Reflector / DotPeek (tudo que você tem). Eu usei o DotPeek e obtive esse código C # para essa única linha F #. Portanto, como você pode ver, o compilador F # fez um ótimo trabalho para garantir que os tipos F # funcionem bem com o .NET comum, como o C # / VB.NET.

 using Microsoft.FSharp.Core; using System; using System.Collections; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; [CompilationMapping(SourceConstructFlags.Module)] public static class Program { [EntryPoint] public static int main(string[] argv) { return 0; } [DebuggerDisplay("{__DebugDisplay(),nq}")] [CompilationMapping(SourceConstructFlags.SumType)] [Serializable] [StructLayout(LayoutKind.Auto, CharSet = CharSet.Auto)] public class Card : IEquatable<Program.Card>, IStructuralEquatable, IComparable<Program.Card>, IComparable, IStructuralComparable { [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public int Tag { [DebuggerNonUserCode] get { return this._tag; } } [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public bool IsValueCard { [DebuggerNonUserCode] get { return this.get_Tag() == 0; } } [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public static Program.Card Jack { [CompilationMapping(SourceConstructFlags.UnionCase, 1)] get { // ISSUE: reference to a compiler-generated field return Program.Card._unique_Jack; } } [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public bool IsJack { [DebuggerNonUserCode] get { return this.get_Tag() == 1; } } [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public static Program.Card Queen { [CompilationMapping(SourceConstructFlags.UnionCase, 2)] get { // ISSUE: reference to a compiler-generated field return Program.Card._unique_Queen; } } [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public bool IsQueen { [DebuggerNonUserCode] get { return this.get_Tag() == 2; } } [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public static Program.Card King { [CompilationMapping(SourceConstructFlags.UnionCase, 3)] get { // ISSUE: reference to a compiler-generated field return Program.Card._unique_King; } } [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public bool IsKing { [DebuggerNonUserCode] get { return this.get_Tag() == 3; } } [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public static Program.Card Ace { [CompilationMapping(SourceConstructFlags.UnionCase, 4)] get { // ISSUE: reference to a compiler-generated field return Program.Card._unique_Ace; } } [CompilerGenerated] [DebuggerNonUserCode] [DebuggerBrowsable(DebuggerBrowsableState.Never)] public bool IsAce { [DebuggerNonUserCode] get { return this.get_Tag() == 4; } } static Card() { } [CompilationMapping(SourceConstructFlags.UnionCase, 0)] public static Program.Card NewValueCard(int item) { return (Program.Card) new Program.Card.ValueCard(item); } [CompilationMapping(SourceConstructFlags.UnionCase, 1)] public static Program.Card get_Jack() { // ISSUE: reference to a compiler-generated field return Program.Card._unique_Jack; } [CompilationMapping(SourceConstructFlags.UnionCase, 2)] public static Program.Card get_Queen() { // ISSUE: reference to a compiler-generated field return Program.Card._unique_Queen; } [CompilationMapping(SourceConstructFlags.UnionCase, 3)] public static Program.Card get_King() { // ISSUE: reference to a compiler-generated field return Program.Card._unique_King; } [CompilationMapping(SourceConstructFlags.UnionCase, 4)] public static Program.Card get_Ace() { // ISSUE: reference to a compiler-generated field return Program.Card._unique_Ace; } public static class Tags { public const int ValueCard = 0; public const int Jack = 1; public const int Queen = 2; public const int King = 3; public const int Ace = 4; } [DebuggerTypeProxy(typeof (Program.Card.ValueCard\u0040DebugTypeProxy))] [DebuggerDisplay("{__DebugDisplay(),nq}")] [Serializable] [SpecialName] public class ValueCard : Program.Card { [CompilationMapping(SourceConstructFlags.Field, 0, 0)] [CompilerGenerated] [DebuggerNonUserCode] public int Item { [DebuggerNonUserCode] get { return this.item; } } } [SpecialName] internal class ValueCard\u0040DebugTypeProxy { [CompilationMapping(SourceConstructFlags.Field, 0, 0)] [CompilerGenerated] [DebuggerNonUserCode] public int Item { [DebuggerNonUserCode] get { return this._obj.item; } } } } } 

Casos recursivos (estruturas em árvore)


As junções rotuladas também podem ser usadas de maneira recursiva, em que a própria união pode ser usada como um dos tipos em um ou mais casos. Isso torna as junções marcadas muito adequadas para modelar estruturas de árvores, como:

  • Expressões matemáticas
  • Árvores de sintaxe abstrata
  • Xml

De fato, o MSDN tem alguns bons exemplos.

No código a seguir, uma união rotulada recursiva é usada para criar uma estrutura de dados de árvore binária. A união consiste em dois casos: Nó, que é um nó com um valor inteiro e subárvores esquerda e direita, e Dica, que completa a árvore.

A estrutura em árvore do myTree no exemplo abaixo é mostrada na figura abaixo:

imagem

E é assim que podemos modelar o myTree usando junções marcadas. Observe como classificamos a junção marcada como um dos casos de junção. Nesse caso, os casos de combinação de

  • Dica (união vazia, atua como uma enumeração padrão no .NET)
  • Ou uma tupla de 3 dígitos de um número, Árvore, Árvore

Também deve ser observado que a função sumTree está marcada com a palavra-chave rec. O que esse feitiço faz com nossa função? Bem, isso marca sumTree funciona como aqueles que serão chamados recursivamente. Sem a palavra-chave "rec" na função sumTree, o compilador F # irá reclamar. Nesse caso, o compilador lançará o seguinte erro.

imagem

Mas somos bons rapazes e usaremos as palavras-chave certas para apoiar nosso caso de uso, por isso continuamos

 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 printfn "Value of sumTree is %A" resultSumTree 

imagem

O MSDN também tem outro bom exemplo que eu acho que valeria a pena roubar (sim, estou falando francamente sobre isso agora. Eu acho que enquanto vocês estão extraindo algo desse exemplo emprestado que, como Digo claramente que sou emprestado, não estou no negócio). Vamos dar uma olhada neste exemplo aqui:

 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 printfn "Value of sumTree is %A" result 

imagem

Source: https://habr.com/ru/post/pt470275/


All Articles