1 puan yazan GN⁺ 2024-08-09 | 1 yorum | WhatsApp'ta paylaş
  • C#'a type unions (discriminated unions) ekleyerek tek bir değişkenin ya da parametrenin, sınırlı sayıdaki birden fazla türden yalnızca birini taşımasını ifade etmeyi amaçlayan bir öneri; şu anda Proposed aşamasında ve prototype, implementation, specification durumları Not Started olarak görünüyor
  • Mevcut kalıtım hiyerarşileri ya da object tabanlı uygulamalar; kapalı tür kümesi, ilişkisiz türlerin birleşimi, sarmalayıcı olmadan değer saklama ve allocation'dan kaçınma gereksinimlerini aynı anda karşılamakta zorlandığı için yaklaşım dört union kategorisine ayrılıyor
  • Öneri; union class, union struct, ad hoc union ve custom union ayrımı yapıyor; her yaklaşım bildirim biçimi, allocation durumu ve mevcut türlerin yeniden kullanılabilirliği açısından farklı kısıtlar içeriyor
  • switch ve pattern matching içinde tüm üye türler ele alındığında default gerektirmeyen exhaustiveness sağlanıyor; ancak union struct için default, bildirilen üyelerden birine karşılık gelmeyebildiğinden uyarı ve açık bir default üye tanımı gerekiyor
  • ad hoc union, mevcut türleri (A or B or C) sözdizimiyle birleştiriyor ve erasure ile çalışma zamanı denetimleri üzerinden uygulanıyor; value type boxing, ref type desteğinin olmaması ve gerçek runtime overloading yapılamaması gibi kısıtları var

Önerinin amacı ve motivasyonu

  • Type unions for C#, C#'a type union'lar, yani discriminated union'lar eklenmesini öneriyor
    • Belge durumu Proposed
    • Prototype, Implementation ve Specification alanlarının tümü Not Started olarak işaretlenmiş
  • Yazılım geliştirmede bazen tek bir değişkende her zaman aynı tür yerine, birbiriyle ilişkili sınırlı sayıdaki türden birini tutmak gerekir
    • Buna Customer ve Supplier örnek veriliyor; bazı özellikleri ortak olsa da farklılıklarına göre benzer işlemler yapılması gerekebilir
  • Ortak abstract metotlar ya da interface'lerle uygulamayı türlerin içine dağıtmak, o türler bu işlem için var olduğunda veya işlem türün özsel bir parçası olduğunda uygundur
    • Türün daha geniş bir amacı varsa, böyle metotlar eklemek arzu edilmeyebilir
  • Contact gibi ortak bir taban tür kalıtımla oluşturulabilir, ancak şu durumlarda bu zor ya da uygun olmayabilir
    • Tür tanımlarının sahibi olmadığınızda
    • Benzer durumların çok olduğu ve kalıtımın yalnızca tek birini çözebildiği durumlarda
    • Belirli bir işlemin gereksinimlerini veri tanımına sızdırmak istemediğinizde
  • object kullanımı işe yarayabilir, ancak yalnızca doğru değerlerin geleceği garantisini belgeler ve yorumlarla korumak gerekir
    • Bunu wrapper katmanı ya da custom aggregate türüyle de güvence altına almak mümkündür; ancak duruma göre tür kümesi sayısı arttığında bu zaman alıcı ve zahmetli olur
  • Amaç, C#'ın sınırlı sayıdaki birden fazla türden birinin aynı yerde saklanacağını bildirebilmesi ve değişkenin güvenliğini dilin üstlenmesidir

Dört union kategorisi

  • Tüm kullanım senaryolarını tek bir uygulamayla karşılamak zor olduğundan yaklaşım dört kategoriye ayrılıyor
  • Standard - union classes

    • Union ile üyelerin birlikte tanımlanabildiği ve üyelerin bağımsız sınıflar olarak da kullanılmasının amaçlandığı durumlarda kullanılır
    • Sınıf allocation'ının sorun olmadığı senaryoları hedefler
    • Örnekler:
      • protokoller, serialization ve veri aktarım türleri
      • UI veri modelleri (XAML)
      • syntax tree
      • sık değişmeyen state machine durumları
      • diğer polimorfik veri modelleri
      • alan ya da özellik gibi, uzun süre union biçiminde tutulan değerler
  • Specialized - union structs

    • Allocation'dan kaçınmak gerektiğinde veya özel tür kullanımına ihtiyaç duyulduğunda ve bunun karşılığında bazı kısıtlar kabul edilebildiğinde kullanılır
    • Örnekler:
      • ardışık dizilere atanan değerler
      • bellek blokları üzerine eşlenen değerler (interop)
      • sık değişen state machine durumları
      • argüman ya da dönüş değeri gibi kısa süre union biçiminde kalan değerler
      • özel kullanım potansiyeli olan kütüphane türleri
  • Ad Hoc - ad hoc unions

    • Halihazırda var olan ve birbiriyle ilişkili olmak zorunda olmayan türlerle union oluşturulması gerektiğinde kullanılır
    • Aynı üye türlerle tanımlanmış union'ların birbiriyle değiştirilebilir olması beklenir
  • Custom unions

    • Diğer kategorilere tam uymayan durumlar için düşünülmüş bir yaklaşımdır
    • Örnekler:
      • kolayca yeniden tanımlanamayan mevcut türler ve hiyerarşiler
      • custom depolama düzenleri
      • custom API biçimleri ve davranışları

