1 poin oleh GN⁺ 2024-08-09 | 1 komentar | Bagikan ke WhatsApp
  • Proposal untuk memperkenalkan type unions (discriminated unions) ke C#, yang mengekspresikan bahwa satu variabel atau parameter hanya dapat memuat salah satu dari beberapa tipe terbatas; saat ini berada pada tahap Proposed, sementara prototype, implementation, dan specification berstatus Not Started
  • Implementasi berbasis hierarki pewarisan yang ada atau object sulit memenuhi sekaligus kebutuhan himpunan tipe tertutup, kombinasi tipe yang tidak saling terkait, penyimpanan nilai tanpa wrapper, dan penghindaran alokasi, sehingga dibagi menjadi empat kategori union
  • Proposal ini membedakan union class, union struct, ad hoc union, dan custom union; tiap pendekatan memiliki batasan berbeda dalam cara deklarasi, ada tidaknya alokasi, dan kemungkinan menggunakan ulang tipe yang sudah ada
  • Dalam switch dan pattern matching, jika semua tipe anggota ditangani, tersedia exhaustiveness tanpa perlu default, tetapi pada union struct, default bisa tidak berkaitan dengan anggota yang dideklarasikan sehingga diperlukan peringatan dan penetapan anggota default secara eksplisit
  • Ad hoc union menggunakan sintaks (A or B or C) untuk menggabungkan tipe yang sudah ada dan diimplementasikan melalui erasure serta pemeriksaan runtime, dengan batasan seperti boxing untuk value type, tidak bisa ref type, dan tidak mendukung true runtime overloading

Tujuan dan motivasi proposal

  • Type unions for C# adalah proposal untuk memperkenalkan type unions, yaitu discriminated unions, ke C#
    • Status dokumen adalah Proposed
    • Prototype, Implementation, dan Specification semuanya ditandai Not Started
  • Dalam pengembangan perangkat lunak, ada situasi ketika satu variabel tidak selalu berisi jenis yang sama, melainkan harus memuat salah satu dari tipe terkait yang terbatas
    • Contohnya adalah kasus seperti Customer dan Supplier, yang hanya berbagi sebagian properti tetapi perlu menjalankan pekerjaan serupa bergantung pada perbedaannya
  • Cara menyebarkan implementasi ke tiap tipe melalui metode abstrak bersama atau interface cocok ketika tipe tersebut memang ada untuk pekerjaan itu, atau pekerjaan tersebut merupakan bagian esensial dari tipe
    • Jika tipe memiliki tujuan yang lebih luas, menambahkan metode semacam itu bisa jadi tidak diinginkan
  • Dengan pewarisan, bisa juga dibuat tipe dasar bersama seperti Contact, tetapi dalam kasus berikut hal itu sulit atau tidak tepat
    • Ketika Anda tidak memiliki definisi tipenya
    • Ketika ada banyak situasi serupa sehingga pewarisan hanya bisa menyelesaikan salah satunya
    • Ketika Anda tidak ingin membocorkan kebutuhan pekerjaan tertentu ke dalam definisi data
  • Menggunakan object bisa berjalan, tetapi jaminan bahwa hanya nilai yang benar yang masuk harus dikelola melalui dokumentasi dan komentar
    • Bisa juga dilindungi dengan hierarki wrapper atau tipe agregat kustom, tetapi jika ada banyak himpunan tipe per situasi, ini memakan waktu dan merepotkan
  • Tujuannya adalah agar C# dapat mendeklarasikan penyimpanan salah satu dari beberapa tipe terbatas di lokasi yang sama, dan bahasa mengambil alih perlindungan variabel tersebut

Empat kategori union

  • Karena sulit memenuhi semua use case dengan satu implementasi, proposal ini dibagi menjadi empat kategori
  • Standard - union classes

    • Digunakan ketika union dan anggotanya dapat didefinisikan bersama, dan anggotanya dimaksudkan untuk digunakan seperti class independen
    • Ditujukan untuk kasus ketika alokasi class bukan masalah
    • Contoh:
      • Protokol, serialisasi, tipe transfer data
      • Model data UI (XAML)
      • Syntax tree
      • State pada state machine yang tidak sering berubah
      • Model data polimorfik lainnya
      • Nilai yang dipertahankan lama dalam bentuk union, seperti field atau property
  • Specialized - union structs

    • Digunakan ketika alokasi harus dihindari atau perlu menggunakan tipe khusus, dan beberapa batasan dapat diterima untuk itu
    • Contoh:
      • Nilai yang dialokasikan dalam array berurutan
      • Nilai yang dipetakan di atas blok memori (interop)
      • State pada state machine yang sering berubah
      • Nilai yang dipertahankan singkat dalam bentuk union, seperti argumen atau return value
      • Tipe library yang berpotensi digunakan secara khusus
  • Ad Hoc - ad hoc unions

    • Digunakan ketika union harus disusun dari tipe yang sudah ada dan mungkin tidak saling terkait
    • Union yang dideklarasikan dengan tipe anggota yang sama harus dapat saling dipertukarkan
  • Custom unions

    • Pendekatan untuk kasus yang tidak cocok dengan kategori lain
    • Contoh:
      • Tipe dan hierarki yang sudah ada dan tidak mudah didefinisikan ulang
      • Layout penyimpanan kustom
      • Bentuk dan perilaku API kustom

