You're correct. The unions we're working on right now are 'type unions'. So the type is inherent in the union distinction, and you would not be able to distinguish that case. That said, we're also looking at full blown discriminated unions (you can look at one of my proposals for that here: https://github.com/dotnet/csharplang/blob/main/meetings/work...), which would allow for that. Syntax entirely tbd, but you'd do something like:
enum struct Either<T1, T2> // or enum class
{
First(T1 value),
Second(T2 value)
}
We view these features as complimentary. Indeed, if you look at the extended enum proposal, you'll see it builds on top of unions and closed types (another proposal coming in the next version of the lang).
C# is strongly-typed, not stringly-typed. The point of the union is to list possible outcomes as defined through their respective types.
The idiomatic way to do this would be to parse, don't validate [1] each string into a relevant type with a record or record struct. If you just wanted to return two results of the same type, you'd wrap them in a named tuple or a record that represented the actual meaning.
Interestingly, dynamic languages which make use of symbols (Ruby, Elixir, Common Lisp) probably fall closer to Haskell than Python or TS. Elixir example:
String literals are structural types which are way more expressive than regular (Haskell) ADTs, which are nominal types.
In TS in particular, in combination with other features (mapped types), they are equivalent to row polymorphism + whatever Haskell/GHC features enable type families to specialize on constant literal arguments (or you can use atomic types, but that's not structural / open-world)... so pretty advanced.
This is valid TS/Python:
type ABC = "A" |"B" | "C"
type AB = "A" | "B"
const x: AB = "A";
const y: ABC = x;
The equivalent Haskell requires using several extensions.
I know. I literally gave the example of a Python Literal in the post you're replying to. TS too. :)
My overall point is that Haskell's type system is sufficiently expressive (you may not have "A" | "B" | "C", but you do have A | B | C) that there's no obvious remaining use case for string literals, unless you're thinking of typing input by way of expected literals instead of actually parsing it, which is... a choice. :P
By Haskell's type system do you mean with all the GHC extensions?
Because TypeScript has structural sub-typing, while standard Haskell (eg. `A | B | C`) has neither subtyping nor structural typing, which both are very useful features for safe "integration/glue" type of programs.
(String) literals form a fundamental part of the TS "row polymorphism" (record types) and eg. tuple union type implementation.
You can type a non-empty array that starts with zero...
My mistake. I see my oversight now. `Either String String` is not equivalent to `String | String`, but to `Left String | Right String`. The same must be done for the C# version.
Yes, you must have individual constructors for the left and right cases in order to distinguish them. In C# you would use two distinct record types for this. Haskell’s syntax is more concise though, since you define the constructors inline in the declaration of the sum type.
You can use implicit operators or a library like Vogen to accomplish the same thing in a way that they can be coerced as strings. This isn’t a real issue.
No, it's a union of a left value (that happens to be a string) and a right value (that happens to be a string). But the compiler-generated code can't tell them apart.
What you are describing is something different called a disjoint union which will maintain the identities of the left and right values when there is overlap.
The C# unions appear to behave like unions, not disjoint unions.
My mistake. I see my oversight now. `Either String String` is not equivalent to `String | String`, but to `Left String | Right String`. The same must be done for the C# version.
You are correct that this requires support for disjoint unions (aka tagged unions), which Haskell always had and C# will soon have.
Yes, you can have two different record types which both wrap a string value.
As a (bad) trivial example, you could wrap reading a file in this kind of monstrosity:
var fileResult = Helpers.ReadFile(@"c:\temp\test.txt");
Console.WriteLine("Extracted:");
Console.WriteLine(Helpers.ExtractString(fileResult));
public record FileRead(string value);
public record FileError(string value);
public union FileResult(FileError, FileRead);
public static class Helpers
{
public static FileResult ReadFile(string fileName)
{
try
{
var fileResult = System.IO.File.ReadAllText(fileName);
return new FileRead(fileResult);
}
catch (Exception ex)
{
return new FileError(ex.Message);
}
}
public static string ExtractString(FileResult result)
{
return result switch
{
FileError err => $"An Error occured: {err.value}",
FileRead content => content.value,
_ => throw new NotImplementedException()
};
}
}
Now, such an example would be an odd way to do things ( particuarly because we're not actually avoiding the try/catch inside ), but you get the point. Both FileRead(string value) and FileError(string value) wrap strings in the same way, but are different record types, and the union FileResult ties them back together in a way where you can tell which you have.
It's more useful implemented a level deeper, so that the exception is never raised and caught, because exceptions aren't particularly cheap in .NET.