Standard - union classes

  • union class, tüm üye türleri tek bir self-contained bildirim içinde barındıran named type union'dır
  • Bildirim enum'a benzer; ancak her üyenin bir veya daha fazla durum değişkeni taşıyabilen bir tür olması bakımından ayrılır
union U
{
    A(int x, string y);
    B(int z);
    C;
}
  • Her üye için yalnızca bir ad ve durum değişkenleri listesi belirtilebilir
  • Oluşturma, üye türün atanmasıyla yapılır
U u = new A(10, "ten");
  • Oluşturulan üyenin türü A'dır ve u değişkenine atanırken U'ya dönüştürülür
  • Ayrıştırma tür testi ve pattern matching ile yapılır
if (u is A a) { ... }

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

if (u is A { y: var y }) { ... }
  • union class exhaustive kabul edilir
    • switch expression ya da statement içinde tüm üye türler ele alınırsa default case gerekmez
var x = u switch {
    A a => a.x,
    B b => b.z,
    C c => 0
    };
  • null, standart nullable gösterimiyle dahil edilebilir
U? u = null;
  • Uygulama, abstract record class ve nested derived record class ile ifade edilir
[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(); };
}
  • Closed attribute'u, dilin taban tür modülü dışında alt tür tanımlanmayacağını anlayabilmesi için kapalı hiyerarşiyi belirtir

Specialized - union structs

  • union struct da tüm üye türleri tek bir self-contained bildirim içinde barındıran named type union'dır
    • Hem union hem de üye türlerin tümü struct'tır ve heap allocation olmadan kullanılabilir
  • Bildirim, union class'a benzer ancak struct anahtar sözcüğü eklenir
union struct U
{
    A(int x, string y);
    B(int z);
    C;
}
  • Oluşturma, ayrıştırma, exhaustive switch ve nullable gösterimi union class ile benzerdir
U u = new A(10, "ten");

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

U? u = null;
  • union struct, atanmamış olabilir ya da default atanması nedeniyle undefined state durumuna girebilir
    • Bu durum, bildirilen hiçbir üye türe karşılık gelmez
    • Exhaustiveness varsayımına dayanan switch'lerde çalışma zamanında istisna oluşabilir
U u = default;

var x = u switch
{
    A a => a.x,
    B b => b.z,
    C c => 0
}
  • Derleyici, struct union'a default atandığında uyarı üretir
// warning: default not a valid state
U u = default;
  • Uyarıdan kaçınmak için union struct içinde bir default durum tanımlanıp belirli bir üye türle eşleştirilebilir
union struct U
{
    A(int x, string y);
    B(int z);
    C = default;
}
  • Uygulama, iç içe record struct üye tipleri ve üye tipler ile aggregate union struct arasında dönüşüm yapan bir API’ye sahip bir struct olarak ifade ediliyor
    • Dahili yerleşim, olası üye tiplerin verisini verimli şekilde depolayacak biçimde derleyici tarafından seçiliyor
    • Hız ve boyut arasındaki tradeoff da derleyici tarafından seçiliyor
[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 => {...};
}
  • Union attribute’ü, ilgili tipin union struct olduğunu tanımlar
  • Varsayılan durumu olan union struct’larda karşılık gelen UnionKind, 0 olarak tanımlanır
  • union struct’ın tam oluşturma API’si belgede henüz gösterilmiyor

union struct için tip testi, boxing, reflection

  • Bilinen bir union struct için tip testi yapıldığında, struct’ın kendi tipi denetlenmek yerine union struct API’si çağrılır
u is A a
  • Yukarıdaki ifade şu şekilde dönüştürülür
u.TryGetA(out var a)
  • switch expression da Kind ve TryGetX çağrılarını kullanan bir biçime dönüştürülür
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’ta üye tip değeri boxed hâle gelmez; union struct’ın kendisi boxing edilir
    • union struct’ın başlıca kullanım senaryosu boxing’den kaçınmaktır, ancak bazen boxing gerekebilir
  • union struct tipi ile üyelerin ilişkili olduğu bilindiğinden, boxed union struct üzerinde üye tipine göre tip testi yapılıp unbox işlemi uygulanabilir
U u = ...;
object value = u;

if (value is A a) {...}
  • Yukarıdaki kod şu şekilde dönüştürülür
if (value is A a || (value is U u && u.TryGetA(out a))) {...}
  • Tersine, boxed member type da union struct olarak test edilip unbox edilebilir
A a = ...;
object value = a;

if (value is U u) {...}
  • Tip testinin her iki tarafının da union struct ile ilişkili olduğu statik olarak bilinemiyorsa, tip testi başarısız olur