Standard - union classes

  • union class adalah named type union yang menempatkan semua tipe anggota dalam satu deklarasi self-contained
  • Deklarasinya mirip enum, tetapi berbeda karena tiap anggota adalah tipe yang dapat memiliki state melalui satu atau lebih variabel state
union U
{
    A(int x, string y);
    B(int z);
    C;
}
  • Tiap anggota hanya dapat diberi nama dan daftar variabel state
  • Pembuatan dilakukan dengan mengalokasikan tipe anggota
U u = new A(10, "ten");
  • Tipe anggota yang dibuat adalah A, dan ketika ditetapkan ke variabel u, ia dikonversi menjadi U
  • Pembongkaran dilakukan dengan type test dan pattern matching
if (u is A a) { ... }

if (u is A(var x, var y)) { ... }

if (u is A { y: var y }) { ... }
  • union class dianggap exhaustive
    • Jika semua tipe anggota ditangani dalam switch expression atau statement, default case tidak diperlukan
var x = u switch {
    A a => a.x,
    B b => b.z,
    C c => 0
    };
  • null dapat disertakan dengan notasi nullable standar
U? u = null;
  • Implementasinya direpresentasikan sebagai abstract record class dan nested derived record class
[Closed]
abstract record U
{
    public record A(int x, string y) : U;
    public record B(int z) : U;
    public record C : U { public static C Singleton = new C(); };
}
  • Atribut Closed membuat bahasa memahami bahwa tipe dasar tersebut merupakan hierarki tertutup yang tidak memiliki subtype yang dideklarasikan di luar modul tipe dasar

Specialized - union structs

  • union struct juga merupakan named type union yang menempatkan semua tipe anggota dalam satu deklarasi self-contained
    • Union dan tipe anggotanya semuanya struct, dan dapat digunakan tanpa heap allocation
  • Deklarasinya mirip union class, tetapi menambahkan keyword struct
union struct U
{
    A(int x, string y);
    B(int z);
    C;
}
  • Pembuatan, pembongkaran, exhaustive switch, dan notasi nullable mirip dengan union class
U u = new A(10, "ten");

if (u is A a) { ... }

U? u = null;
  • union struct dapat menjadi undefined state ketika belum dialokasikan atau ketika default dialokasikan
    • State ini tidak berkaitan dengan tipe anggota mana pun yang dideklarasikan
    • Exception runtime dapat terjadi pada switch yang mengandalkan exhaustiveness
U u = default;

var x = u switch
{
    A a => a.x,
    B b => b.z,
    C c => 0
}
  • Compiler menghasilkan peringatan jika default ditetapkan ke struct union
// warning: default not a valid state
U u = default;
  • Untuk menghindari peringatan, default state dapat dideklarasikan pada union struct dan dihubungkan dengan tipe anggota tertentu
union struct U
{
    A(int x, string y);
    B(int z);
    C = default;
}
  • Implementasi direpresentasikan sebagai struct yang memiliki tipe anggota berupa nested record struct, serta API untuk mengonversi antara tipe anggota dan aggregate union struct
    • Tata letak internal dipilih oleh compiler agar dapat menyimpan data dari tipe anggota yang mungkin secara efisien
    • Tradeoff antara kecepatan dan ukuran juga dipilih oleh compiler
[Union]
struct U
{
    public record struct A(int x, string y);
    public record struct B(int z);
    public record struct C { public static C Singleton = default; };

    public static implicit operator U(A value) {...};
    public static implicit operator U(B value) {...};
    public static implicit operator U(C value) {...};

    public static explicit operator A(U union) {...};
    public static explicit operator B(U union) {...};
    public static explicit operator C(U union) {...};

    public bool TryGetA(out A value) {...};
    public bool TryGetB(out B value) {...};
    public bool TryGetC(out C value) {...};

    public enum UnionKind { A = 1, B = 2, C = 3 };
    public UnionKind Kind => {...};
}
  • Attribute Union mengidentifikasi bahwa tipe tersebut adalah union struct
  • Union struct yang memiliki status default mendeklarasikan UnionKind yang sesuai sebagai 0
  • API pembuatan lengkap untuk union struct belum ditampilkan dalam dokumen

Pengujian tipe, boxing, dan reflection pada union struct

  • Saat melakukan pengujian tipe terhadap union struct yang diketahui, yang dipanggil adalah API union struct, bukan memeriksa tipe struct itu sendiri
u is A a
  • Ekspresi di atas ditransformasikan sebagai berikut
u.TryGetA(out var a)
  • Switch expression juga ditransformasikan ke bentuk yang menggunakan pemanggilan Kind dan TryGetX
u.Kind switch {
   U.UnionKind.A when u.TryGetA(out var a) => a.x,
   U.UnionKind.B when u.TryGetB(out var b) => b.z,
   U.UnionKind.C when u.TryGetC(out var c) => 0,
   _ => throw ...;
}
  • Boxed union struct bukanlah nilai tipe anggota yang di-boxing, melainkan union struct itu sendiri yang di-boxing
    • Kasus penggunaan utama union struct adalah menghindari boxing, tetapi kadang boxing bisa diperlukan
  • Karena tipe union struct dan anggotanya diketahui saling terkait, boxed union struct dapat diuji tipenya sebagai tipe anggota dan di-unbox
