Поделиться через


Дискриминированные профсоюзы

Дискриминированные профсоюзы обеспечивают поддержку значений, которые могут быть одним из нескольких именованных случаев, возможно, каждый из которых имеет разные значения и типы. Дискриминированные объединения полезны для разнородных данных; данные, которые могут иметь особые случаи, включая как допустимые, так и ошибочные ситуации; данные, которые могут изменяться по типу от экземпляра к экземпляру; и как альтернатива небольшим иерархиям объектов. Кроме того, рекурсивные размеченные объединения используются для представления древовидных структур данных.

Синтаксис

[ attributes ]
type [accessibility-modifier] type-name =
    | case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 ...]
    | case-identifier2 [of [fieldname3 : ]type3 [ * [ fieldname4 : ]type4 ...]

    [ member-list ]

Замечания

Дискриминированные профсоюзы похожи на типы профсоюзов на других языках, но существуют различия. Как и в случае с типом объединения в C++ или типом варианта в Visual Basic, данные, хранящиеся в значении, не фиксированы; это может быть один из нескольких вариантов. В отличие от объединений в этих других языках, однако, каждому из возможных вариантов присваивается идентификатор варианта . Идентификаторы регистра — это имена различных возможных типов значений, которые могут быть объектами этого типа; значения являются необязательными. Если значения отсутствуют, случай эквивалентен случаю перечисления. Если значения присутствуют, каждое значение может быть одним значением указанного типа или кортежем, который объединяет несколько полей одного или разных типов. Вы можете дать отдельное поле имя, но имя является необязательным, даже если другие поля в том же случае именуются.

Доступность для дискриминируемых объединений имеет значение по умолчанию public.

Например, рассмотрим следующее объявление типа Shape.

type Shape =
    | Rectangle of width : float * length : float
    | Circle of radius : float
    | Prism of width : float * float * height : float

Предыдущий код объявляет дискриминированное объединение Shape, значение которой может быть одним из трех возможных вариантов: прямоугольник, круг и призма. В каждом случае имеется другой набор полей. В объекте Rectangle есть два именованных поля типа floatс именами width и length. В случае "Круг" есть только одно именованное поле, радиус. В объекте Prism есть три поля, два из которых (ширина и высота) называемые полями. Неименованные поля называются анонимными полями.

Объекты создаются путем предоставления значений именованных и анонимных полей в соответствии со следующими примерами.

let rect = Rectangle(length = 1.3, width = 10.0)
let circ = Circle (1.0)
let prism = Prism(5., 2.0, height = 3.0)

В этом коде показано, что можно использовать именованные поля в инициализации или использовать порядок полей в объявлении и просто указать значения для каждого поля в свою очередь. Вызов конструктора для rect в предыдущем коде использует именованные поля, но вызов конструктора для circ использует упорядочение. Вы можете смешивать упорядоченные поля и именованные поля, как в построении prism.

Тип option — это простой различаемый союз в основной библиотеке F#. Тип option объявлен следующим образом.

// The option type is a discriminated union.
type Option<'a> =
    | Some of 'a
    | None

Предыдущий код указывает, что тип Option является дискриминированным объединением, которое имеет два случая, Some и None. В данном случае Some имеется связанное значение, которое состоит из одного анонимного поля, тип которого обозначается параметром типа 'a. В случае None нет связанного значения. Таким образом, тип option указывает универсальный тип, который может содержать значение какого-либо типа или не имеет значения. Тип Option также имеет псевдоним с маленькой буквы, option, который используется чаще.

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

let myOption1 = Some(10.0)
let myOption2 = Some("string")
let myOption3 = None

Идентификаторы случаев также используются в выражениях сопоставления шаблонов. В выражении сопоставления шаблонов идентификаторы предоставляются для значений, связанных с отдельными случаями. Например, в следующем коде x является идентификатором, которому присвоено значение, связанное с вариантом Some типа option.

let printValue opt =
    match opt with
    | Some x -> printfn "%A" x
    | None -> printfn "No value."

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

let getShapeWidth shape =
    match shape with
    | Rectangle(width = w) -> w
    | Circle(radius = r) -> 2. * r
    | Prism(width = w) -> w

Как правило, идентификаторы дела можно использовать без указания имени объединения. Если вы хотите, чтобы имя всегда идентифицировалось с именем объединения, можно применить атрибут RequireQualifiedAccess к определению типа объединения.

Отмена дискриминированных профсоюзов

В различаемых профсоюзах F# часто используются в моделировании домена для упаковки одного типа. Можно легко извлечь базовое значение с помощью сопоставления шаблонов. Для одного случая не нужно использовать выражение сопоставления:

let ([UnionCaseIdentifier] [values]) = [UnionValue]

В следующем примере показано следующее:

type ShaderProgram = | ShaderProgram of id:int

let someFunctionUsingShaderProgram shaderProgram =
    let (ShaderProgram id) = shaderProgram
    // Use the unwrapped value
    ...

Сопоставление шаблонов также допускается непосредственно в параметрах функции, поэтому можно обрабатывать отдельный случай прямо там.

let someFunctionUsingShaderProgram (ShaderProgram id) =
    // Use the unwrapped value
    ...

Структура дискриминированных объединений

Вы также можете представлять дискриминируемые объединения в качестве структур. Это делается с помощью атрибута [<Struct>].

[<Struct>]
type SingleCase = Case of string

[<Struct>]
type Multicase =
    | Case1 of string
    | Case2 of int
    | Case3 of double

Поскольку это типы значений, а не ссылочные типы, существуют дополнительные рекомендации по сравнению с ссылочными различаемых профсоюзов:

  1. Они копируются как типы значений и имеют семантику типа значений.
  2. Вы не можете использовать рекурсивное определение типа с многоальтернативным дискриминированным объединением.

До F# 9 существовало требование для каждого случая указать уникальное имя варианта (в объединении). Начиная с F# 9, ограничение будет отменено.

Использование дискриминированных профсоюзов вместо иерархий объектов

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

type Shape =
    // The value here is the radius.
    | Circle of float
    // The value here is the side length.
    | EquilateralTriangle of double
    // The value here is the side length.
    | Square of double
    // The values here are the height and width.
    | Rectangle of double * double

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

let pi = 3.141592654

let area myShape =
    match myShape with
    | Circle radius -> pi * radius * radius
    | EquilateralTriangle s -> (sqrt 3.0) / 4.0 * s * s
    | Square s -> s * s
    | Rectangle(h, w) -> h * w

let radius = 15.0
let myCircle = Circle(radius)
printfn "Area of circle that has radius %f: %f" radius (area myCircle)

let squareSide = 10.0
let mySquare = Square(squareSide)
printfn "Area of square that has side %f: %f" squareSide (area mySquare)

let height, width = 5.0, 10.0
let myRectangle = Rectangle(height, width)
printfn "Area of rectangle that has height %f and width %f is %f" height width (area myRectangle)

Выходные данные приведены следующим образом:

Area of circle that has radius 15.000000: 706.858347
Area of square that has side 10.000000: 100.000000
Area of rectangle that has height 5.000000 and width 10.000000 is 50.000000

Использование дискриминированных объединений в структурах данных дерева

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

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

В предыдущем коде resultSumTree имеет значение 10. На следующем рисунке показана структура дерева для myTree.

схема, показывающая структуру дерева для myTree.

Дискриминированные профсоюзы работают хорошо, если узлы в дереве разнородны. В следующем коде тип Expression представляет абстрактное дерево синтаксиса выражения на простом языке программирования, который поддерживает добавление и умножение чисел и переменных. Некоторые из вариантов объединения не рекурсивны и представляют числа (Number) или переменные (Variable). Другие случаи рекурсивны и представляют операции (Add и Multiply), где операнды также являются выражениями. Функция Evaluate использует выражение сопоставления для рекурсивной обработки дерева синтаксиса.

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 [ "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

При выполнении этого кода значение result равно 5.

Члены

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

open System

type IPrintable =
    abstract Print: unit -> unit

type Shape =
    | Circle of float
    | EquilateralTriangle of float
    | Square of float
    | Rectangle of float * float

    member this.Area =
        match this with
        | Circle r -> Math.PI * (r ** 2.0)
        | EquilateralTriangle s -> s * s * sqrt 3.0 / 4.0
        | Square s -> s * s
        | Rectangle(l, w) -> l * w

    interface IPrintable with
        member this.Print () =
            match this with
            | Circle r -> printfn $"Circle with radius %f{r}"
            | EquilateralTriangle s -> printfn $"Equilateral Triangle of side %f{s}"
            | Square s -> printfn $"Square with side %f{s}"
            | Rectangle(l, w) -> printfn $"Rectangle with length %f{l} and width %f{w}"

Свойства .Is* в случаях применения

Начиная с F# 9, дискриминированные объединения предоставляют автоматически созданные свойства .Is* для каждого случая, что позволяет проверить, является ли значение определённым случаем.

Вот как его можно использовать:

type Contact =
    | Email of address: string
    | Phone of countryCode: int * number: string

type Person = { name: string; contact: Contact }

let canSendEmailTo person =
    person.contact.IsEmail      // .IsEmail is auto-generated

Общие атрибуты

Следующие атрибуты обычно рассматриваются в различаемых профсоюзах:

  • [<RequireQualifiedAccess>]
  • [<NoEquality>]
  • [<NoComparison>]
  • [<Struct>]

См. также