bool IsType(object value) => value is T;

U u = new A(...);

if (IsType(u)) {...}
  • reflection kullanırken, boxed member type A’nın boxed union struct U’ya dönüştürülmesi gerekebilir
  • struct union özelliği, çalışma zamanında boxed union struct ile boxed member type arasında dönüşüm yapan yardımcı metotlar sağlar
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 ve ad hoc union, reflection kullanımında zaten doğru biçimdedir; bu yüzden dönüşüm gerekmez

Ref union structs

  • ref modifier’ı olan union struct, durum değişkeni olarak ref veya ref struct içerebilir
ref union struct U
{
    A(ref int x);
    B(ReadOnlySpan y);
    C;
}
  • Bu durumda union implementasyonu ve ref struct değeri taşıyan üye tipleri ref struct’a dönüştürülür
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; }
    ...
}
  • C#’a ref record struct tipi eklenirse, etkilenen üye tipleri record struct olarak kalmaya devam edebilir

Ad Hoc - ad hoc unions

  • Ad hoc union, başka bir yerde bildirilmiş tiplerden oluşturulan anonymous union’dır
  • Söz dizimi, parantez ve or pattern syntax kullanır
(A or B or C)
  • Ortak bir adla başvurmak için file veya global using alias kullanılır
global using U = (A or B or C);
  • Oluşturma işlemi, union üye tiplerinden birinin örneğini ad hoc union tipindeki bir değişkene atayarak yapılır
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");
  • Ayrıştırma, tip testi ve pattern matching ile yapılır
if (u is A a) {...}

if (u is A(var x, var y)) { ... }
  • ad hoc union da exhaustive kabul edilir; tüm üye tipleri ele alınmışsa default case gerekmez
  • null, nullable gösterimiyle dahil edilebilir
(A or B)? x = null;
  • Aynı üye tiplerine sahip ad hoc union’lar, sıra fark etmeksizin derleyici tarafından aynı tip olarak anlaşılır
(A or B) x = new A(10, "ten");
(B or A) y = x;

ad hoc union için atama, değiştirilebilirlik, çıkarım

  • Aynı tipte olan veya subset oluşturan ad hoc union’lar, çalışma zamanı denetimi olmadan superset ad hoc union’a atanabilir
(A or B) x = new A(10, "ten");
(A or B or C) y = x;
  • Bir superset ad hoc union’ı subset ad hoc union’a atamak için açık coercion ve çalışma zamanı denetimi gerekir
(A or B or C) x = new A(10, "ten");
var y = (A or B)x;
  • Kaynak union’daki tüm üye tipleri, hedef union’daki en az bir üyeyle aynıysa veya onun subtype’ıysa, çalışma zamanı denetimi olmadan implicit coercion mümkündür
(Chihuahua or Siamese) pet = ...;
(Cat or Dog) animal = pet;
  • Bunun dışında, source üye türlerinden biri veya daha fazlası target üye türlerinden birinin alt türüyse explicit coercion ve çalışma zamanı kontrolü mümkündür
(Cat or Chihuahua) mostlyCats = ...;
(Dog or Siamese) mostlyDogs = (Dog or Siamese)mostlyCats;
  • ad hoc union olmayan türler de atanabilirlik değerlendirmesinde tek türden ad hoc union gibi görülebilir
    • Bu kural, implemente edilen interface'lerde de çalışır
  • generalized coercions da tanımlanır
    • Herhangi bir türden union'ın üye türlerinden birine implicit coercion varsa, o türdeki bir değer union türüne implicit coercion ile dönüştürülebilir
    • Union'ın tüm üye türleri herhangi bir türe implicit coercion ile dönüştürülebiliyorsa, union değeri o türe implicit coercion ile dönüştürülebilir
    • Union'ın üye türlerinden biri herhangi bir türe coercion ile dönüştürülebiliyorsa, union değeri o türe explicit coercion ile dönüştürülebilir
    • Source union'ın tüm üye türleri target union'ın üyelerinden birine implicit coercion ile dönüştürülebiliyorsa, union'lar arasında implicit coercion mümkündür
    • Source union'ın üyelerinden biri veya daha fazlası target union'ın üyelerinden birine explicit coercion ile dönüştürülebiliyorsa, union'lar arasında explicit coercion mümkündür
  • Birden fazla coercion mümkün olduğunda hangisinin seçileceğine dair kurallar hâlâ gereklidir
  • Bu assignability ilişkisi bir subtyping ilişkisi değildir
    • Bir ad hoc union, başka bir ad hoc union'ın alt türü değildir
  • Aynı üye türlerine sahip ad hoc union'lar generics ve array elements üzerinden birbirinin yerine kullanılabilir
(T1 or T2)[] F(T1 v1, T2 v2) => new (T1 or T2)[] { v1, v2 };

(Dog or Cat)[] pets = F(rufus, petunia);
  • Generic type argument olarak kullanılan ad hoc union'lar, ilgili iki union'ın tüm üye türleri karşılık gelen üyelerle alt tür ilişkisine sahip olduğunda covariance ve contravariance için kullanılabilir
    • Somut kurallar yerine "Have Mads write this part" notu bırakılmıştır
  • ad hoc union, pattern matching'de or pattern'e benzer şekilde çalışır ve bununla birlikte değişken bildirimi de içerebilir