U u = ...;
object value = u;

if (value is A a) {...}
  • Kode di atas ditransformasikan seperti berikut
if (value is A a || (value is U u && u.TryGetA(out a))) {...}
  • Sebaliknya, boxed member type juga dapat diuji dan di-unbox sebagai union struct
A a = ...;
object value = a;

if (value is U u) {...}
  • Jika tidak dapat diketahui secara statis bahwa kedua sisi pengujian tipe terkait dengan union struct, pengujian tipe akan gagal
bool IsType(object value) => value is T;

U u = new A(...);

if (IsType(u)) {...}
  • Saat menggunakan reflection, boxed member type A mungkin perlu dikonversi menjadi boxed union struct U
  • Fitur struct union menyediakan metode utilitas untuk mengonversi antara boxed union struct dan boxed member type pada runtime
public static class TypeUnion
{
    public bool TryConvert(Type unionType, object value, out object? boxedUnion);
    public bool TryConvert(object value, out TUnion union);
    public object? GetValue(object? boxedUnion);
}
  • Union class dan ad hoc union sudah berada dalam bentuk yang benar untuk penggunaan reflection, sehingga tidak memerlukan konversi

Ref union structs

  • Union struct dengan modifier ref dapat memuat refs atau ref structs sebagai variabel status
ref union struct U
{
    A(ref int x);
    B(ReadOnlySpan y);
    C;
}
  • Dalam kasus ini, implementasi union dan tipe anggota yang memiliki nilai ref struct dikonversi menjadi ref struct
ref struct U
{
    public ref struct A { public ref int x; public A(ref int x) {...}; }
    public ref struct B { public ReadOnlySpan y; public B(ReadOnlySpan y) {...} }
    public record struct C { public static C Singleton = default; }
    ...
}
  • Jika ref record struct type ditambahkan ke C#, tipe anggota yang terdampak dapat tetap dipertahankan sebagai record struct

Ad Hoc - ad hoc unions

  • Ad hoc union adalah anonymous union yang dibuat dari tipe-tipe yang dideklarasikan di tempat lain
  • Sintaksnya menggunakan tanda kurung dan or pattern syntax
(A or B or C)
  • Untuk merujuknya dengan nama umum, gunakan file atau global using alias
global using U = (A or B or C);
  • Pembuatan dilakukan dengan menetapkan instance salah satu tipe anggota union ke variabel bertipe ad hoc union
record A(int x, string y);
record B(int z);
record C() { public static C Singleton = new C(); };

(A or B or C) u = new A(10, "ten");
  • Deconstruction dilakukan dengan pengujian tipe dan pattern matching
if (u is A a) {...}

if (u is A(var x, var y)) { ... }
  • Ad hoc union juga dianggap exhaustive, sehingga jika semua tipe anggota ditangani, default case tidak diperlukan
  • null dapat disertakan dengan notasi nullable
(A or B)? x = null;
  • Ad hoc union yang memiliki tipe anggota yang sama dipahami compiler sebagai tipe yang sama, terlepas dari urutannya
(A or B) x = new A(10, "ten");
(B or A) y = x;

Penugasan, kemampuan dipertukarkan, dan inferensi pada ad hoc union

  • Ad hoc union yang bertipe sama atau merupakan subset dapat ditugaskan ke ad hoc union superset tanpa pemeriksaan runtime
(A or B) x = new A(10, "ten");
(A or B or C) y = x;
  • Untuk menugaskan ad hoc union superset ke ad hoc union subset, diperlukan coercion eksplisit dan pemeriksaan runtime
(A or B or C) x = new A(10, "ten");
var y = (A or B)x;
  • Jika semua tipe anggota source union sama dengan atau merupakan subtype dari setidaknya satu anggota target union, implicit coercion dapat dilakukan tanpa pemeriksaan runtime
(Chihuahua or Siamese) pet = ...;
(Cat or Dog) animal = pet;
  • Meski tidak demikian, jika setidaknya salah satu tipe anggota source merupakan subtype dari salah satu tipe anggota target, coercion eksplisit dan pemeriksaan runtime dimungkinkan
(Cat or Chihuahua) mostlyCats = ...;
(Dog or Siamese) mostlyDogs = (Dog or Siamese)mostlyCats;
  • Tipe yang bukan ad hoc union juga dapat dipandang seperti ad hoc union bertipe tunggal dalam penentuan assignability
    • Aturan ini juga bekerja pada interface yang diimplementasikan
  • generalized coercions juga didefinisikan
    • Jika ada implicit coercion dari suatu tipe ke salah satu tipe anggota union, nilai tipe tersebut dapat di-implicit coercion ke tipe union
    • Jika semua tipe anggota union dapat di-implicit coercion ke suatu tipe, nilai union dapat di-implicit coercion ke tipe tersebut
    • Jika salah satu tipe anggota union dapat di-coercion ke suatu tipe, nilai union dapat di-explicit coercion ke tipe tersebut
    • Jika semua tipe anggota source union dapat di-implicit coercion ke salah satu anggota target union, implicit coercion antar-union dimungkinkan
    • Jika setidaknya salah satu anggota source union dapat di-explicit coercion ke salah satu anggota target union, explicit coercion antar-union dimungkinkan
  • Masih diperlukan aturan tentang coercion mana yang dipilih saat beberapa coercion dimungkinkan
  • Relasi assignability ini bukan relasi subtyping
    • Sebuah ad hoc union bukan subtype dari ad hoc union lain
  • Ad hoc union dengan tipe anggota yang sama dapat dipertukarkan melalui generics dan array elements
