Перечисления и дискриминированные объединения

Последнее обновление: 14.12.2023

Перечисления

Перечисления представляет тип данных, который определяет набор констант. Значение перечисления может представлять только одну из этих констант. Каждой константе перечисления надо присвоить числовое значение. Например:

type Color =
    | Red = 0
    | Green = 1
    | Blue = 2

Здесь определено перечисление Color, которое условно представляет цвет и имеет три константы. Каждой константе сопоставлено числовое значение.

Затем мы можем использовать это перечисление:

type Color =
    | Red = 0
    | Green = 1
    | Blue = 2

// определяем переменную перечисления
let  color = Color.Red

// проверяем значение перечисления
if color = Color.Red then printfn "красный" 
elif color = Color.Green then printfn "зеленый" 
else printfn "синий" 

В данном случае определяется значение color, которое представляет одну из констант перечисления.

Поскольку константам перечисления сопоставляется число, то значения перечисления мы можем преобразовать к числовому типу:

type Color =
    | Red = 0
    | Green = 1
    | Blue = 2

let  color = Color.Blue

printfn "%d" (int color)  // преобразуем значение color к типу int
printfn "%d" (int Color.Red)  // преобразуем Color.Red к типу int

Дискриминированные объединения

Дискриминированные объединения или discriminated unions позволяют определить набор возможных вариантов, которые может иметь значение. Для определения объединений применяется оператор type, после которого после оператора | указываются возможные варианты данного объединения:

type имя_объединения =
    | случай_1
    | случай_2
    | случай_N

Например, определим простейшее объединение:

// дискриминированное объединение
type FamilyStatus =
    | Married
    | Single
    | Complicated

let status: FamilyStatus = Married

if status = Married then
    printfn "женат/замужем"
elif status = Single then
    printfn "холост/не замужем"
elif status = Complicated then
    printfn "все сложно"

Здесь определено дискриминированное объединение FamilyStatus, которое определяет три варианта. Если мы определим значение этого объединения, то оно сможет принимать только один из трех вариантов - Married, Single или Complicated. Например, далее определяем значение status, которое представляет тип FamilyStatus, а точнее его вариант Married. Затем с помощью конструкции if..then..elif проверяем это значение.

Подобным образом можно использовать объединения в комплексных типах:

type FamilyStatus =
    | Married
    | Single
    | Complicated

type Person = 
    { Name:string
      Age:int 
      Status: FamilyStatus}

let tom = { 
    Name= "Tom"
    Age= 39
    Status= Single
}

printfn $"Name: {tom.Name}  Age: {tom.Age}  Status: {tom.Status}"

В данном случае в типе Person свойство Status представляет объединение FamilyStatus и поэтому может хранить только одно из значений этого объединения.

Члены объединения

Кроме собственно вариантов объединения могут иметь свойства и методы, которые определяются с помощью ключевого слова member:

type FamilyStatus =
    | Married
    | Single
    | Complicated
    // свойство Type
    member this.Type = 
        if this = Married then "женат/замужем"
        elif this = Single then "холост/не замужем"
        else "все сложно"
    // метод Print 
    member this.Print() = 
        printfn "Семейный статус: %s" this.Type

type Person = 
    { Name:string
      Age:int 
      Status: FamilyStatus}

let tom = { 
    Name= "Tom"
    Age= 39
    Status= Single
}

tom.Status.Print()  // Семейный статус: холост/не замужем

Здесь в объединении FamilyStatus определено свойство Type, которое возвращает определенную строку на основе текущего варианта. Текущий вариант можно получить с помощью ключевого слова this.

Кроме того, определена функция Print, которая выводит значение свойства Type на консоль.

Для определения свойства Type и метода Print применяется ключевое слово this, так как они относятся к одному значению объединения, зависят от этого значения. Если значения разные, то и результат работы свойства Type и метода Print будут разные. Для обращения к таким свойствам и функциями объединения внутри этого объединения также применяется ключевое слово this

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

type FamilyStatus =
    | Married
    | Single
    | Complicated
    // статическое свойство Description
    static member Description = "Семейный статус"


printfn "%s" FamilyStatus.Description