if (u is Dog or Cat) { ... }

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

if (u is (Dog or Cat) pet) {...}
  • ad hoc union değişkenine atama yapıldığında value type boxing oluşabilir
  • conditional expression ve switch expression, bileşen expression'lardan ad hoc union result type çıkarımı yapabilir
Dog rufus = ...;
Cat petunia = ...;
Bird polly = ...;

var u =
      x == 1 ? rufus
    : x == 2 ? petunia
    : polly;
  • lambda expression'ın dönüş türü de lambda body'nin dönüş türlerinden oluşturulan ad hoc union olarak çıkarılabilir
    • Bu durumda da value type boxing oluşabilir

ad hoc union uygulaması ve sınırlamalar

  • ad hoc union, erasure ve çalışma zamanı kontrolleriyle uygulanır
(A or B) ab = new A(10, "ten");
  • Yukarıdaki kod şu şekilde dönüştürülür
object ab = new A(10, "ten");
  • Statik olarak doğru olduğu anlaşılamayan assignment'lar çalışma zamanı kontrolü gerektirir
    • Derleyici, modülde kullanılan her benzersiz ad hoc union için custom method üretir
object value = ...;
var ab = (A or B)value;
  • Dönüştürme örneği şu şekildedir
object value = ...;
object ab = (value);

object (object? value) =>
    value is A or B ? value : throw ...;
  • Method girişinde parameter'lar kontrol edilmez
  • Metadata'da ad hoc union türünü kodlamak için custom attribute'lar kullanılır
void M((A or B) x);
  • Dönüştürme örneği şu şekildedir
void M([AdHocUnion([typeof(A), typeof(B)])] object x);
  • Attribute ayrıntıları henüz belirtilmemiştir
  • Tüm ad hoc union'lar aynı türe erase edildiği için, ad hoc union parameter'ına sahip method'larda gerçek çalışma zamanı overloading'i mümkün değildir
public void Wash((Cat or Dog) pet) { ... }
public void Wash((Compact or Sedan) car) { ... }
  • overloading hâlâ tartışmaya açık bir alandır

Custom union'lar

  • union class veya union struct sözdizimiyle belirtilemeyen bir davranış gerekiyorsa, doğrudan custom class ya da struct tanımlanabilir ve C#'ın bunu custom union type olarak tanıması sağlanabilir
  • Bir union class hierarchy ile uygulanmışsa, Closed attribute'u verildiğinde union class ile aynı exhaustiveness davranışı elde edilebilir
[Closed]
public class U { ... }
public class A(int x, string y) : U { ... }
public class B(int z) : U { ... }
  • specialized storage rules içeren bir struct wrapper ile union uygulanmışsa, Union attribute'u eklenip union pattern'ini izleyen bir API sağlandığında işlevsel olarak union struct ile aynı hâle gelir
[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) { ... }
}
  • Union, member type'ları içermiyorsa veya başka bir API pattern'i kullanıyorsa, derleyicinin beklediği API extensions ile sağlanabilir
  • union struct API pattern'inin tamamı henüz belirtilmemiştir
  • ad hoc union, tek tek member type'ların davranışını değiştirmek dışında özelleştirilemez

Common union'lar: Option ve Result

  • Option, diğer dillerde aynı ada veya aynı amaca sahip türlere benzeyen bir struct union'dır
    • Bir değerin var olabileceğini veya olmayabileceğini ifade eder
public union struct Option
{
    Some(TValue value);
    None = default;
}
  • Kullanım örneği şu şekildedir
Option x = new Some("text");
Option y = None;

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

var v = x is Some(var value) ? value : 0;
  • Option türü tamamen belirtilmemiştir
  • Result da diğer dillerde aynı ada veya amaca sahip türlere benzeyen bir struct union'dır
    • Bir fonksiyondan başarı sonucu veya hata döndürmek için kullanılır
public union struct Result
{
    Success(TValue value);
    Failure(TError error);
}
  • Kullanım örneği şu şekildedir
Result x = Success("hurray!");
Result y = Failure("boo");

switch (x)
{
    case Success(var value): ...;
    case Failure(var error): ...;
}
  • Result tipi de tam olarak belirtilmiş değil

İlgili öneriler

  • Bu öneri, var olduğu varsayılan önerileri veya henüz önerilecek özellikleri içerir
  • Closed Hierarchies

    • abstract base type'a Closed attribute'u uygulandığında, bildirim modülü içindeki tüm subtype'lar kapalı bir subtype kümesi olarak bildirilir
    • Bildirim modülü dışında subtype bildirilirse derleyici hatası oluşur
    • closed hierarchy, derleyici tarafından exhaustive olarak ele alınır ve switch içinde tüm subtype'lar işlenirse default case gerekmez
  • Singleton values

    • static Singleton property'sine sahip singleton type'lar, non-type context içinde bu property'ye örtük olarak erişilip değer gibi kullanılabilir
