- É uma proposta para introduzir type unions (discriminated unions) no C#, permitindo expressar que uma variável ou parâmetro contenha apenas um entre vários tipos limitados; no momento está na fase Proposed, e prototype, implementation e specification estão em estado Not Started
- As implementações existentes baseadas em hierarquia de herança ou em
object têm dificuldade para satisfazer ao mesmo tempo conjunto fechado de tipos, combinação de tipos não relacionados, armazenamento de valores sem wrapper e evasão de alocação, por isso a proposta se divide em quatro categorias de union
- A proposta distingue union class, union struct, ad hoc union e custom union, e cada abordagem tem restrições diferentes quanto à forma de declaração, alocação e possibilidade de reutilizar tipos existentes
- Em
switch e pattern matching, ao tratar todos os tipos membros, ela fornece exhaustiveness, sem necessidade de default; porém, em union struct, default pode não corresponder a um membro declarado, exigindo warning e definição explícita de um membro default
- Ad hoc union usa a sintaxe
(A or B or C) para agrupar tipos existentes e é implementada com erasure e verificação em tempo de execução, mas tem limitações como boxing de value types, impossibilidade de ref type e ausência de true runtime overloading
Objetivo e motivação da proposta
- Type unions for C# é uma proposta para introduzir type unions, ou seja, discriminated unions, no C#
- O status do documento é Proposed
- Prototype, Implementation e Specification estão todos marcados como Not Started
- No desenvolvimento de software, há situações em que uma variável precisa armazenar não sempre o mesmo tipo, mas sim um entre um conjunto limitado de tipos relacionados
- Um exemplo é o caso de
Customer e Supplier, que compartilham apenas algumas propriedades, mas exigem operações parecidas conforme suas diferenças
- A abordagem de distribuir a implementação para cada tipo por meio de métodos abstratos comuns ou interfaces é adequada quando aquele tipo existe para essa operação ou quando essa operação é uma parte essencial do tipo
- Se o tipo tiver um propósito mais amplo, adicionar esse tipo de método pode não ser desejável
- Também é possível criar um tipo base comum, como
Contact, por herança, mas isso é difícil ou inadequado nos seguintes casos
- quando você não controla a definição dos tipos
- quando há muitas situações parecidas e a herança só resolve uma delas
- quando você não quer vazar os requisitos de uma operação específica para a definição dos dados
- Usar
object pode funcionar, mas a garantia de que apenas valores corretos serão inseridos precisa ser mantida com documentação e comentários
- Também é possível proteger isso com uma camada de wrapper ou um tipo agregado customizado, mas, se houver muitos conjuntos de tipos para cada situação, isso se torna demorado e trabalhoso
- O objetivo é permitir que o C# declare que uma mesma posição armazena um entre vários tipos limitados, deixando a linguagem responsável por proteger a variável
Quatro categorias de union
- Como é difícil atender todos os casos de uso com uma única implementação, a proposta se divide em quatro categorias
-
Standard - union classes
- Usado quando se quer definir a union e seus membros juntos, e quando há intenção de usar os membros como classes independentes
- Voltado para casos em que alocação de classes não é um problema
- Exemplos:
- protocolos, serialização e tipos de transferência de dados
- modelos de dados de UI (XAML)
- syntax tree
- estados de máquina de estados que não mudam com frequência
- outros modelos de dados polimórficos
- valores mantidos por bastante tempo em forma de union, como campos ou propriedades
-
Specialized - union structs
- Usado quando é necessário evitar alocação ou usar tipos especializados, aceitando algumas restrições em troca disso
- Exemplos:
- valores alocados em arrays contíguos
- valores mapeados sobre blocos de memória (interop)
- estados de máquina de estados que mudam com frequência
- valores mantidos por pouco tempo em forma de union, como argumentos ou valores de retorno
- tipos de biblioteca com potencial de uso especializado
-
Ad Hoc - ad hoc unions
- Usado quando é preciso compor uma union com tipos já existentes e que podem não ter relação entre si
- Unions declaradas com os mesmos tipos membros devem ser intercambiáveis entre si
-
Custom unions
- Uma abordagem para casos que não se encaixam bem nas outras categorias
- Exemplos:
- tipos e hierarquias existentes que não podem ser redefinidos com facilidade
- layout de armazenamento customizado
- formato e comportamento de API customizados
Standard - union classes
- union class é uma named type union que coloca todos os tipos membros dentro de uma única declaração self-contained
- A declaração é parecida com a de um enum, mas difere porque cada membro é um tipo que pode ter estado por meio de uma ou mais variáveis de estado
union U
{
A(int x, string y);
B(int z);
C;
}
- Para cada membro, só é possível especificar o nome e a lista de variáveis de estado
- A criação é feita atribuindo um tipo membro
U u = new A(10, "ten");
- O tipo do membro criado é
A, e ele é convertido para U ao ser atribuído à variável u
- A desconstrução é feita com teste de tipo e pattern matching
if (u is A a) { ... }
if (u is A(var x, var y)) { ... }
if (u is A { y: var y }) { ... }
- Union class é considerada exhaustive
- Se todos os tipos membros forem tratados em uma
switch expression ou statement, não é necessário um caso default
var x = u switch {
A a => a.x,
B b => b.z,
C c => 0
};
null pode ser incluído com a notação nullable padrão
U? u = null;
- A implementação é representada com abstract record class e 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(); };
}
- O atributo
Closed faz a linguagem entender que se trata de uma hierarquia fechada, na qual subtipos não são declarados fora do módulo do tipo base
Specialized - union structs
- union struct também é uma named type union que coloca todos os tipos membros dentro de uma única declaração self-contained
- Tanto a union quanto os tipos membros são struct, podendo ser usados sem heap allocation
- A declaração é semelhante à de union class, mas adiciona a palavra-chave
struct
union struct U
{
A(int x, string y);
B(int z);
C;
}
- Criação, desconstrução, exhaustive switch e notação nullable são semelhantes aos de union class
U u = new A(10, "ten");
if (u is A a) { ... }
U? u = null;
- Union struct pode ficar sem atribuição ou receber
default, entrando em undefined state
- Esse estado não corresponde a nenhum dos tipos membros declarados
- Em uma switch que depende de exhaustiveness, isso pode causar uma exceção em tempo de execução
U u = default;
var x = u switch
{
A a => a.x,
B b => b.z,
C c => 0
}
- O compilador gera warning quando
default é atribuído a uma struct union
// warning: default not a valid state
U u = default;
- Para evitar o warning, é possível declarar um estado default na union struct e vinculá-lo a um tipo membro específico
union struct U
{
A(int x, string y);
B(int z);
C = default;
}
- A implementação é representada como uma
struct com tipos de membros record struct aninhados e APIs para converter entre os tipos de membros e a struct de união agregada
- O layout interno é escolhido pelo compilador para armazenar com eficiência os dados dos possíveis tipos de membros
- O compilador também escolhe o trade-off entre velocidade e tamanho
[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 => {...};
}
- O atributo
Union identifica que aquele tipo é uma union struct
- Uma
union struct com estado default declara o UnionKind correspondente como 0
- A API completa de criação da
union struct ainda não está mostrada no documento
Teste de tipo, boxing e reflection em union struct
- Ao fazer um teste de tipo em uma
union struct conhecida, o compilador chama a API da union struct em vez de verificar o próprio tipo da struct
u is A a
- A expressão acima é transformada da seguinte forma
u.TryGetA(out var a)
- A
switch expression também é transformada para uma forma que usa Kind e chamadas 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 ...;
}
- Uma
union struct boxed não significa que o valor do tipo de membro foi boxed, e sim que a própria union struct foi boxed
- Um dos principais casos de uso de
union struct é evitar boxing, mas às vezes boxing pode ser necessário
- Como o tipo
union struct e seus membros são conhecidos como relacionados entre si, é possível fazer teste de tipo e unboxing de uma union struct boxed como um tipo de membro
U u = ...;
object value = u;
if (value is A a) {...}
- O código acima é transformado da seguinte forma
if (value is A a || (value is U u && u.TryGetA(out a))) {...}
- No sentido inverso, um tipo de membro boxed também pode ser testado e ter unboxing como
union struct
A a = ...;
object value = a;
if (value is U u) {...}
- Se não for possível saber estaticamente que os dois lados do teste de tipo estão relacionados a uma
union struct, o teste de tipo falha
bool IsType(object value) => value is T;
U u = new A(...);
if (IsType(u)) {...}
- Ao usar reflection, pode ser necessário converter o tipo de membro boxed
A para a union struct boxed U
- O recurso de
struct union fornece métodos utilitários em tempo de execução para converter entre union struct boxed e tipo de membro boxed
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 e ad hoc union já estão na forma correta para uso com reflection, então não precisam de conversão
Ref union structs
- Uma
union struct com modificador ref pode incluir refs ou ref structs como variáveis de estado
ref union struct U
{
A(ref int x);
B(ReadOnlySpan y);
C;
}
- Nesse caso, a implementação da união e os tipos de membros que têm valores
ref struct são transformados em 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; }
...
}
- Se um tipo
ref record struct for adicionado ao C#, os tipos de membros afetados poderão continuar sendo record struct
Ad Hoc - uniões ad hoc
- Uma ad hoc union é uma união anônima formada por tipos declarados em outro lugar
- A sintaxe usa parênteses e a sintaxe de pattern
or
(A or B or C)
- Para referenciar por um nome comum, usa-se um alias
using de arquivo ou global
global using U = (A or B or C);
- A criação é feita atribuindo uma instância de um dos tipos de membro da união a uma variável do tipo de união ad hoc
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");
- A desmontagem é feita com teste de tipo e pattern matching
if (u is A a) {...}
if (u is A(var x, var y)) { ... }
- A
ad hoc union também é considerada exaustiva, então, se todos os tipos de membro forem tratados, não é necessário um caso default
null pode ser incluído com a notação nullable
(A or B)? x = null;
ad hoc unions com os mesmos tipos de membro são entendidas pelo compilador como o mesmo tipo, independentemente da ordem
(A or B) x = new A(10, "ten");
(B or A) y = x;
Atribuição, intercambialidade e inferência em ad hoc union
- Uma
ad hoc union do mesmo tipo ou que seja um subconjunto pode ser atribuída a uma ad hoc union superconjunto sem verificação em tempo de execução
(A or B) x = new A(10, "ten");
(A or B or C) y = x;
- Para atribuir uma
ad hoc union superconjunto a uma ad hoc union subconjunto, é necessária uma coerção explícita e verificação em tempo de execução
(A or B or C) x = new A(10, "ten");
var y = (A or B)x;
- Se todos os tipos de membro da união de origem forem iguais a pelo menos um membro da união de destino, ou forem subtipo dele, a coerção implícita é possível sem verificação em tempo de execução
(Chihuahua or Siamese) pet = ...;
(Cat or Dog) animal = pet;
- Mesmo quando não for esse o caso, se um ou mais tipos de membro do source forem subtipo de um ou mais tipos de membro do target, coercion explícita e verificação em tempo de execução são possíveis
(Cat or Chihuahua) mostlyCats = ...;
(Dog or Siamese) mostlyDogs = (Dog or Siamese)mostlyCats;
- Tipos que não são ad hoc unions também podem ser tratados como uma ad hoc union de tipo único ao determinar assignability
- Essa regra também funciona para interfaces implementadas
- Generalized coercions também são definidas
- Se houver implicit coercion de um tipo para um dos tipos de membro da union, um valor desse tipo pode sofrer implicit coercion para o tipo union
- Se todos os tipos de membro da union puderem sofrer implicit coercion para algum tipo, um valor union pode sofrer implicit coercion para esse tipo
- Se um dos tipos de membro da union puder sofrer coercion para algum tipo, um valor union pode sofrer explicit coercion para esse tipo
- Se todos os tipos de membro da union source puderem sofrer implicit coercion para um dos membros da union target, implicit coercion entre unions é possível
- Se um ou mais membros da union source puderem sofrer explicit coercion para um dos membros da union target, explicit coercion entre unions é possível
- Ainda são necessárias regras sobre qual coercion escolher quando várias forem possíveis
- Essa relação de assignability não é uma relação de subtyping
- Uma ad hoc union não é subtipo de outra ad hoc union
- Ad hoc unions com os mesmos tipos de membro podem ser trocadas por meio de generics e elementos de array
(T1 or T2)[] F(T1 v1, T2 v2) => new (T1 or T2)[] { v1, v2 };
(Dog or Cat)[] pets = F(rufus, petunia);
- Uma ad hoc union usada como generic type argument pode ser usada com covariance e contravariance quando todos os tipos de membro das duas unions relacionadas tiverem relação de subtipo com o membro correspondente
- Em vez de regras concretas, ficou apenas a nota “Have Mads write this part”
- Ad hoc unions funcionam de forma semelhante ao padrão
or em pattern matching e também podem incluir declaração de variável
if (u is Dog or Cat) { ... }
if (u is (Dog or Cat)) { ... }
if (u is (Dog or Cat) pet) {...}
- Ao atribuir a uma variável de ad hoc union, pode ocorrer boxing de value type
- Conditional expression e switch expression podem inferir um ad hoc union result type a partir das expressions que os compõem
Dog rufus = ...;
Cat petunia = ...;
Bird polly = ...;
var u =
x == 1 ? rufus
: x == 2 ? petunia
: polly;
- O tipo de retorno de uma lambda expression também pode ser inferido como uma ad hoc union formada pelos tipos de retorno do corpo da lambda
- Nesse caso também pode ocorrer boxing de value type
Implementação e limitações de ad hoc unions
- Ad hoc unions são implementadas com erasure e verificação em tempo de execução
(A or B) ab = new A(10, "ten");
- O código acima é transformado da seguinte forma
object ab = new A(10, "ten");
- Assignments cuja correção não pode ser conhecida estaticamente exigem verificação em tempo de execução
- O compilador gera um método custom para cada ad hoc union única usada no módulo
object value = ...;
var ab = (A or B)value;
- Um exemplo da transformação é o seguinte
object value = ...;
object ab = (value);
object (object? value) =>
value is A or B ? value : throw ...;
- Na entrada do método, os parâmetros não são verificados
- Nos metadados, tipos ad hoc union são codificados usando custom attributes
void M((A or B) x);
- Um exemplo da transformação é o seguinte
void M([AdHocUnion([typeof(A), typeof(B)])] object x);
- Os detalhes do attribute ainda não foram especificados
- Como todas as ad hoc unions sofrem erase para o mesmo tipo, true runtime overloading para métodos com parâmetro ad hoc union não é possível
public void Wash((Cat or Dog) pet) { ... }
public void Wash((Compact or Sedan) car) { ... }
- Overloading ainda é uma área em aberto para discussão
Custom unions
- Se for necessário um comportamento que não pode ser especificado com a sintaxe de union class ou union struct, é possível declarar diretamente uma custom class ou struct e fazer o C# reconhecê-la como um custom union type
- Quando a union é implementada com class hierarchy, aplicar o atributo
Closed permite obter o mesmo comportamento de exhaustiveness de uma union class
[Closed]
public class U { ... }
public class A(int x, string y) : U { ... }
public class B(int z) : U { ... }
- Quando a union é implementada com um struct wrapper com specialized storage rules, adicionar o atributo
Union e fornecer uma API que siga o union pattern a torna funcionalmente equivalente a uma 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) { ... }
}
- Se a union não incluir member types ou usar outro API pattern, é possível fornecer por meio de extensions a API esperada pelo compilador
- O union struct API pattern completo ainda não foi especificado
- Ad hoc unions não podem ter comportamento customizado, exceto pela modificação do comportamento dos tipos de membro individuais
Unions comuns: Option e Result
Option é uma struct union semelhante a tipos com o mesmo nome ou a mesma finalidade em outras linguagens
- Representa que um valor pode existir ou não existir
public union struct Option
{
Some(TValue value);
None = default;
}
- Um exemplo de uso é o seguinte
Option x = new Some("text");
Option y = None;
if (x is Some(var value)) {...}
var v = x is Some(var value) ? value : 0;
- O tipo
Option não está totalmente especificado
Result também é uma struct union semelhante a tipos com o mesmo nome ou propósito em outras linguagens
- É usado para retornar de uma função um resultado de sucesso ou um erro
public union struct Result
{
Success(TValue value);
Failure(TError error);
}
- Um exemplo de uso é o seguinte
Result x = Success("hurray!");
Result y = Failure("boo");
switch (x)
{
case Success(var value): ...;
case Failure(var error): ...;
}
- O tipo
Result também não está completamente especificado
Propostas relacionadas
- Esta proposta inclui propostas assumidas como existentes ou recursos que ainda serão propostos
-
Closed Hierarchies
- Aplicar o atributo
Closed a um tipo base abstrato declara todos os subtipos dentro do módulo de declaração como um conjunto fechado de subtipos
- Declarar um subtipo fora do módulo de declaração gera um erro do compilador
- A closed hierarchy é tratada pelo compilador como exaustiva, e se todos os subtipos forem tratados no
switch, o caso default não é necessário
-
Singleton values
- Tipos singleton com a propriedade estática
Singleton podem acessar implicitamente essa propriedade em um contexto não tipado e ser usados como valores
var x = U.C.Singleton;
- O código acima pode ser escrito assim
var x = U.C;
-
Nested Member Shorthand
- Nomes não vinculados podem ser vinculados a membros estáticos ou tipos aninhados do tipo de destino
Color color = Color.Red;
- O código acima pode ser escrito assim
Color color = Red;
U u = new U.A(10, "ten");
- O código acima pode ser escrito assim
U u = new A(10, "ten");
Decisões de design e limitações explicitadas no Q&A
- Uma union class não é necessariamente indispensável se for possível declarar facilmente uma nested record hierarchy diretamente, mas tem como vantagens a sintaxe concisa e a facilidade de mudar para uma union struct com apenas o modificador
struct
- Uma union struct faz menos alocações e pode usar mais tipos de tipo, mas nem sempre é a opção mais adequada
- Mesmo sem alocação própria, não é necessariamente mais rápida
- Tem uma stack footprint maior e normalmente é copiada em assignment, pass e return
- Não se encaixa bem em unions anônimas ad hoc, já que não é facilmente intercambiável
- Há problemas com testes de tipo, casts e pattern matching quando é representada estaticamente em estado boxed ou como generic type parameter
- Uma union struct é internamente uma tagged union e, ao mesmo tempo, uma type union
- Internamente, ela expõe uma propriedade enum que atua como tag para tornar mais rápido o código gerado pelo compilador
- Na superfície da linguagem, ela aparece como uma type union para poder ser tratada com formas familiares como testes de tipo, casts e pattern matching
- O compilador pode fazer uma otimização que pula a criação do tipo membro quando um member type de union struct é atribuído imediatamente a uma variável de union struct
- Ao desconstruir uma union diretamente em uma variável, espera-se também uma otimização que evita copiar a variável de estado da union struct para o tipo membro
- Uma union struct não é o tipo base real do member type como ocorre com uma union class
struct não permite herança real
- Logicamente, ela funciona como um tipo base por meio de conversões automáticas, mas essa relação não se estende por todo o type system e runtime
- Atualmente, unions ad hoc não podem ser declaradas diretamente com nome próprio
- Para evitar repetição longa de unions ou quando for necessário um nome descritivo, é preciso usar
global using alias
- Unions ad hoc fazem boxing de value types
- Se for preciso evitar boxing, deve-se usar union struct
- Unions ad hoc não podem incluir
ref types
- Se forem necessários
ref types, deve-se usar union struct
- O motivo de a ad hoc union ser apagada para
object é que isso melhora, com type safety em tempo de compilação e checks de validação gerados, a solução baseada em object que os desenvolvedores usam hoje com mais frequência
- Propriedades ou métodos comuns de uma ad hoc union não podem ser acessados diretamente sem tratar cada caso de tipo
- Só é possível acessar o valor depois de convertê-lo com sucesso para um tipo individual
- Os union types de F# correspondem à union class e à union struct desta especificação, mas esta proposta trata os membros como tipos da linguagem, e não como tag state e associated state variables
- A ad hoc union é semelhante às type unions do Typescript
Option pode não ser necessário se null e nullable reference types já puderem cumprir o mesmo objetivo
- Alguns desenvolvedores preferem option type para uma imposição mais forte do que os nullable types de C#
- Atualmente, C# não inclui na linguagem os comportamentos monádicos possíveis em F# para
Option e Result
- Muitos casos de uso de
Result podem ser resolvidos com exception handling em C#
- Ainda assim, quando erros são esperados em runtime e ocorrem com frequência, pode ser desejável evitar exception handling e fazer com que o chamador trate o erro explicitamente
- Tipos semelhantes a
Option e Result já existem em bibliotecas de terceiros, mas vários desenvolvedores pedem que tipos padronizados em runtime sejam incluídos para interoperabilidade entre bibliotecas
1 comentários
Opiniões no Hacker News
Depois de usar uniões discriminadas em F# por alguns anos, achei que C# já teria isso como algo óbvio
Talvez não seja um recurso que todo mundo adore, mas, em uma linguagem com tipos, é realmente difícil voltar para uma linguagem que não tenha alguma forma de tipo de dado algébrico (ADT). Hoje uso Java e, no geral, é ok, mas incomoda bastante quando preciso contornar com classes wrapper algo que em F# levaria três linhas
Ela é confortável para desenvolver porque atinge um ponto de equilíbrio excelente, aproveitando muitas vantagens do funcional sem ficar preso a restrições como funções puras. Por outro lado, estão transformando C# em F# muito lentamente, e é estranho ver isso
Mas isso é, claro, considerando os padrões do Java
Por exemplo,
SomeeNoneeram classes derivadas da classeOption, e dava para confirmar isso vendo um assembly F# em um descompilador. Não sei se ainda é assim, mas esta proposta para C# parece difícil de implementar apenas como açúcar sintático sobre herança, inclusive com o enum anônimo da sintaxeA or B. Então, para isso funcionar, acho que o CLR precisaria dar suporte a enums como cidadãos de primeira classeTambém tem pattern matching
Records https://en.wikipedia.org/wiki/Java_version_history#Java_16
Sealed classes https://en.wikipedia.org/wiki/Java_version_history#Java_17
E há quase 1 ano também tem pattern matching https://en.wikipedia.org/wiki/Java_version_history#Java_21
Ainda assim, concordo bastante com o prazer de desenvolver em F#
Estou realmente empolgado com esta proposta. Era o maior recurso ausente que eu sempre precisava acrescentar meio em tom de desculpa ao falar das vantagens de C#
Fora isso, é difícil pensar em algum grande recurso de linguagem que falte em C#. Agora, mesmo que isso entre, também estou ansioso para ver o HN continuar tratando C# como se fosse a mesma linguagem de 10 anos atrás
Por isso ele consegue se adaptar a qualquer pessoa e não afasta usuários como uma linguagem muito opinativa, mas pode ficar um pouco complicado de aprender no começo
Alguém pode explicar por que chamam isso de type unions? Nunca ouvi esse nome antes
Não parece uma union entre tipos no estilo ALGOL68, mas sim uma tagged union de linguagens da família ML. Fico pensando se é mais um caso de desenvolvedores C# criando um nome diferente em vez de usar a terminologia existente, como
SelectMany,IEnumerablee coisas do tipoTalvez tenham colocado “type” antes de “union” para deixar claro que se trata de tipos. Na documentação, a sintaxe em si parece ser apenas
union. O FAQ diz o seguinte: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.
Ela inclui hierarquias fechadas de tipos de referência verificadas pelo compilador no build, uniões tagged que são tipos de valor tratados pelo compilador para se comportarem como hierarquias fechadas, tipos definidos pelo usuário que podem ter implementação arbitrária e ainda assim se conectar ao mesmo mecanismo do compilador, além de unions ad-hoc de tipos existentes
Perdi todas as metáforas de cores tipo red/blue/white/black pill, mas uma union com pattern matching exaustivo é um daqueles recursos de linguagem dos quais fica mais difícil viver sem depois que você conhece
Nunca senti que entendi completamente as implicações do expression problem, mas minha hipótese atual é esta: a forma de oferecer pontos de extensão com polimorfismo tradicional é adequada quando clientes futuros, que eu não conheço, vão estender o código; já unions com pattern matching exaustivo se encaixam melhor em código que eu ou minha equipe possuímos. Normalmente, mais do que querer que esse código seja estendido externamente, eu quero atualizar as estruturas de dados centrais conforme o entendimento do domínio de negócio muda e, depois, fazer o compilador capturar o máximo possível, como erros, dos pontos em que o código imperativo não combina mais com a estrutura do domínio
https://deliberate-software.com/christmas-f-number-polymorph...
No modelo de interfaces/herança da orientação a objetos, é fácil adicionar novas variantes de tipo a uma classe base ou interface, mas é difícil adicionar uma nova funcionalidade, porque ela precisa ser implementada em todos os tipos existentes. No modelo funcional de uniões discriminadas, é fácil adicionar nova funcionalidade criando uma nova função e fazendo match sobre a union, com o compilador garantindo o tratamento de todos os casos; mas, ao adicionar uma nova variante de tipo, fica difícil porque é preciso atualizar o pattern matching exaustivo em toda a base de código. Kotlin dá suporte razoavelmente bom aos dois modelos, então serve bem como exemplo
A terminologia não está meio desalinhada? Pelo que sei, TypeScript tem union types
Mas isto parece mais uma discriminated union como as que eu via em F# ou Haskell. Acho que a diferença de uma união discriminada é ter case constructors nomeados
“type union” pelo menos não é um termo formal que eu já tenha ouvido. Parece uma expressão criada no lado do C# para explicar sum types
Talvez tenha havido um bom motivo para adiarem isso até agora. É preciso conhecer o público-alvo. Com o tempo você se acostuma, mas ver
A|B|undefineddemais cansa a leitura. Também aparece uma certa preguiça de receber uma coisa e retornar de qualquer jeito uma combinação de três ou mais, e quanto mais isso sobe na pilha de chamadas, mais confuso ficaGosto do fato de C# tratar isso de uma forma restrita. Dito isso, se houver um argumento convincente de quem espera esse recurso, eu gostaria de ouvir
Uso C# há muito tempo, mas sinto que estou deixando passar algo nesta proposta. Os casos de uso não parecem bem definidos; alguém pode dar um exemplo realista?
Os exemplos da proposta parecem implementáveis declarando uma interface vazia e fazendo algumas classes record “implementarem” essa interface. Não sei bem o que se perde ao fazer assim
Qualquer um, até fora do seu código, pode criar uma nova implementação da interface. Além disso, as opções podem não compartilhar nenhuma superfície comum, então a “interface” de todos os tipos acaba ficando vazia, o que é um pouco antinatural em orientação a objetos. Internamente ainda é uma hierarquia de tipos, mas a diferença essencial em relação a uma implementação manual é que a extensão é fechada e, quando o código usa cases e deixa algum de fora, há erro em tempo de compilação
Pegando JSON como exemplo: em vez de colocar os dados em uma única classe
JsonValue, você pode modelar como um tipo union que é um dentre string, booleano, número, array do tipo union ou map cujos valores são do tipo union e cujas chaves são strings. Também dá para implementar tipos de resultado como oResultdo Rust, permitindo definir que uma API retorna um valor ou um erro. Não vi se a proposta trata de genéricos. No mundo da programação funcional, isso costuma ser chamado de algebraic data type, e, depois que você se acostuma a esse tipo de modelagem de tipos, sente muita falta em linguagens que não dão suporte a ele[1] https://en.m.wikipedia.org/wiki/Algebraic_data_type
[0] https://github.com/mcintyre321/OneOf
Usei quando queria que uma camada externa retornasse um tipo
Resultencapsulando DTOs, erros etc. em uma coisa só. Como dá para fazer pattern matching sobre o resultado, o tratamento de erros fica mais fácil, e o modelo pode evoluir com o tempo sem mudar a assinatura das funções na fronteiraO problema de “não tratei todos os cases no switch” apareceu muito raramente, algo como uma vez a cada 50 mil linhas de código, e normalmente era corrigido depois de rodar o primeiro smoke test. Então não foi um grande problema. Acho que o motivo de a equipe do .NET ainda não ter adicionado uniões discriminadas é que dá para imitá-las de forma eficaz com interfaces
Ok(T value)ouError(string message)Para obter o valor, você precisa fazer
switchnos dois casos, então é forçado a tratar o caso de erro ali mesmoAchei engraçado que, na seção sobre covariance / contravariance, está escrito “Note: Have Mads write this part.” :)
Há uma parte que diz: “o layout interno de uma union struct faz o compilador escolher um compromisso entre velocidade e tamanho para armazenar de forma eficiente os dados dos possíveis tipos membros”
Como alguém que, no passado, tentou demais fazer magia negra de union em C# com
FieldOffsete se deu muito mal, há aqui um problema lamentável. Fazer aliasing entre valores de ponteiro/ref e tipos de valor é UB. Ou seja, uma struct union deu64eobjectprecisa de campos separados para cada um, desperdiçando 8 bytes. Pelo menos enquanto oryujit/GC não for atualizado para saber dissoNo .NET 8.0, um código que coloca
UInt64 FooeObject Barjuntos emFieldOffset(0)compila, mas lançaSystem.TypeLoadExceptionao carregar. A mensagem diz que o campo object no offset 0 está alinhado incorretamente ou se sobrepõe a um campo non-object. Curiosamente, o compilador AOT avisa que esse método sempre lança, mas o compilador C# não emite nenhum avisoÉ uma pena que, em vez de se tornar uma linguagem orientada a objetos melhor, C# pareça continuar tentando virar um F# mais feio
Por exemplo, por que a sintaxe de multiple dispatch ainda é tão desajeitada? Entendo que pseudo-OO dominou o mundo e as pessoas reagiram contra isso, então era mais fácil continuar relevante virando “uma linguagem um pouco funcional com chaves” do que realmente tentar fazer orientação a objetos bem
Mas o que está faltando do lado da orientação a objetos? O que seria necessário para C# ou outra linguagem chegar ao ponto de “fazer orientação a objetos bem”?
Fico curioso para saber quais conceitos ou recursos você tem em mente
Atualmente uso records aninhados com construtores privados junto com o pacote NuGet https://github.com/shuebner/ClosedTypeHierarchyDiagnosticSup... para garantir que não seja necessário um case
_em tipos switchEssencialmente, é uma versão dessugarizada das “Union Classes” desta proposta, e já funciona bem. Ainda assim, seria bom não precisar do pacote NuGet e ganhar açúcar sintático, então gostei da proposta
O compilador gera um construtor protected para operações de cópia, e esse construtor pode ser herdado. Para impedir isso, é preciso definir manualmente um construtor protected e fazê-lo lançar uma exceção em tempo de execução quando não for um case válido