Discriminated Union

Discriminated union memberikan dukungan untuk nilai yang dapat berupa salah satu dari sejumlah kasus bernama, yang mungkin masing-masing dengan nilai dan jenis yang berbeda. Discriminated union berguna untuk data yang heterogen; data yang dapat memiliki kasus khusus, termasuk kasus valid dan kesalahan; data yang jenisnya bervariasi dari satu instans ke instans lainnya; dan sebagai alternatif untuk hierarki objek kecil. Selain itu, discriminated union rekursif digunakan untuk mewakili struktur data pohon.

Sintaks

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

    [ member-list ]

Keterangan

Discriminated union mirip dengan jenis union dalam bahasa pemrograman lain, tetapi ada perbedaannya. Seperti pada jenis union di C++ atau jenis varian dalam Visual Basic, data yang disimpan di dalam nilai tidak tetap; data tersebut dapat berupa salah satu dari beberapa opsi yang berbeda. Namun, tidak seperti union dalam bahasa pemrograman lain ini, setiap opsi yang mungkin diberikan pengidentifikasi kasus. Pengidentifikasi kasus adalah nama untuk berbagai kemungkinan jenis nilai yang bisa saja adalah objek jenis ini; nilai bersifat opsional. Jika tidak ada nilai, kasusnya setara dengan kasus enumerasi. Jika ada nilai, setiap nilai dapat berupa nilai tunggal dari jenis tertentu atau tuple yang mengagregasi beberapa bidang dengan jenis yang sama atau berbeda. Anda dapat memberi nama bidang individu, tetapi nama bersifat opsional, bahkan jika bidang lain dalam kasus yang sama diberi nama.

Aksesibilitas untuk discriminated union ditetapkan secara default ke public.

Misalnya, perhatikan deklarasi jenis Bentuk berikut.

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

Kode sebelumnya mendeklarasikan discriminated union Bentuk, yang dapat memiliki nilai salah satu dari tiga kasus: Persegi Panjang, Lingkaran, dan Prisma. Setiap kasus memiliki serangkaian bidang yang berbeda. Kasus Persegi Panjang memiliki dua bidang bernama, keduanya berjenis float, yang memiliki nama lebar dan panjang. Kasus lingkaran hanya memiliki satu bidang bernama, yaitu radius. Kasus Prisma memiliki tiga bidang, dua di antaranya (lebar dan tinggi) adalah bidang bernama. Bidang tanpa nama disebut sebagai bidang anonim.

Anda membuat objek dengan menyediakan nilai untuk bidang bernama dan bidang anonim sesuai dengan contoh berikut.

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

Kode ini menunjukkan bahwa Anda dapat menggunakan bidang bernama dalam inisialisasi, atau Anda dapat mengandalkan urutan bidang dalam deklarasi dan hanya menyediakan nilai untuk setiap bidang secara bergantian. Panggilan konstruktor untuk rect dalam kode sebelumnya menggunakan bidang bernama, tetapi panggilan konstruktor untuk circ menggunakan pengurutan. Anda dapat mencampur bidang berurutan dan bidang bernama, seperti dalam konstruksi prism.

Jenis option adalah discriminated union sederhana di pustaka inti F#. Jenis option dideklarasikan sebagai berikut.

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

Kode sebelumnya menetapkan bahwa jenis Option adalah discriminated union yang memiliki dua kasus, yaitu Some dan None. Kasus Some memiliki nilai terkait yang terdiri dari satu bidang anonim yang jenisnya diwakili oleh parameter jenis 'a. Kasus None tidak memiliki nilai terkait. Oleh karena itu, jenis option menetapkan jenis generik yang memiliki nilai suatu jenis atau tidak memiliki nilai. Jenis Option juga memiliki alias jenis huruf kecil, yaitu option, yang lebih umum digunakan.

Pengidentifikasi kasus dapat digunakan sebagai konstruktor untuk jenis discriminated union. Misalnya, kode berikut digunakan untuk membuat nilai jenis option.

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

Pengidentifikasi kasus juga digunakan dalam ekspresi pencocokan pola. Dalam ekspresi pencocokan pola, pengidentifikasi disediakan untuk nilai yang terkait dengan kasus individu. Misalnya, dalam kode berikut, x adalah pengidentifikasi yang diberikan nilai yang terkait dengan kasus Some dari jenis option.

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