Здесь определено статическое свойство Description, которое не зависит от конкретного значения и представляет строку с описанием объединения. Обратиться к статическим свойствам и методам мы можем через имя объединения.

Тип вариантов

По умолчанию варианты объединения представляют текущий тип объединения. Но с помощью оператора of можно задать вариантам другой тип. Например:

type Contact = 
    | Email of string
    | Phone of int64

let format data = 
    match data with
    | Email emailAddr -> emailAddr
    | Phone number -> $"+{number}"

let contact1 = Email("some@brrmail.com")
let contact2 = Phone(79876543210L)

printfn "%s" (format contact1)
printfn "%s" (format contact2)

Здесь определено объединение Contact, которое представляет два варианта - Email (адрес электронной почты) и Phone (номер телефона). Причем вариант Email представляет строку, а вариант Phone - тип int64. При определении значений объединения Contact в скобках можно передать конкретные значения соответствующего типа:

let contact1 = Email("some@brrmail.com")
let contact2 = Phone(79876543210L)

Для форматированного вывода значений объединения на консоль определена функция format, которая принимает значение Contact через параметр data и форматирует его в строку. Для этого применяется конструкция match, которая сопоставляет значение data с некоторым шаблоном. В данном случае шаблоны представляют варианты объединения:

match data with
    | Email emailAddr -> emailAddr
    | Phone number -> $"+{number}"

Шаблоны указываются после оператора |. Первый шаблон представляет вариант Email. Если значение data представляет этот вариант, то это значение помещается в значение emailAddr, которое возвращается из функции.

Email emailAddr -> emailAddr

Если data представляет вариант Phone, то данные помещаются в значение number и возвращаются в виде строки $"+{number}"

В итоге в данном случае мы получим следующий консольный вывод:

some@brrmail.com
+79876543210

Поля вариантов объединений

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

type имя_объединения =
    | вариант1 of поле1 : тип_поля_1 [ * поле2 : тип_поля2 ...]
    | вариант1 of поле1 : тип_поля_1 [ * поле2 : тип_поля2 ...]

После названия варианта идет оператор of, после которого указывается имя поля и через двоеточие тип этого поля. Если полей несколько, то они отделяются символом звездочки *.

Например, в качестве контактов конкретного человека можно использовать различные критерии - адрес электронной почты, номер телефона, почтовый адрес и т.д. Мы могли бы отразить эти варианты следующим образом:

type Contact = 
    | Email
    | Phone
    | PostAddress

Но, допустим, мы хотим, чтобы вместе с вариантом Phone также был доступен код страны и номер телефона, вместе с вариантом Email - адрес электронной почты, а вместе с PostAddress - страна, город, улица и прочая сопутствующая информация. И здесь важно отметить, что все три варианта могут использоваться как альтернативы - например, кто-то при регистрации указал номер телефона, кто-то email, кто-то почтовый адрес, но эти варианты представляют гетерогенные данные, которые нельзя свести к какому-то единому набору свойств: для почтового адреса - одни данные, для email - другие и т.д. Тогда мы могли бы определить следующее объединение:

type Contact = 
    | Email of emailAddress: string
    | Phone of code: int * number: string
    | PostAddress of country: string * city: string * street: string * building:string


let contact1 = Email("some@brrmail.com")
let contact2 = Phone(1, "654-321-4590")
let contact3 = PostAddress(country="Римская империя", city="Помпеи", street = "улица Цицерона", building="20A")

printfn $"{contact1}"
printfn $"{contact2}"
printfn $"{contact3}"

В данном случае для варианта Email определено одно поле - emailAddress. При определении значения после названия варианта в скобках передается значение для данного поля.

let contact1 = Email("some@brrmail.com")

Если послей несколько, то можно передать им значения по позиции:

let contact2 = Phone(1, "654-321-4590")

То есть первое значение передается первому полю, второе значение - второму полю и так далее

Также можно передать значения по именно:

let contact3 = PostAddress(country="Римская империя", city="Помпеи", street = "улица Цицерона", building="20A")

Консольный вывод:

Email "some@brrmail.com"
Phone (1, "654-321-4590")
PostAddress ("Римская империя", "Помпеи", "улица Цицерона", "20A")

