Unioni discriminate

Le unioni discriminate forniscono supporto per i valori che possono essere uno dei diversi case denominati, possibilmente ognuno con valori e tipi diversi. Le unioni discriminate sono utili per dati eterogenei; dati che possono avere casi speciali, inclusi casi validi e di errore; dati che variano in base al tipo da un'istanza a un'altra; e come alternativa alle gerarchie di oggetti di piccole dimensioni. Inoltre, le unioni discriminate ricorsive vengono usate per rappresentare strutture di dati ad albero.

Sintassi

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

    [ member-list ]

Osservazioni:

Le unioni discriminate sono simili ai tipi di unione in altre lingue, ma esistono differenze. Come per un tipo di unione in C++ o un tipo variant in Visual Basic, i dati archiviati nel valore non sono fissi; può essere una delle diverse opzioni distinte. A differenza delle unioni in queste altre lingue, tuttavia, a ognuna delle possibili opzioni viene assegnato un identificatore di maiuscole e minuscole. Gli identificatori di maiuscole e minuscole sono nomi per i vari tipi possibili di valori che gli oggetti di questo tipo potrebbero essere; i valori sono facoltativi. Se i valori non sono presenti, la distinzione tra maiuscole e minuscole equivale a un caso di enumerazione. Se sono presenti valori, ogni valore può essere un singolo valore di un tipo specificato o una tupla che aggrega più campi dello stesso tipo o di tipi diversi. È possibile assegnare un nome a un singolo campo, ma il nome è facoltativo, anche se vengono denominati altri campi nello stesso caso.

Per impostazione predefinita, l'accessibilità per le unioni discriminate è public.

Si consideri, ad esempio, la dichiarazione seguente di un tipo Shape.

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

Il codice precedente dichiara una forma di unione discriminata, che può avere valori di uno qualsiasi dei tre casi: Rectangle, Circle e Prism. Ogni caso ha un set di campi diverso. Il case Rectangle ha due campi denominati, entrambi di tipo float, che hanno la larghezza e la lunghezza dei nomi. Il case Circle ha un solo campo denominato, raggio. Il caso Prism include tre campi, due dei quali (larghezza e altezza) sono campi denominati. I campi senza nome vengono definiti campi anonimi.

Per creare oggetti, specificare i valori per i campi denominati e anonimi in base agli esempi seguenti.

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

Questo codice mostra che è possibile usare i campi denominati nell'inizializzazione oppure fare affidamento sull'ordinamento dei campi nella dichiarazione e specificare solo i valori per ogni campo a sua volta. La chiamata del costruttore per rect nel codice precedente usa i campi denominati, ma la chiamata del costruttore per circ usa l'ordinamento. È possibile combinare i campi ordinati e i campi denominati, come nella costruzione di prism.

Il option tipo è un'unione discriminata semplice nella libreria principale F#. Il option tipo viene dichiarato come segue.

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

Il codice precedente specifica che il tipo Option è un'unione discriminata con due case e SomeNone. Il Some case ha un valore associato costituito da un campo anonimo il cui tipo è rappresentato dal parametro 'adi tipo . Il None case non ha alcun valore associato. Di conseguenza, il option tipo specifica un tipo generico che ha un valore di un tipo o nessun valore. Il tipo Option ha anche un alias di tipo minuscolo, option, più comunemente usato.

Gli identificatori di maiuscole e minuscole possono essere usati come costruttori per il tipo di unione discriminante. Ad esempio, il codice seguente viene usato per creare valori del option tipo.

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

Gli identificatori di maiuscole e minuscole vengono usati anche nelle espressioni di ricerca di criteri. In un'espressione di ricerca di criteri vengono forniti identificatori per i valori associati ai singoli case. Nel codice seguente, ad esempio, x è l'identificatore dato il valore associato al Some caso del option tipo.

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

Nelle espressioni di ricerca dei criteri di ricerca è possibile usare campi denominati per specificare corrispondenze di unione discriminate. Per il tipo shape dichiarato in precedenza, è possibile utilizzare i campi denominati come illustrato nel codice seguente per estrarre i valori dei campi.

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