var x = U.C.Singleton;
  • Yukarıdaki kod şu şekilde yazılabilir
var x = U.C;
  • Nested Member Shorthand

    • Bağlanmamış adlar, target type'ın static member'ına veya nested type'ına bağlanabilir
Color color = Color.Red;
  • Yukarıdaki kod şu şekilde yazılabilir
Color color = Red;
U u = new U.A(10, "ten");
  • Yukarıdaki kod şu şekilde yazılabilir
U u = new A(10, "ten");

Soru-Cevap'ta belirtilen tasarım kararları ve sınırlamalar

  • Union class, doğrudan nested record hierarchy'yi kolayca bildirmek mümkünse zorunlu olmayabilir; ancak kısa sözdizimi ve tek bir struct modifier'ıyla union struct'a kolayca dönüştürülebilmesi avantaj sağlar
  • Union struct, daha az allocation yapar ve daha fazla türde type kullanabilir, ancak her durumda daha uygun değildir
    • Kendi başına allocation yapmasa bile mutlaka daha hızlı değildir
    • Stack footprint'i daha büyüktür ve assignment, pass, return sırasında genellikle kopyalanır
    • Anonymous ad hoc union'lar için kolayca değiştirilebilir olmadığından buna çok uygun değildir
    • Boxed durumda veya generic type parameter olarak statik biçimde temsil edildiğinde type test, cast ve pattern matching sorunları vardır
  • Union struct, iç yapıda tagged union iken aynı zamanda bir type union'dır
    • İçeride, derleyicinin oluşturduğu kodu hızlandırmak için tag görevi gören bir enum property'yi açığa çıkarabilir
    • Dil yüzeyinde ise type test, cast, pattern matching gibi tanıdık yollarla ele alınabildiği için bir type union gibi görünür
  • Derleyici, union struct member type'ı doğrudan union struct değişkenine atarken member type oluşturmayı atlayan optimizasyonlar yapabilir
  • Union doğrudan değişkene ayrıştırıldığında, union struct state değişkenini member type'a kopyalamayı atlayan optimizasyonlar da beklenir
  • Union struct, union class'ta olduğu gibi member type'ın gerçek base type'ı değildir
    • struct, gerçek kalıtıma izin vermez
    • Mantıksal olarak otomatik dönüşümlerle base type gibi davranır, ancak bu ilişki type system ve runtime'ın geneline yayılmaz
  • Ad hoc union şu anki adıyla doğrudan bildirilemez
    • Uzun union tekrarlarından kaçınmak veya açıklayıcı bir ad gerektiğinde global using alias kullanılmalıdır
  • Ad hoc union, value type'ları boxing yapar
    • Boxing'den kaçınmak gerekiyorsa union struct kullanılmalıdır
  • Ad hoc union, ref type'ları içeremez
    • Ref type gerekiyorsa union struct kullanılmalıdır
  • Ad hoc union'ın object'e erase edilmesinin nedeni, günümüzde geliştiricilerin yaygın olarak kullandığı object tabanlı çözümü compile-time type safety ve generated validation checks ile iyileştirme yaklaşımı olmasıdır
  • Ad hoc union'ın ortak property veya method'larına, her type case ele alınmadan doğrudan erişilemez
    • Değere ancak ilgili type'a başarıyla dönüştürüldükten sonra erişilebilir
  • F#'ın union type'ları, bu spesifikasyondaki union class ve union struct'a karşılık gelir; ancak bu öneri üyeleri tag state ve associated state variables olarak değil, dilin type'ları olarak ele alır
    • Ad hoc union, Typescript'in type union'larına benzer
  • Option, null ve nullable reference type'lar ile aynı amaç gerçekleştirilebiliyorsa gerekli olmayabilir
    • Bazı geliştiriciler, C#'ın nullable type'larına kıyasla daha güçlü zorunluluk için option type'ı tercih eder
  • C#, şu anda Option ve Result için F#'ta mümkün olan monadic behavior'ları dile dahil etmiyor
  • Result için birçok kullanım senaryosu, C#'ın exception handling'i ile çözülebilir
    • Ancak çalışma zamanında hata bekleniyor ve sık yaşanıyorsa, exception handling'den kaçınıp çağıranın hatayı açıkça ele alması istenebilir
  • Option ve Result benzeri type'lar üçüncü taraf kütüphanelerde zaten var, ancak birçok geliştirici, kütüphaneler arası interoperability için çalışma zamanında standartlaştırılmış type'ların da eklenmesini talep ediyor

