Unioni discriminate (F#)

Le unioni discriminate offrono supporto per valori che possono essere uno tra diversi casi denominati, ognuno con valori e tipi potenzialmente diversi. Le unioni discriminate risultano particolarmente utili per i dati eterogenei, i dati con casi speciali, tra cui casi di errore e casi validi, i dati che variano nel tipo da un'istanza a un'altra e come alternativa alle gerarchie di oggetti di piccole dimensioni. Le unioni discriminate ricorsive vengono inoltre utilizzate per rappresentare strutture di dati ad albero.

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

Note

Le unioni discriminate sono analoghe ai tipi di unione in altri linguaggi, ma sussistono alcune differenze. Come nel caso di un tipo di unione in C++ o un tipo variant in Visual Basic, i dati archiviati nel valore non sono fissi, ma possono corrispondere a una di molte opzioni distinte. A differenza delle unioni in questi altri linguaggi, tuttavia, per ognuna delle possibili opzioni viene fornito un identificatore di case. Gli identificatori di case sono nomi per i vari tipi di valori possibili per gli oggetti di questo tipo. I valori sono facoltativi. Se non sono presenti valori, il case è equivalente a un case di enumerazione. Se sono presenti, ogni valore può essere un singolo valore di un tipo specificato o una tupla che aggrega più campi degli stessi tipi o di tipi diversi. A partire da F# 3.1, è possibile fornire un nome a un singolo campo, ma il nome è facoltativo, anche se gli altri campi nello stesso case vengono denominati.

Si consideri ad esempio la seguente dichiarazione di tipo Shape.

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

Nel codice precedente viene dichiarata una Forma di unione discriminata, che può accettare valori di uno qualsiasi dei tre casi: Rettangolo, Cerchio e Prisma. Ogni case dispone di un set di campi diverso. Il caso Rettangolo dispone di due campi denominati, entrambi tipo float, che hanno i nomi larghezza e lunghezza. Il caso Cerchio dispone di un unico campo denominato, il raggio. Il caso Prisma dispone di tre campi, due dei quali sono denominati. I campi senza nome sono definiti campi anonimi.

Gli oggetti vengono costruiti fornendo valori per i campi denominati e anonimi, secondo quanto illustrato negli 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 utilizzare i campi denominati nell'inizializzazione oppure affidarsi all'ordinamento dei campi nella dichiarazione e fornire semplicemente i valori per ogni campo a turno. La chiamata del costruttore per rect nel codice precedente utilizza campi denominati, ma la chiamata del costruttore per circ utilizza l'ordinamento. È possibile combinare i campi ordinati e i campi denominati, come nella costruzione di prism.

Il tipo option è un'unione discriminata semplice nella libreria di base di F#. Il tipo option viene dichiarato nel modo seguente:

// 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, Some e None. Il caso Some presenta un valore associato che consiste in un campo anonimo il cui tipo è rappresentato dal parametro di tipo 'a. Il case None non presenta valori associati. Il tipo option specifica pertanto un tipo generico che presenta un valore di un certo tipo o non presenta alcun valore. Il tipo Option presenta inoltre un alias di tipo minuscolo, option, di uso più comune.

Gli identificatori di case possono essere utilizzati come costruttori per il tipo di unione discriminata. Nell'esempio di codice riportato di seguito viene illustrato come creare valori del tipo option.

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

Gli identificatori di case vengono inoltre utilizzati nelle espressioni dei criteri di ricerca. In un'espressione di criterio di ricerca vengono forniti identificatori per i valori associati ai singoli case. Nell'esempio di codice seguente, x è l'identificatore con il valore associato al case Some del tipo option.

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

Nelle espressioni con criteri di ricerca è possibile utilizzare 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 getShapeHeight shape =
    match shape with
    | Rectangle(height = h) -> h
    | Circle(radius = r) -> 2. * r
    | Prism(height = h) -> h

In genere, gli identificatori di case possono essere utilizzati senza qualifica del nome dell'unione. Se si desidera che il nome venga sempre qualificato con il nome dell'unione, è possibile applicare l'attributo RequireQualifiedAccess alla definizione del tipo dell'unione.

Utilizzo di unioni discriminate al posto di gerarchie di oggetti

È spesso possibile utilizzare un'unione discriminata come alternativa più semplice a una gerarchia di oggetti di piccole dimensioni. È ad esempio possibile utilizzare l'unione discriminata seguente al posto di una classe di base Shape con tipi derivati per cerchi, quadrati 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é utilizzare un metodo virtuale per calcolare un'area o un perimetro, come nel caso di un'implementazione orientata agli oggetti, è possibile utilizzare criteri di ricerca per creare rami per formule appropriate al fine di calcolare queste quantità. Nell'esempio seguente vengono utilizzate formule diverse per calcolare l'area, in base alla 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 è indicato di seguito:

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

Utilizzo di unioni discriminate per strutture di dati ad albero

Le unioni discriminate possono essere ricorsive, pertanto l'unione stessa può essere inclusa nel tipo di uno o più case. Le unioni discriminate ricorsive possono essere utilizzate per creare strutture ad albero, che consentono di modellare espressioni nei linguaggi di programmazione. Nel codice seguente viene utilizzata un'unione discriminata ricorsiva per creare una struttura di dati di una struttura ad albero binaria. L'unione è costituita da due case, Node, un nodo con un Integer e sottoalberi destro e sinistro, e Tip, che termina la struttura ad 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, il valore di resultSumTree è 10. Nella figura seguente viene indicata la struttura ad albero di myTree.

Struttura ad albero di myTree

Diagramma della struttura ad albero per le unioni discriminate

Le unioni discriminate sono funzionali se i nodi nella struttura ad albero sono eterogenei. Nel codice seguente, il tipo Expression rappresenta la struttura ad albero sintattica astratta di un'espressione in un linguaggio di programmazione semplice che supporta l'addizione e la moltiplicazione di numeri e variabili. Alcuni case di unione non sono ricorsivi e rappresentano numeri (Number) o variabili (Variable). Altri case sono ricorsivi e rappresentano operazioni (Add e Multiply) in cui anche gli operandi sono espressioni. La funzione Evaluate utilizza un'espressione di corrispondenza per elaborare in modo ricorsivo la struttura ad albero sintattica.

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.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

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

Vedere anche

Altre risorse

Riferimenti per il linguaggio F#