In genere, gli identificatori di maiuscole e minuscole possono essere usati senza qualificarli con il nome dell'unione. Se si desidera che il nome sia sempre qualificato con il nome dell'unione, è possibile applicare l'attributo RequireQualifiedAccess alla definizione del tipo di unione.

Annullamento del wrapping di unioni discriminate

In Unioni discriminate F# vengono spesso usate nella modellazione del dominio per eseguire il wrapping di un singolo tipo. È facile estrarre il valore sottostante anche tramite criteri di ricerca. Non è necessario usare un'espressione di corrispondenza per un singolo caso:

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

Questo concetto è illustrato nell'esempio seguente:

type ShaderProgram = | ShaderProgram of id:int

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

I criteri di ricerca sono consentiti anche direttamente nei parametri della funzione, quindi è possibile annullare il wrapping di un singolo caso:

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

Struct Discriminated Unions

È anche possibile rappresentare unioni discriminate come struct. Questa operazione viene eseguita con l'attributo [<Struct>] .

[<Struct>]
type SingleCase = Case of string

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

Poiché si tratta di tipi valore e non di tipi riferimento, esistono considerazioni aggiuntive rispetto alle unioni discriminate di riferimento:

  1. Vengono copiati come tipi valore e hanno semantica del tipo valore.
  2. Non è possibile usare una definizione di tipo ricorsivo con un'unione discriminante tra struct multicase.
  3. È necessario specificare nomi di maiuscole e minuscole univoci per un'unione discriminante tra struct multicase.

Uso di unioni discriminate anziché gerarchie di oggetti

È spesso possibile usare un'unione discriminata come alternativa più semplice a una piccola gerarchia di oggetti. Ad esempio, l'unione discriminata seguente può essere usata invece di una Shape classe base con tipi derivati per cerchio, quadrato e così via.

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

Anziché un metodo virtuale per calcolare un'area o un perimetro, come si farebbe in un'implementazione orientata agli oggetti, è possibile usare criteri di ricerca per diramare le formule appropriate per calcolare queste quantità. Nell'esempio seguente vengono usate formule diverse per calcolare l'area, a seconda della forma.

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)

L'output è il seguente:

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

Uso di unioni discriminate per strutture di dati ad albero

Le unioni discriminate possono essere ricorsive, vale a dire che l'unione stessa può essere inclusa nel tipo di uno o più casi. Le unioni discriminate ricorsive possono essere usate per creare strutture ad albero usate per modellare le espressioni nei linguaggi di programmazione. Nel codice seguente viene usata un'unione discriminante ricorsiva per creare una struttura di dati di albero binario. L'unione è costituita da due case, Node, ovvero un nodo con un valore intero e sottoalberi sinistro e destro e Tip, che termina l'albero.

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

Nel codice precedente ha resultSumTree il valore 10. Nella figura seguente viene illustrata la struttura ad albero per myTree.

Diagram that shows the tree structure for myTree.

Le unioni discriminate funzionano bene se i nodi nell'albero sono eterogenei. Nel codice seguente il tipo Expression rappresenta l'albero della sintassi astratta di un'espressione in un linguaggio di programmazione semplice che supporta l'aggiunta e la moltiplicazione di numeri e variabili. Alcuni casi di unione non sono ricorsivi e rappresentano numeri (Number) o variabili (Variable). Altri casi sono ricorsivi e rappresentano le operazioni (Add e Multiply), dove gli operandi sono anche espressioni. La Evaluate funzione usa un'espressione di corrispondenza per elaborare in modo ricorsivo l'albero della sintassi.

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

Quando questo codice viene eseguito, il valore di result è 5.

Membri

È possibile definire i membri delle unioni discriminate. L'esempio seguente illustra come definire una proprietà e implementare un'interfaccia:

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

Attributi comuni

Gli attributi seguenti sono comunemente visualizzati nelle unioni discriminate:

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

Vedi anche