Dalam ekspresi pencocokan pola, Anda dapat menggunakan bidang bernama untuk menentukan kecocokan discriminated union. Untuk jenis Bentuk yang dideklarasikan sebelumnya, Anda dapat menggunakan bidang bernama seperti yang ditunjukkan kode berikut untuk mengekstrak nilai bidang.

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

Biasanya, pengidentifikasi kasus dapat digunakan tanpa membuat mereka memenuhi syarat dengan nama union. Jika Anda ingin nama tersebut selalu memenuhi syarat dengan nama union, Anda dapat menerapkan atribut RequireQualifiedAccess ke definisi jenis union.

Membongkar Discriminated Union

Dalam F#, Discriminated Union sering digunakan dalam pemodelan domain untuk membungkus jenis tunggal. Mengekstrak nilai yang mendasarinya melalui pencocokan pola juga sangat mudah. Anda tidak perlu menggunakan ekspresi pencocokan untuk kasus tunggal:

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

Contoh berikut menunjukkan hal berikut:

type ShaderProgram = | ShaderProgram of id:int

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

Pencocokan pola juga diizinkan secara langsung dalam parameter fungsi, sehingga Anda dapat membongkar kasus tunggal di sana:

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

Discriminated Union Struktur

Anda juga dapat merepresentasikan Discriminated Union sebagai struktur. Hal ini dilakukan dengan atribut [<Struct>].

[<Struct>]
type SingleCase = Case of string

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

Karena ini adalah jenis nilai dan bukan jenis referensi, ada pertimbangan tambahan dibandingkan dengan discriminated union referensi:

  1. Mereka disalin sebagai jenis nilai dan memiliki semantik jenis nilai.
  2. Anda tidak dapat menggunakan definisi jenis rekursif dengan Discriminated Union struktur multikasus.
  3. Anda harus memberikan nama kasus unik untuk Discriminated Union struktur multikasus.

Menggunakan Discriminated Union Alih-Alih Hierarki Objek

Sering kali Anda dapat menggunakan discriminated union sebagai alternatif yang lebih sederhana untuk hierarki objek kecil. Misalnya, discriminated union berikut dapat digunakan alih-alih kelas dasar Shape yang memiliki jenis turunan untuk lingkaran, persegi, dan sebagainya.

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

Alih-alih metode virtual untuk menghitung luas atau keliling, seperti yang akan Anda gunakan dalam implementasi berorientasi objek, Anda dapat menggunakan pencocokan pola untuk mencabangkan ke rumus yang sesuai untuk menghitung jumlah ini. Dalam contoh berikut, rumus yang berbeda digunakan untuk menghitung luas, tergantung bentuknya.

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)

Outputnya sebagai berikut:

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

Menggunakan Discriminated Union untuk Struktur Data Pohon

Discriminated union dapat bersifat rekursif, artinya, union itu sendiri dapat disertakan dalam jenis satu atau beberapa kasus. Discriminated union rekursif dapat digunakan untuk membuat struktur pohon, yang digunakan untuk ekspresi model dalam bahasa pemrograman. Dalam kode berikut, sebuah discriminated union rekursif digunakan untuk membuat struktur data pohon biner. Union tersebut terdiri dari dua kasus, Node, yang merupakan node dengan nilai bilangan bulat serta sub pohon kiri dan kanan, dan Tip, yang mengakhiri pohon.

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

Pada kode sebelumnya, resultSumTree memiliki nilai 10. Illustrasi berikut menunjukkan struktur pohon untuk myTree.

Diagram that shows the tree structure for myTree.

Discriminated union berfungsi dengan baik jika node di dalam pohon bersifat heterogen. Dalam kode berikut, Expression jenis mewakili pohon sintaks abstrak sebuah ekspresi dalam bahasa pemrograman sederhana yang mendukung penjumlahan dan perkalian angka dan variabel. Beberapa kasus union tidak bersifat rekursif dan mewakili angka (Number) atau variabel (Variable). Kasus lain bersifat rekursif dan mewakili operasi (Add dan Multiply), di mana operand-nya juga merupakan ekspresi. Fungsi Evaluate menggunakan ekspresi pencocokan untuk memproses pohon sintaks secara rekursif.

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

Ketika kode ini dijalankan, nilai dari result adalah 5.

Anggota

Anggota discriminated union dapat ditentukan. Contoh berikut menunjukkan cara menentukan properti dan mengimplementasikan antarmuka:

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

Atribut Umum

Atribut berikut biasanya terlihat dalam discriminated union:

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

Lihat juga