Однако консольный вывод может нас не устроить - мы хотим получить единый адрес, а не набор разрозненных компонентов с именем варианта. Определим дополнительные свойство и функцию для форматированного вывода данных:

type Contact = 
    | Email of emailAddress: string
    | Phone of code: int * number: string
    | PostAddress of country: string * city: string * street: string * building:string

    member this.Data = 
        match this with
        | Email(emailAddress) -> emailAddress
        | Phone(code, number) -> $"+{code}-{number}"
        | PostAddress(country, city, street, building) ->  $"{country}, {city}, {street}, {building}"

    member this.Print() =  printfn "%s" this.Data


let contact1 = Email("some@brrmail.com")
let contact2 = Phone(1, "654-321-4590")
let contact3 = PostAddress(country="Римская империя", city="Помпеи", street = "улица Цицерона", building="20A")

contact1.Print()
contact2.Print()
contact3.Print()

Здесь добавлено свойство Data, которое использует конструкцию match. Данная конструкция сопоставляет данные текущего значения типа Contact с тремя вариантами. Чтобы получить поля всех трех вариантов, эти поля указываются в скобках, например,

| Email(emailAddress) -> emailAddress

А после оператора -> указывается значение, которое будет возвращаться, если this представляет соответствующий вариант.

И в этом случае мы получим следующий консольный вывод:

some@brrmail.com
+1-654-321-4590
Римская империя, Помпеи, улица Цицерона, 20A

Разложение объединения

Мы не можем напрямую получить поля варианта объединения типа contact3.city, но F# позволяет разложить значение объединения на отдельные поля:

type Contact = 
    | Email of emailAddress: string
    | Phone of code: int * number: string
    | PostAddress of country: string * city: string * street: string * building:string

let contact3 = PostAddress(country="Римская империя", city="Помпеи", street = "улица Цицерона", building="20A")
//  раскладываем contact3 на отдельные компоненты
let (PostAddress (country, city, street, building)) = contact3
printfn $"{country}"    // Римская империя
printfn $"{city}"       // Помпеи
printfn $"{street}"     // улица Цицерона
printfn $"{building}"   // 20A

Представление иерархических данных

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

type Expression =
    | Number of int
    | Add of Expression * Expression
    | Multiply of Expression * Expression

let rec evaluate exp =
    match exp with
    | Number n -> n
    | Add(x, y) -> evaluate x + evaluate y
    | Multiply(x, y) -> evaluate x * evaluate y

// result = 8 + (2 * 3) 
let expressionTree = Add(Number 8, Multiply(Number 2, Number 3))
let result = evaluate expressionTree
printfn "%d" result     // 14

В данном случае объединение Expression представляет дерево выражений. Для простоты в нем определено только три варианта. Вариант Number представляет число. Варианты Add и Multiply представляют соответственно операции сложения и умножения и определяют два выражения Expression, которые могут представлять либо числа, либо другие операции сложения и умножения.

Для выполнения выражения определена рекурсивная функция evaluate. Она принимает выражение Expression и проверяет его вариант. Если это вариант Number, то есть число, то возвращается данное число. Если это вариант Add (то есть операция сложения), то получаем оба выражения в x и y, передаем их опять же в функцию evaluate и выполняем сложение результатов. Если вариант - Multiply, то аналогичным образом выполняем умножение.

Далее определяем дерево выражений, которое мы хотим выполнить:

let expressionTree = Add(Number 8, Multiply(Number 2, Number 3))

То есть фактически в данном случае мы выполняем выражение

8 + (2 * 3)

Аналогичным образом можно создавать более сложные выражения:

// result = 10 + (8 + (2 * 3))
let expressionTree = 
    Add(
        Number 10, Add(
            Number 8, Multiply(Number 2, Number 3)
        )
    )

Объединения-структуры

По умолчанию дискриминированные объединения представляют ссылочные типы, которые размещаются в хипе. Однако мы также можем определить объединения-структуры, которые будут размещаться в стеке. Для этого перед объединением указывается атрибут []

[<Struct>]
type Contact = 
    | Email
    | Phone
    | PostAddress
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850