(T1 or T2)[] F(T1 v1, T2 v2) => new (T1 or T2)[] { v1, v2 };

(Dog or Cat)[] pets = F(rufus, petunia);
  • Ad hoc union yang digunakan sebagai generic type arguments dapat dipakai untuk covariance dan contravariance ketika semua tipe anggota dari dua union yang terkait memiliki relasi subtype dengan anggota pasangannya
    • Alih-alih aturan konkret, masih ada note “Have Mads write this part”
  • Ad hoc union berperilaku mirip dengan pattern or dalam pattern matching, dan dapat disertai deklarasi variabel
if (u is Dog or Cat) { ... }

if (u is (Dog or Cat)) { ... }

if (u is (Dog or Cat) pet) {...}
  • Saat nilai di-assign ke variabel ad hoc union, boxing untuk value type dapat terjadi
  • conditional expression dan switch expression dapat menginfer ad hoc union result type dari expression-expression penyusunnya
Dog rufus = ...;
Cat petunia = ...;
Bird polly = ...;

var u =
      x == 1 ? rufus
    : x == 2 ? petunia
    : polly;
  • Tipe return dari lambda expression juga dapat diinfer sebagai ad hoc union yang dibuat dari tipe-tipe return lambda body
    • Dalam kasus ini juga, boxing untuk value type dapat terjadi

Implementasi dan batasan ad hoc union

  • Ad hoc union diimplementasikan dengan erasure dan pemeriksaan runtime
(A or B) ab = new A(10, "ten");
  • Kode di atas ditransformasikan seperti berikut
object ab = new A(10, "ten");
  • Assignment yang tidak dapat diketahui benar secara statis memerlukan pemeriksaan runtime
    • Compiler membuat custom method untuk setiap ad hoc union unik yang digunakan dalam modul
object value = ...;
var ab = (A or B)value;
  • Contoh transformasinya adalah sebagai berikut
object value = ...;
object ab = (value);

object (object? value) =>
    value is A or B ? value : throw ...;
  • Pada method entry, parameters tidak diperiksa
  • Dalam metadata, tipe ad hoc union dienkode menggunakan custom attributes
void M((A or B) x);
  • Contoh transformasinya adalah sebagai berikut
void M([AdHocUnion([typeof(A), typeof(B)])] object x);
  • Detail attribute belum ditentukan
  • Karena semua ad hoc union di-erase menjadi tipe yang sama, true runtime overloading untuk method yang memiliki parameter ad hoc union tidak dimungkinkan
public void Wash((Cat or Dog) pet) { ... }
public void Wash((Compact or Sedan) car) { ... }
  • Overloading masih merupakan area yang terbuka untuk dibahas

Custom unions

  • Jika diperlukan perilaku yang tidak dapat ditentukan dengan sintaks union class atau union struct, Anda dapat mendeklarasikan custom class atau struct sendiri dan membuat C# mengenalinya sebagai custom union type
  • Jika union diimplementasikan dengan class hierarchy, pemberian attribute Closed memungkinkan perilaku exhaustiveness seperti union class
[Closed]
public class U { ... }
public class A(int x, string y) : U { ... }
public class B(int z) : U { ... }
  • Jika union diimplementasikan sebagai struct wrapper dengan specialized storage rules, menambahkan attribute Union dan menyediakan API yang mengikuti union pattern membuatnya secara fungsional sama dengan union struct
[Union]
public struct U
{
    public record struct A(int x, string y);
    public record struct B(int z);

    public bool TryGetA(out var A a) { ... }
    public bool TryGetB(out var B b) { ... }
}
  • Jika union tidak menyertakan member type atau menggunakan API pattern lain, API yang diharapkan compiler dapat disediakan lewat extensions
  • Seluruh union struct API pattern belum ditentukan
  • Ad hoc union tidak dapat mengustomisasi perilakunya selain dengan memodifikasi perilaku masing-masing member type

Common unions: Option dan Result

  • Option adalah struct union yang mirip dengan tipe bernama sama atau bertujuan sama di bahasa lain
    • Mengekspresikan bahwa sebuah nilai mungkin ada atau mungkin tidak ada
public union struct Option
{
    Some(TValue value);
    None = default;
}
  • Contoh penggunaannya adalah sebagai berikut
Option x = new Some("text");
Option y = None;

if (x is Some(var value)) {...}

