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