[Перевод] F#8: Разграниченные/размеченные объединения(Discriminated Unions)

Итак, наше путешествие F # продолжается. Мы рассмотрели некоторые основные типы строительных блоков, такие как записи / кортежи, теперь пришло время взглянуть на размеченные объединения.
Размеченные союзы обеспечивают поддержку значений, которые могут быть одним из нескольких возможных значений. Возможные значения известны как «объединенные случаи», и принимают форму, показанную ниже:
case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 …]

Не беспокойтесь, если этот синтаксис выглядит пугающе, то, к чему он действительно сводится, — это наличие метки, чтобы каждый случай можно было распознать (отличить) от других, и типа для случая объединения. Название имеет определенные правила, такие как

  • Должен начинаться с заглавной буквы
  • Может быть идентификатором, включая само имя типа объединения. Это может немного сбить с толку, но полезно описать случай объединения


 

Вот пример плохого идентификатора
image
И вот как может выглядеть нечто подобное при использовании идентификатора метки, который совпадает с регистром объединения, который, как было сказано ранее, совершенно действителен

type LabelUnionType = Int of int | String of string

Построение размеченных объединений


Так как же построить случай объединения? Ну, есть разные способы, вы можете использовать один из следующих подходов:

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

Который при запуске может дать следующие результаты при запуске через функцию printfn (я использую форматтер %A или %O printfn ниже):
image
Вы можете в значительной степени использовать любой тип в случаях объединения, таких как

  • Кортеж
  • документация
  • Другие типы

Единственное правило заключается в том, что тип должен быть определен до того, как ваш случай объединения сможет использовать его

Вот пример, который использует тип кортежа в случаях объединения:

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

Пустые Союзы


Вы также можете использовать пустые союзы. Которые являются теми, где вы не указываете тип. Это делает их намного более похожими на стандартные значения перечисления .NET. Вот пример:

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

Как насчет аналогичных случаев по типам


Орлиный глаз, типа Вас, может увидеть проблему. Что случилось бы, если бы у нас было что-то подобное:

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


Это вызывает у нас проблемы, не так ли. Как бы мы различали эти типы разграниченных союзов? К счастью, мы можем использовать полностью квалифицированный подход к этому, поэтому мы можем просто сделать это, и все будет работать, как ожидалось. Следует отметить, что вы можете сделать еще один шаг вперед и включить имя модуля, если модуль задействован (об этом мы узнаем позже, в следующей статье):

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

Сравнение


Как и во многих типах F#, разграниченные объединения считаются равными, только если

  • Длина их объединенного случая совпадает
  • Типы их случаев объединения совпадают
  • Значения их случаев объединения совпадают

Не равны


Вот пример, где не соблюдается равенство

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)


image

Равны


Вот пример равенства. Это что-то вроде обычного кода .NET, вы знаете, если члены одинаковые, они имеют одинаковые значения и их правильное число, то это почти одно и то же (если мы игнорируем хеш-коды, которые есть)

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)


image
Следует отметить, что мы не можем использовать равенство, когда мы должны полностью квалифицировать типы объединений, поскольку они являются различными типами, поэтому это не будет работать
image

Паттерны сравнения


Ниже показана небольшая функция, которая принимает объединение Card и выводит случаи объединения, с которыми она была вызвана, и просто возвращает Unit (void, если вы помните это из предыдущих статей этой серии)

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


image

Так что именно то, что происходит за кулисами


Итак, теперь мы увидели несколько примеров того, как работают размеченные объединения. Итак, что, по вашему мнению, могло бы произойти, если бы у нас была библиотека F#, которая использовала размеченные объединения, и мы решили использовать ее из C # / VB.NET. Как вы думаете, это будет работать. Ответ: уверен, что будет. Я сделаю целый пост о Interop где-нибудь в будущем, но я просто подумал, что может быть интересно рассмотреть часть этого прямо сейчас для размеченных объединений, поскольку они настолько отличаются от всего, что мы видим в стандартном программировании .NET.

Итак, давайте возьмем Card выше, который был этот код:

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


И запустите его через декомпилятор, такой как Reflector / DotPeek (все, что у вас есть). Я использовал DotPeek и получил этот код C# для этой единственной строки F#. Итак, как вы можете видеть, компилятор F# проделал большую работу, чтобы убедиться, что типы F# будут хорошо взаимодействовать с обычными .NET, такими как 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, IStructuralEquatable, 
    IComparable, 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;
        }
      }
    }
  }
}

Рекурсивные случаи (древовидные структуры)


Размеченные объединения могут также использоваться рекурсивным способом, где само объединение может использоваться как один из типов в одном или нескольких случаях. Это делает размеченные объединения очень подходящими для моделирования древовидных структур, таких как:

  • Математические выражения
  • Абстрактные синтаксические деревья
  • Xml

На самом деле MSDN имеет несколько хороших примеров на этот счет

В следующем коде рекурсивное размеченное объединение используется для создания структуры данных двоичного дерева. Объединение состоит из двух случаев: Node, который является узлом с целочисленным значением и левым и правым поддеревьями, и Tip, который завершает дерево.

Древовидная структура для myTree в приведенном ниже примере показана на рисунке ниже:
image
И вот как мы могли бы смоделировать myTree, используя размеченные объединения. Обратите внимание, как мы относим само размеченное объединение в качестве одного из случаев объединения. В этом случае случаи объединения либо

  • Tip (пустое объединение, действует как стандартное перечисление в .NET)
  • Или 3-х значный кортеж из числа, Tree, Tree


Следует также отметить, что функция sumTree помечается ключевым словом rec. Что это волшебное заклинание делает с нашей функцией? Ну, это помечает функции sumTree как те, которые будут вызываться рекурсивно. Без ключевого слова «rec» в функции sumTree компилятор F# будет жаловаться. В этом случае компилятор выдаст следующую ошибку.
image
Но мы хорошие ребята, и мы будем использовать правильные ключевые слова для поддержки нашего варианта использования, поэтому мы продолжаем

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


image
У MSDN также есть еще один хороший пример, который, я думаю, стоило бы украсть (да, сейчас я откровенно об этом говорю. Я думаю, что пока вы, ребята/девочки, извлекаете что-то из этого заимствованного примера, который, как я ясно говорю, заимствован, я не при делах). Давайте посмотрим на этот пример здесь:

type Expression = 
    | Number of int
    | Add of Expression * Expression
    | Multiply of Expression * Expression
    | Variable of string
....
....
....
let rec Evaluate (env:Map) 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


image

© Habrahabr.ru