var v = x is Some(var value) ? value : 0;
  • Tipe Option belum ditentukan sepenuhnya
  • Result juga merupakan struct union yang mirip dengan tipe bernama sama atau bertujuan sama di bahasa lain
    • Digunakan untuk mengembalikan hasil sukses atau error dari fungsi
public union struct Result
{
    Success(TValue value);
    Failure(TError error);
}
  • Contoh penggunaannya adalah sebagai berikut
Result x = Success("hurray!");
Result y = Failure("boo");

switch (x)
{
    case Success(var value): ...;
    case Failure(var error): ...;
}
  • Tipe Result juga belum sepenuhnya ditentukan

Proposal terkait

  • Proposal ini mencakup proposal yang diasumsikan ada atau fitur-fitur yang belum diajukan
  • Closed Hierarchies

    • Jika attribute Closed diterapkan pada abstract base type, semua subtype di dalam modul deklarasi dinyatakan sebagai kumpulan subtype tertutup
    • Jika subtype dideklarasikan di luar modul deklarasi, compiler error akan terjadi
    • closed hierarchy diperlakukan oleh compiler sebagai exhaustive, dan jika semua subtype ditangani dalam switch, default case tidak diperlukan
  • Singleton values

    • Tipe singleton yang memiliki property static Singleton dapat secara implisit mengakses property tersebut dalam non-type context dan digunakan seperti nilai
var x = U.C.Singleton;
  • Kode di atas dapat ditulis seperti berikut
var x = U.C;
  • Nested Member Shorthand

    • Nama yang tidak terikat dapat di-bind ke static member atau nested type dari target type
Color color = Color.Red;
  • Kode di atas dapat ditulis seperti berikut
Color color = Red;
U u = new U.A(10, "ten");
  • Kode di atas dapat ditulis seperti berikut
U u = new A(10, "ten");

Keputusan desain dan batasan yang dinyatakan dalam Q&A

  • union class tidak mutlak diperlukan jika hierarchy nested record dapat dideklarasikan langsung dengan mudah, tetapi kelebihannya adalah sintaks yang ringkas dan kemudahan beralih ke union struct hanya dengan satu modifier struct
  • union struct memiliki alokasi lebih sedikit dan dapat memakai lebih banyak jenis tipe, tetapi tidak selalu lebih cocok untuk semua kasus
    • Meski tidak memiliki allocation sendiri, belum tentu lebih cepat
    • stack footprint lebih besar dan biasanya disalin saat assignment, pass, dan return
    • Tidak mudah dipertukarkan dengan anonymous ad hoc union, sehingga kurang cocok
    • Ada masalah type tests, casts, dan pattern matching ketika dalam keadaan boxed atau direpresentasikan secara statis sebagai generic type parameter
  • union struct secara internal adalah tagged union sekaligus type union
    • Di internal, enum property yang berperan sebagai tag dapat diekspos untuk mempercepat kode yang dihasilkan compiler
    • Pada permukaan bahasa, ia terlihat sebagai type union agar dapat ditangani dengan cara yang familier seperti type tests, casts, dan pattern matching
  • Compiler dapat melakukan optimisasi yang melewati pembuatan member type ketika member type union struct langsung di-assign ke variabel union struct
  • Saat union didekonstruksi langsung ke variabel, optimisasi yang melewati penyalinan variabel status union struct ke member type juga diharapkan
  • union struct bukan base type aktual dari member type seperti union class
    • struct tidak mengizinkan inheritance aktual
    • Secara logis ia berperilaku seperti base type melalui konversi otomatis, tetapi relasi itu tidak meluas ke seluruh type system dan runtime
  • ad hoc union saat ini tidak dapat dideklarasikan langsung dengan nama
    • Jika ingin menghindari pengulangan union yang panjang atau memerlukan nama untuk penjelasan, harus menggunakan global using alias
  • ad hoc union melakukan boxing pada value type
    • Jika harus menghindari boxing, gunakan union struct
  • ad hoc union tidak dapat mencakup ref types
    • Jika memerlukan ref types, gunakan union struct
  • Alasan ad hoc union di-erase menjadi object adalah karena ia merupakan cara memperbaiki solusi berbasis object yang umum digunakan developer saat ini dengan compile-time type safety dan generated validation checks
  • Property atau method umum pada ad hoc union tidak dapat diakses langsung tanpa menangani tiap type case
    • Nilai hanya dapat diakses setelah berhasil dikonversi ke tipe individual
  • union types di F# sepadan dengan union class dan union struct dalam spesifikasi ini, tetapi proposal ini memperlakukan member sebagai tipe bahasa, bukan sebagai tag state dan associated state variables
    • ad hoc union mirip dengan type unions di Typescript
  • Option mungkin tidak diperlukan jika tujuan yang sama dapat dicapai dengan null dan nullable reference types
    • Sebagian developer lebih menyukai option type untuk pemaksaan yang lebih kuat dibanding nullable types di C#
  • C# saat ini tidak memasukkan monadic behaviors yang dimungkinkan di F# untuk Option dan Result ke dalam bahasa
  • Banyak kasus penggunaan Result dapat diselesaikan dengan exception handling di C#
    • Namun, ketika error diperkirakan dan sering terjadi saat runtime, mungkin ada keinginan untuk menghindari exception handling dan membuat caller menangani error secara eksplisit
  • Tipe yang mirip dengan Option dan Result sudah ada di library pihak ketiga, tetapi banyak developer meminta agar tipe standar disertakan di runtime demi interoperability antar-library

