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 type1 [ * type2 ...]
| case-identifier2 [of type3 [ * 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ù valori degli stessi tipi o di tipi diversi.
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 case Some presenta un valore associato il cui tipo viene 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."
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
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.