F # 8: Uniones discriminadas

Entonces nuestro viaje F # continúa. Analizamos algunos de los tipos básicos de bloques de construcción, como registros / tuplas, ahora es el momento de echar un vistazo a las asociaciones marcadas.

Las uniones etiquetadas proporcionan soporte para valores, que pueden ser uno de varios valores posibles. Los valores posibles se conocen como "casos combinados" y toman la forma que se muestra a continuación:

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

No se preocupe si esta sintaxis parece intimidante, entonces lo que realmente se reduce a la presencia de una etiqueta para que cada caso pueda reconocerse (distinguirse) de los demás y el tipo para el caso de combinación. El nombre tiene ciertas reglas, como

  • Debe comenzar con una letra mayúscula
  • Puede ser un identificador, incluido el nombre de un tipo de unión. Esto puede ser un poco confuso, pero es útil describir el caso de fusión

Aquí hay un ejemplo de una mala identificación

imagen

Y así es como podría verse algo como esto cuando se usa un identificador de etiqueta que coincide con el registro de unión, que, como se mencionó anteriormente, es perfectamente válido

 type LabelUnionType = Int of int | String of string 

Construyendo asociaciones etiquetadas


Entonces, ¿cómo construir un caso de unificación? Bueno, hay diferentes maneras, puede usar uno de los siguientes enfoques:

 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 en el inicio puede dar los siguientes resultados cuando se inicia a través de la función printfn (uso el formateador% A o% O printfn a continuación):

imagen

Puede usar prácticamente cualquier tipo para combinar casos como

  • Tupla
  • la documentación
  • Otros tipos

La única regla es que el tipo debe definirse antes de que su caso sindical pueda usarlo.

Aquí hay un ejemplo que usa un tipo de tupla en casos de unión:

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

Uniones vacías


También puedes usar uniones vacías. Cuáles son aquellos en los que no especifica un tipo. Esto los hace mucho más similares a los valores de enumeración .NET estándar. Aquí hay un ejemplo:

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

¿Qué tal casos similares por tipo?


Un ojo de águila, como tú, puede ver el problema. ¿Qué pasaría si tuviéramos algo como esto?

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

Esto nos está causando problemas, ¿no es así? ¿Cómo distinguiríamos entre estos tipos de alianzas demarcadas? Afortunadamente, podemos adoptar un enfoque totalmente calificado para esto, por lo que podemos hacerlo, y todo funcionará como se espera. Debe tenerse en cuenta que puede ir un paso más allá e incluir el nombre del módulo si el módulo está involucrado (aprenderemos más sobre esto más adelante en el próximo artículo):

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

Comparación


Al igual que con muchos tipos de F #, las uniones delimitadas se consideran iguales solo si

  • La longitud de su caso combinado es la misma.
  • Los tipos de sus casos sindicales coinciden.
  • Los valores de sus casos de asociación son los mismos.

No igual


Aquí hay un ejemplo donde no se respeta la igualdad:

 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) 

imagen

Igual


Aquí hay un ejemplo de igualdad. Esto es algo así como el código .NET normal, ya sabes, si los miembros son iguales, tienen los mismos valores y su número correcto, entonces esto es casi lo mismo (si ignoramos los códigos hash que son):

 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) 

imagen

Cabe señalar que no podemos usar la igualdad cuando debemos calificar completamente los tipos de sindicatos, ya que son tipos diferentes, por lo que esto no funcionará:

imagen

Patrones de comparación


A continuación se muestra una pequeña función que toma una unión de Tarjeta y muestra los casos de la unión con la que se llamó, y simplemente devuelve Unidad (nula, si recuerda esto de los artículos anteriores de esta serie):

 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 

imagen

Entonces, ¿qué sucede exactamente detrás de escena?


Así que ahora hemos visto algunos ejemplos de cómo funcionan las asociaciones etiquetadas. Entonces, qué, en su opinión, podría suceder si tuviéramos una biblioteca F # que usara asociaciones de marcado, y decidiéramos usarla desde C # / VB.NET. ¿Crees que esto funcionará? Respuesta: Estoy seguro de que lo será. Haré una publicación completa sobre Interop en algún momento en el futuro, pero pensé que podría ser interesante considerar algo de esto ahora para las uniones marcadas, ya que son muy diferentes de todo lo que vemos en la programación estándar de .NET.

Entonces, tomemos la Tarjeta anterior, que era este código:

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

Y ejecútelo a través de un descompilador como Reflector / DotPeek (todo lo que tiene). Usé DotPeek y obtuve este código C # para esta sola línea F #. Entonces, como puede ver, el compilador de F # hizo un gran trabajo para asegurarse de que los tipos de F # funcionen bien con .NET normal, como 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 (estructuras de árbol)


Las uniones etiquetadas también se pueden usar de forma recursiva, donde la unión en sí se puede usar como uno de los tipos en uno o más casos. Esto hace que las uniones marcadas sean muy adecuadas para modelar estructuras de árbol, como:

  • Expresiones matematicas
  • Árboles de sintaxis abstracta
  • Xml

De hecho, MSDN tiene algunos buenos ejemplos.

En el siguiente código, se usa una unión etiquetada recursiva para crear una estructura de datos de árbol binario. La unión consta de dos casos: Nodo, que es un nodo con un valor entero y subárboles izquierdo y derecho, y Tip, que completa el árbol.

La estructura de árbol para myTree en el ejemplo a continuación se muestra en la figura a continuación:

imagen

Y así es como podríamos modelar myTree usando combinaciones marcadas. Observe cómo clasificamos la combinación marcada como uno de los casos de combinación. En este caso, los casos de combinar cualquiera

  • Consejo (unión vacía, actúa como una enumeración estándar en .NET)
  • O una tupla de 3 dígitos de un número, Árbol, Árbol

También debe tenerse en cuenta que la función sumTree está marcada con la palabra clave rec. ¿Qué hace este hechizo mágico con nuestra función? Bueno, esto marca las funciones sumTree como aquellas que se llamarán recursivamente. Sin la palabra clave "rec" en la función sumTree, el compilador de F # se quejará. En este caso, el compilador arrojará el siguiente error.

imagen

Pero somos buenos, y usaremos las palabras clave adecuadas para respaldar nuestro caso de uso, por lo que 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 

imagen

MSDN también tiene otro buen ejemplo que creo que valdría la pena robar (sí, estoy hablando con franqueza al respecto ahora. Creo que mientras ustedes / niñas están extrayendo algo de este ejemplo prestado que, como Digo claramente: estoy prestado, no estoy en el negocio). Veamos este ejemplo aquí:

 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 

imagen

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


All Articles