1 komentar

 
GN⁺ 2024-08-09
Pendapat Hacker News
  • Setelah memakai discriminated union di F# selama beberapa tahun, saya kira C# sekarang sudah pasti memilikinya
    Mungkin bukan fitur yang disukai semua orang, tetapi dalam bahasa bertipe, sangat sulit kembali ke bahasa yang tidak memiliki algebraic data type (ADT) dalam bentuk apa pun. Saat ini saya memakai Java dan secara umum baik-baik saja, tetapi cukup mengganggu ketika harus mengakali dengan kelas wrapper untuk sesuatu yang di F# cukup tiga baris

    • Kalau sudah terbiasa dengan F#, sangat sulit untuk kembali. Saya berharap Microsoft mendukung dan mendorongnya dengan lebih serius
      Pengembangan jadi nyaman karena ini adalah titik keseimbangan yang pas, mengambil banyak kelebihan fungsional tanpa terikat oleh batasan seperti fungsi murni. Di sisi lain, C# sedang diubah sangat pelan-pelan agar makin mirip F#, dan melihat itu juga terasa aneh
    • Kabar baiknya, belakangan ini di Java pun hal itu bisa dilakukan dengan boilerplate yang relatif sedikit memakai sealed interface dan record
      Tentu saja, itu menurut standar Java
    • Saat saya berhenti memakai F# dan .NET pada 2020, CLR hanya memiliki inheritance, dan enum di F# pun sebenarnya diimplementasikan dengan inheritance
      Misalnya Some dan None adalah kelas turunan dari kelas Option, dan itu bisa dilihat jika assembly F# dibuka dengan decompiler. Saya tidak tahu apakah sekarang masih sama, tetapi proposal C# ini, termasuk enum anonim dengan sintaks A or B, tampaknya sulit dibuat hanya sebagai gula sintaks di atas inheritance. Jadi agar ini bekerja, sepertinya CLR perlu mendukung enum sebagai warga kelas satu
    • Sebagai catatan, Java memiliki tagged union type sejak versi 16
      Pattern matching juga ada
    • Java sudah memiliki ADT sejak beberapa tahun lalu, jadi pernyataan “mengakali dengan kelas wrapper untuk sesuatu yang cukup tiga baris” terdengar agak aneh
      Records https://en.wikipedia.org/wiki/Java_version_history#Java_16
      Sealed classes https://en.wikipedia.org/wiki/Java_version_history#Java_17
      Dan pattern matching juga sudah ada sejak hampir 1 tahun lalu https://en.wikipedia.org/wiki/Java_version_history#Java_21
      Meski begitu, saya sangat setuju bahwa mengembangkan dengan F# itu menyenangkan
  • Saya benar-benar menantikan proposal ini. Ini adalah fitur besar yang paling terasa hilang yang selalu harus saya tambahkan dengan nada meminta maaf saat membicarakan kelebihan C#
    Selain ini, sulit memikirkan fitur bahasa besar lain yang kurang dari C#. Saya juga menantikan bahwa setelah ini masuk pun, di HN kita mungkin masih akan melihat C# diperlakukan seolah-olah masih bahasa yang sama seperti 10 tahun lalu

    • Akan bagus juga jika ada type alias yang benar, tetapi belakangan ini coding dengan C# memang sangat menyenangkan
    • Baik atau buruk, tujuan desain C# tampaknya adalah memasukkan semua fitur yang mungkin, bahkan dalam beberapa variasi
      Karena itu ia bisa menyesuaikan diri untuk siapa saja dan tidak menyingkirkan orang seperti bahasa yang sangat beropini kuat, tetapi bisa menjadi sedikit rumit untuk dipelajari pada awalnya
  • Bisa jelaskan mengapa ini disebut type unions? Saya baru pertama kali mendengar nama seperti ini
    Ini bukan union antar tipe seperti ALGOL68, melainkan tampak seperti tagged union pada bahasa keluarga ML. Saya jadi bertanya-tanya apakah ini contoh pengembang C# membuat istilah lain alih-alih memakai istilah yang sudah ada. Seperti SelectMany, IEnumerable, dan semacamnya

    • “tagged” terdengar seperti detail implementasi bahwa di dalamnya ada tag untuk membedakan tipe
      Mungkin mereka menambahkan “type” di depan “union” untuk memperjelas bahwa ini terkait tipe. Dalam dokumen, sintaksnya sendiri tampaknya hanya union. Di FAQ tertulis begini:
      Q: Why are there no tagged unions?
      A: Union structs are both tagged unions and type unions. Under the hood, a union struct is a tagged union, even exposing an enum property that is the tag to enable faster compiler generated code, but in the language it is presented as a type union to allow you to interact with it in familiar ways, like type tests, casts and pattern matching.
    • Karena proposal ini membahas beberapa jenis union
      Ini mencakup hierarki tertutup untuk tipe referensi yang diverifikasi compiler saat build, tagged union sebagai tipe nilai yang diproses compiler agar berperilaku seperti hierarki tertutup, tipe buatan pengguna yang dapat memiliki implementasi arbitrer namun tetap terhubung ke mekanisme compiler yang sama, serta ad-hoc union dari tipe-tipe yang sudah ada
  • Meski melewatkan semua metafora warna seperti red/blue/white/black pill, union dengan pattern matching yang menyeluruh adalah salah satu fitur bahasa yang, begitu kamu mengenalnya, paling sulit untuk hidup tanpanya
    Saya tidak pernah merasa benar-benar memahami implikasi dari expression problem, tetapi hipotesis saya saat ini seperti ini. Cara menyediakan titik ekstensi dengan polimorfisme tradisional cocok ketika klien di masa depan yang tidak saya kenal memperluas kode, sedangkan union dengan pattern matching yang menyeluruh lebih cocok untuk kode yang dimiliki saya atau tim saya. Biasanya bukan karena ingin kode itu diperluas dari luar, melainkan karena ingin memperbarui struktur data inti seiring berubahnya pemahaman domain bisnis, lalu sebisa mungkin membiarkan compiler menangkap lewat error titik-titik di mana kode imperatif tidak lagi sesuai dengan struktur domain

    • Saya menulis artikel ini untuk mencoba memahami perbedaannya
      https://deliberate-software.com/christmas-f-number-polymorph...
    • Menurut saya inti expression problem bisa dijelaskan sebagai kontras antara cara mendapatkan ekstensibilitas kode di object-oriented dan functional
      Pada pendekatan interface/inheritance di object-oriented, mudah menambahkan variasi tipe baru dari base class atau interface, tetapi sulit menambahkan fitur baru karena harus mengimplementasikannya di semua tipe yang ada. Pada pendekatan discriminated union di functional, mudah menambahkan fitur baru karena kita bisa membuat fungsi baru dan melakukan matching terhadap union, dengan compiler menjamin semua kasus ditangani. Namun menambahkan variasi tipe baru menjadi sulit karena harus memperbarui pattern matching yang menyeluruh di seluruh codebase. Kotlin mendukung kedua pendekatan ini dengan cukup baik, jadi cocok sebagai contoh
  • Bukankah istilahnya agak meleset? Setahu saya TypeScript punya union types
    Namun ini terlihat seperti discriminated union yang biasa saya lihat di F# atau Haskell. Menurut saya bedanya discriminated union memiliki case constructor yang diberi nama

    • TypeScript punya “union types”, dan proposal ini tampaknya menyebutnya ad hoc unions
      “type union” setidaknya bukan istilah resmi yang pernah saya dengar. Ini terlihat seperti ungkapan yang dibuat di ranah C# untuk menjelaskan sum type
    • Berpindah dari C# ke type union TypeScript selalu terasa aneh
      Mungkin ada alasan tersendiri mengapa ini ditunda sampai sekarang. Karena perlu tahu siapa pengguna sasarannya. Seiring waktu memang terbiasa, tetapi kalau terlalu sering melihat A|B|undefined, membacanya jadi melelahkan. Selain itu, bisa muncul kemalasan seperti menerima satu hal lalu mengembalikan kombinasi tiga hal atau lebih secara asal, dan semakin naik ke call stack, makin membingungkan
      Saya suka karena C# membuat ini ditangani dengan cara yang dibatasi. Namun kalau pihak yang menginginkan fitur ini punya argumen yang meyakinkan, saya ingin mendengarnya
  • Saya sudah lama memakai C#, tetapi merasa ada sesuatu yang saya lewatkan dari proposal ini. Use case-nya tidak terlihat terdefinisi dengan baik; bisakah memberi contoh yang realistis?
    Contoh dalam proposal sepertinya bisa diimplementasikan dengan mendeklarasikan interface kosong lalu membuat beberapa kelas record “mengimplementasikan” interface itu. Saya kurang paham apa yang hilang jika dilakukan begitu

    • Hierarki seperti itu adalah hierarki terbuka, dalam arti saat memprosesnya kita tidak bisa tahu apakah semua kasus sudah ditangani
      Siapa pun, bahkan di luar kode saya, bisa membuat implementasi baru dari interface tersebut. Selain itu, pilihan-pilihannya bisa saja tidak berbagi permukaan umum sama sekali, sehingga “interface” untuk semua tipe menjadi kosong, yang agak tidak alami dalam object-oriented. Secara internal pada akhirnya tetap berupa hierarki tipe, tetapi perbedaan utama dengan implementasi manual adalah ekstensinya tertutup, dan ketika kode memakai case lalu ada case yang terlewat, akan muncul error compile-time
    • Contoh paling realistis adalah AST atau protokol data
      Ambil JSON sebagai contoh: alih-alih menaruh data dalam satu kelas JsonValue, kita bisa membuatnya sebagai union type yang merupakan salah satu dari string, boolean, number, array dari union type, atau map yang memakai key string dan value union type seperti itu. Tipe hasil seperti Result di Rust juga bisa diimplementasikan, sehingga API dapat didefinisikan mengembalikan nilai atau error. Saya belum melihat apakah proposalnya membahas generic. Di dunia functional programming ini biasanya disebut algebraic data type, dan setelah terbiasa memodelkan tipe seperti ini, kita benar-benar merindukannya di bahasa yang tidak mendukungnya
      [1] https://en.m.wikipedia.org/wiki/Algebraic_data_type
    • Library ini mengimplementasikan discriminated union ala F#
      [0] https://github.com/mcintyre321/OneOf
      Saya menggunakannya ketika ingin membuat lapisan luar mengembalikan tipe Result yang membungkus DTO, error, dan sebagainya menjadi satu. Karena bisa melakukan pattern matching terhadap hasilnya, penanganan error menjadi lebih mudah, dan model dapat berevolusi seiring waktu tanpa mengubah function signature di boundary
    • Selama lebih dari 5 tahun terakhir saya meniru discriminated union F# dengan interface kosong, dan itu bekerja sangat baik
      Masalah “tidak menangani semua case di switch” sangat jarang terjadi, kira-kira sekali per 50 ribu baris kode, dan biasanya diperbaiki setelah smoke test pertama dijalankan. Jadi itu bukan masalah besar. Saya rasa alasan tim .NET belum menambahkan discriminated union juga karena ini bisa ditiru secara efektif dengan interface
    • Contoh sederhana adalah Result yang bisa menjadi salah satu dari Ok(T value) atau Error(string message)
      Untuk mendapatkan nilainya, kita harus melakukan switch pada kedua kasus, sehingga dipaksa menangani kasus error di tempat itu juga
  • Di bawah bagian covariance / contravariance tertulis “Note: Have Mads write this part.”, lucu juga :)

    • Mads di sini merujuk pada Mads Torgersen, lead designer C#
  • Ada bagian yang mengatakan bahwa “layout internal union struct dipilih oleh compiler sebagai kompromi antara kecepatan dan ukuran agar dapat menyimpan data dari tipe anggota yang memungkinkan secara efisien”
    Dari sudut pandang seseorang yang dulu pernah terlalu banyak mencoba ilmu hitam C# union dengan FieldOffset lalu kena batunya, di sini ada masalah yang disayangkan. Melakukan aliasing antara pointer/nilai ref dan value type adalah UB. Artinya, struct union antara u64 dan object memerlukan field terpisah untuk masing-masing, sehingga 8 byte terbuang. Begitulah selama ryujit/GC tidak diperbarui agar mengetahui hal ini

    • Bukan UB, melainkan ilegal. Menurut ECMA 335, II.10.7, field memang bisa dibuat saling tumpang tindih seperti ini, tetapi offset yang ditempati referensi objek tidak boleh bertumpang tindih dengan offset yang ditempati built-in value type atau bagian dari referensi objek lain
      Di .NET 8.0, kode yang menaruh UInt64 Foo dan Object Bar bersama-sama pada FieldOffset(0) memang bisa dikompilasi, tetapi saat dimuat akan melempar System.TypeLoadException. Pesannya menyatakan bahwa object field pada offset 0 tidak selaras dengan benar atau bertumpang tindih dengan non-object field. Menariknya, compiler AOT memperingatkan bahwa metode ini akan selalu throw, tetapi compiler C# tidak mengeluarkan peringatan apa pun
  • Sayang rasanya C# bukannya menjadi bahasa berorientasi objek yang lebih baik, tetapi malah terus berusaha menjadi F# yang lebih jelek
    Misalnya, mengapa sintaks multiple dispatch masih sekaku ini? Pseudo-OO sudah menguasai dunia, dan orang-orang pun bereaksi menentangnya, jadi saya paham bahwa menjadi “bahasa agak fungsional dengan kurung kurawal” lebih mudah untuk tetap relevan daripada benar-benar berusaha melakukan orientasi objek dengan baik

    • Ada keluhan yang sama dari sisi sebaliknya. Bahasa-bahasa arus utama dulu memiliki mutable state dan side effect yang luas, dan sekarang hanya ditambahi lambda
      Lalu apa yang kurang dari sisi berorientasi objek? Apa yang diperlukan agar C# atau bahasa lain bisa disebut “melakukan orientasi objek dengan baik”?
    • Apa yang dimaksud dengan “berorientasi objek yang lebih baik” di sini?
      Saya penasaran konsep atau fitur seperti apa yang ada dalam pikiran Anda
  • Saat ini saya menggunakan nested record dengan constructor private bersama paket NuGet https://github.com/shuebner/ClosedTypeHierarchyDiagnosticSup... untuk menjamin bahwa tipe switch tidak memerlukan case _
    Pada dasarnya ini versi desugaring dari “Union Classes” dalam proposal ini, dan sudah bekerja cukup baik. Namun tetap menyenangkan kalau tidak perlu lagi paket NuGet dan ada syntax sugar, jadi saya menyukai proposal ini

    • Perlu diperhatikan bahwa tipe record tidak tertutup meskipun memiliki constructor private
      Compiler membuat constructor protected untuk operasi penyalinan, dan constructor itu bisa diwarisi. Untuk mencegahnya, Anda harus mendefinisikan sendiri constructor protected dan membuatnya melempar exception runtime jika bukan case yang valid