1 yorum

 
GN⁺ 2024-08-09
Hacker News yorumları
  • F#’ta discriminated union’ları birkaç yıl kullandığım için C#’ta artık doğal olarak vardır sanıyordum
    Herkesin sevdiği bir özellik olmayabilir ama tipli dillerde bir şekilde cebirsel veri tipleri (ADT) olmayan bir dile geri dönmek gerçekten zor. Şu an Java kullanıyorum ve genel olarak iyi, ama F#’ta üç satırda yapılacak bir işi wrapper sınıflarla dolanmak zorunda kalmak epey can sıkıyor

    • F#’a bir kez alışınca geri dönmek çok zor. Keşke Microsoft onu daha düzgün destekleyip öne çıkarsa
      Saf fonksiyon gibi kısıtlamalara bağlı kalmadan fonksiyonel programlamanın avantajlarının çoğunu aldığı çok iyi bir denge noktası olduğu için geliştirmesi rahat. Öte yandan C#’ı çok yavaş biçimde F#’a benzetiyorlar; bunu izlemek de tuhaf
    • İyi haber şu ki, günümüzde Java’da da sealed interface ve record ile nispeten az boilerplate’le yapılabiliyor
      Tabii bu, Java ölçütlerine göre böyle
    • 2020’de F# ve .NET kullanmayı bıraktığım zamanki hâliyle CLR’de yalnızca kalıtım vardı ve F#’ın enum’ları da aslında kalıtımla uygulanıyordu
      Örneğin Some ve None, Option sınıfının türetilmiş sınıflarıydı; F# assembly’sine bir decompiler ile bakınca görülebiliyordu. Şu an da aynı mı bilmiyorum ama bu C# önerisi, A or B sözdizimli anonim enum’lar da dahil olmak üzere yalnızca kalıtım üstünde sözdizimsel şekerle yapılması zor görünüyor. Bu yüzden bunun çalışması için CLR’nin enum’u birinci sınıf olarak desteklemesi gerekecek gibi
    • Bu arada Java’da 16. sürümden beri tagged union tipleri var
      Pattern matching de var
    • Java’da zaten birkaç yıldır ADT var, bu yüzden “üç satırda yapılacak işi wrapper sınıflarla dolanmak” sözü biraz garip geliyor
      Records https://en.wikipedia.org/wiki/Java_version_history#Java_16
      Sealed classes https://en.wikipedia.org/wiki/Java_version_history#Java_17
      Ve neredeyse 1 yıldır pattern matching de var https://en.wikipedia.org/wiki/Java_version_history#Java_21
      Yine de F# ile geliştirme yapmanın keyfine büyük ölçüde katılıyorum
  • Bu öneriyi gerçekten heyecanla bekliyorum. C#’ın güçlü yanlarından söz ederken hep özür diler gibi eklemek zorunda kaldığım en büyük eksik özellik buydu
    Bunun dışında C#’ta eksik olan büyük bir dil özelliği aklıma gelmiyor. Bu eklendikten sonra bile HN’de C#’a hâlâ 10 yıl önceki dille aynıymış gibi davranıldığını görmeyi bekliyorum; onu da merak ediyorum

    • Düzgün tip takma adları da olsa iyi olurdu ama bugünlerde C# yazmak gerçekten çok keyifli
    • İyi ya da kötü, C#’ın tasarım hedefi mümkün olan her özelliği, üstelik birden fazla varyantıyla eklemek gibi görünüyor
      Bu yüzden herkese uyum sağlayabiliyor ve güçlü görüşleri olan bir dil gibi insanları dışarı itmiyor; ama ilk öğrenmesi biraz zorlaşabiliyor
  • Buna neden type unions dendiğini açıklayabilir misiniz? Böyle bir adı ilk kez duyuyorum
    ALGOL68’deki gibi tipler arasında union değil, ML ailesi dillerindeki tagged union’a benziyor. C# geliştiricilerinin mevcut terimler yerine başka adlar uydurduğu örneklerden biri mi diye de düşündürüyor. SelectMany, IEnumerable gibi

    • “tagged”, içinde tipi ayırt eden bir tag bulunduğuna dair bir uygulama detayı gibi geliyor
      Muhtemelen “union”ın önüne “type” ekleyerek bunun tiplerle ilgili olduğunu netleştirmek istemişlerdir. Belgelerdeki sözdiziminin kendisi sadece union gibi görünüyor. FAQ’da şöyle yazıyor:
      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.
    • Çünkü bu öneri birkaç union türünü birden ele alıyor
      Derleyicinin build sırasında doğruladığı referans tiplerinin kapalı hiyerarşisi, kapalı hiyerarşi gibi davranacak şekilde derleyicinin işlediği değer tipi olan tagged union, keyfi implementasyona sahip olup yine de aynı derleyici mekanizmasına bağlanabilen kullanıcı tanımlı tipler ve mevcut tiplerin ad-hoc union’ları da buna dahil
  • red/blue/white/black pill gibi renk metaforlarını tamamen kaçırmış olsam da, kapsamlı pattern matching içeren union, bir kez öğrendikten sonra onsuz yaşaması en zor dil özelliklerinden biri
    expression problem’ın sonuçlarını tamamen anladığımı hiç hissetmedim ama şu anki varsayımım şöyle: Geleneksel polimorfizmle genişleme noktaları sunma yaklaşımı, tanımadığım gelecekteki istemcilerin kodu genişletmesi için uygun; kapsamlı pattern matching içeren union ise benim ya da ekibimin sahip olduğu koda daha iyi uyuyor. Genelde o kodu dışarıdan genişletmek istemekten ziyade, iş alanı anlayışı değiştikçe temel veri yapılarını güncellemek, ardından imperative kodun artık alan yapısıyla uyuşmadığı noktaları derleyici hatalarıyla mümkün olduğunca yakalamak istiyorum

    • Farkı anlamaya çalışmak için bu yazıyı yazdım
      https://deliberate-software.com/christmas-f-number-polymorph...
    • expression problem’in özü, nesne yönelimli ve fonksiyonel yaklaşımlarda kod genişletilebilirliğinin nasıl elde edildiği arasındaki karşıtlıkla açıklanabilir diye düşünüyorum
      Nesne yönelimli arayüz/kalıtım yaklaşımında temel sınıfa ya da arayüze yeni tip varyantları eklemek kolaydır, ancak yeni işlev eklemek zordur çünkü mevcut tiplerin hepsinde uygulanması gerekir. Fonksiyonel discriminated union yaklaşımında yeni bir fonksiyon yazıp union üzerinde eşleştirme yapmak kolaydır; derleyici tüm durumların ele alındığını garanti eder, bu yüzden yeni işlev eklemek kolaydır. Ancak yeni bir tip varyantı eklendiğinde kod tabanındaki tüm kapsamlı pattern matching noktalarını güncellemek gerekir, bu da zordur. Kotlin iki yaklaşımı da oldukça iyi desteklediği için iyi bir örnek
  • Terminoloji biraz kaymış değil mi? Bildiğim kadarıyla TypeScript’te union types var
    Ama bu, F# veya Haskell’de gördüğümüz discriminated union’a benziyor. Discriminated union’ın farkının adlandırılmış case constructor’lara sahip olması olduğunu düşünüyorum

    • TypeScript’te “union types” var ve bu öneri onları ad hoc unions diye adlandırıyor gibi
      “type union” en azından benim duyduğum resmî bir terim değil. C# tarafında sum type’ı açıklamak için üretilmiş bir ifade gibi görünüyor
    • C#’tan TypeScript’in type union’ına geçince bunu hep garip bulmuşumdur
      Bunun şimdiye kadar ertelenmiş olmasının iyi bir nedeni olabilir. Çünkü hedef kullanıcıları bilmek gerekiyor. Zamanla alışılıyor ama A|B|undefined çok fazla görünmeye başlayınca okumak yorucu oluyor. Ayrıca bir şeyi alıp üç veya daha fazla kombinasyondan birini kabaca döndürme gibi bir tembellik de doğuyor; çağrı yığınında yukarı çıktıkça giderek daha kafa karıştırıcı hale geliyor
      C#’ın bunu kısıtlı bir biçimde ele almasını seviyorum. Yine de bu özelliği bekleyen tarafta ikna edici bir mantık varsa duymak isterim
  • Uzun süredir C# kullanıyorum ama bu öneride bir şeyi kaçırıyormuşum gibi geliyor. Kullanım senaryosu iyi tanımlanmış görünmüyor; gerçekçi bir örnek verebilir misiniz?
    Önerideki örnekler, boş bir arayüz tanımlayıp birkaç record sınıfın onu “uygulamasını” sağlayarak yapılabilir gibi duruyor. Böyle yapınca ne kaybedildiğini pek anlamıyorum

    • Böyle bir hiyerarşi, işleme sırasında tüm durumları ele alıp almadığınızı bilememeniz anlamında açık bir hiyerarşidir
      Herkes, hatta benim kodumun dışındaki biri bile arayüzün yeni bir uygulamasını oluşturabilir. Ayrıca seçenekler ortak bir yüzeyi hiç paylaşmayabilir; bu da tüm tiplerin “arayüzünün” boş kalması anlamına gelir ki nesne yönelimli tarafta biraz doğallıktan uzaktır. İçeride sonuçta yine bir tip hiyerarşisidir, ancak genişletmenin kapalı olması ve kod case’leri kullanırken atlanmış bir case varsa derleme zamanı hatası vermesi, elle yazılmış uygulamadan temel farkıdır
    • En gerçekçi örnek AST veya veri protokolleridir
      JSON’u örnek alırsak, veriyi tek bir JsonValue sınıfında tutmak yerine, string, boolean, sayı, union tipinden diziler ya da string anahtarlı ve union tipli değerler kullanan map’lerden biri olabilen bir union tipi olarak modelleyebilirsiniz. Rust’taki Result gibi sonuç tipleri de uygulanabilir; böylece bir API’nin değer veya hata döndürdüğü tanımlanabilir. Önerinin generics’i ele alıp almadığını görmedim. Fonksiyonel programlama dünyasında buna çoğunlukla algebraic data type denir ve bu tür tip modellemeye alışınca desteklemeyen dillerde gerçekten özlenir
      [1] https://en.m.wikipedia.org/wiki/Algebraic_data_type
    • Bu kütüphane F# tarzı discriminated union uygular
      [0] https://github.com/mcintyre321/OneOf
      Dış katmanda DTO, hata vb. şeyleri tek bir sarmalayıcıya alan Result tipi döndürmek için kullanmıştım. Sonuç üzerinde pattern matching yapılabildiği için hata işleme kolaylaşıyor ve sınırdaki fonksiyon imzalarını değiştirmeden modeli zaman içinde evrimleştirebiliyorsunuz
    • Son 5 yıldan uzun süredir boş arayüzlerle F#’ın discriminated union’ını taklit ediyorum ve gayet iyi çalıştı
      “switch’te tüm case’leri ele almadım” sorunu çok nadiren, kabaca 50 bin satır kodda bir kez ortaya çıktı ve genelde ilk smoke test çalıştırmasından sonra düzelttim. Bu yüzden büyük bir sorun değildi. .NET ekibinin discriminated union’ı hâlâ eklememiş olmasının nedeni de bunun arayüzlerle etkili biçimde taklit edilebilmesi diye düşünüyorum
    • Basit bir örnek, Ok(T value) veya Error(string message) değerlerinden biri olabilen Result’tır
      Değeri almak için iki durumu da switch etmeniz gerekir; bu da hata durumunu o noktada ele almayı zorunlu kılar
  • covariance / contravariance başlığının altında “Note: Have Mads write this part.” yazması güldürdü :)

    • Buradaki Mads, C#’ın baş tasarımcısı Mads Torgersen’i ifade ediyor
  • “union struct’ın iç yerleşimi, olası üye türlerinin içindeki verileri verimli biçimde saklamak için derleyicinin hız ve boyut arasında bir ödünleşim seçmesiyle belirlenir” gibi bir bölüm var.
    Geçmişte FieldOffset ile C# union kara büyüsünü fazlaca denemiş ve fena yanmış biri olarak, burada üzücü bir sorun var. İşaretçi/ref değerleriyle değer türlerini alias etmek UB’dir. Yani u64 ile object’ten oluşan bir struct union’da her biri için ayrı alan gerekir ve 8 bayt boşa gider. ryujit/GC bunu bilecek şekilde güncellenmediği sürece durum budur.

    • UB değil, yasadışı. ECMA 335, II.10.7’ye göre alanları bu şekilde üst üste bindirebilirsiniz, ancak nesne referansının kapladığı offset, yerleşik bir değer türünün ya da başka bir nesne referansının bir parçasının kapladığı offset ile çakışmamalıdır.
      .NET 8.0’da FieldOffset(0) üzerinde UInt64 Foo ile Object Bar’ı birlikte koyan kod derlenir, ancak yükleme sırasında System.TypeLoadException fırlatır. Mesaj, offset 0’daki object alanının non-object alanla hatalı hizalandığını veya çakıştığını söyler. İlginç biçimde AOT derleyicisi bu metodun her zaman throw edeceği konusunda uyarır, ama C# derleyicisi hiçbir uyarı vermez.
  • C#’ın daha iyi bir nesne yönelimli dil olmak yerine, sürekli daha çirkin bir F# olmaya çalışıyor gibi görünmesi üzücü.
    Örneğin çoklu dispatch sözdizimi neden hâlâ bu kadar kaba? Pseudo OO dünyayı ele geçirdi ve insanlar buna tepki gösterdi; bu yüzden gerçekten nesne yönelimliliği iyi yapmaya çalışmak yerine “süslü parantezli biraz fonksiyonel bir dil” olmanın ilgiyi korumak açısından daha kolay olduğunu anlıyorum, ama yine de.

    • Karşı tarafta da aynı şikâyet var. Ana akım diller eskiden geniş kapsamlı değiştirilebilir durum ve yan etkilere sahipti; şimdi bunlara yalnızca lambda eklenmiş durumda.
      Peki nesne yönelimli tarafta eksik olan ne? C#’ın ya da başka bir dilin “nesne yönelimliliği iyi yaptığı” bir duruma gelmesi için ne gerekir?
    • Burada “daha iyi nesne yönelimli” ile ne kastediliyor?
      Aklınızda hangi kavramların veya özelliklerin olduğunu merak ediyorum.
  • Şu anda private constructor’lı iç içe record’ları ve NuGet paketi https://github.com/shuebner/ClosedTypeHierarchyDiagnosticSup... ile birlikte kullanarak switch türünde _ case’inin gerekli olmamasını garanti ediyorum.
    Özünde bu teklifin “Union Classes” kısmının desugar edilmiş bir sürümü ve şimdiden oldukça iyi çalışıyor. Yine de NuGet paketine gerek kalmaması ve sözdizimsel şeker gelmesi güzel olur; bu yüzden bu teklifi beğendim.

    • record türlerinin private constructor’ı olsa bile kapalı olmadığına dikkat etmek gerekir.
      Derleyici kopyalama işlemi için protected bir constructor üretir ve bu constructor miras alınabilir. Bunu engellemek için kendiniz protected bir constructor tanımlayıp, geçerli bir case değilse çalışma zamanında istisna fırlatmasını sağlamalısınız.