Hacker News new | ask | show | jobs
by moomin 29 days ago
AFAICT, this means you won’t be able to define Either<string, string>, which is definitely a thing you sometimes want to do.
6 comments

It seems like if you wrap both in a record then it should be possible:

    public record Left<T>(T Value);
    public record Right<T>(T Value);
    public union Either<L, R>(Left<L>, Right<R>);
Hi there. One of the C# lang designers here.

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).
Thank you! Even what’s there is going to be really useful to me but I’ll continue to look forward to a full-fat implementation.
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.

[1] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...

I guess C# is more strongly-typed than Haskell then... /s
String literal typing appears to be a common feature of type systems bolted onto dynamic languages:

    # Python
    MyStringBool = Literal("Yes") | Literal("No")

    // TypeScript
    type MyStringBool = "Yes" | "No"
I assume it exists to compensate for the previous lack of typing, and consequent likelihood of ersatz typing via strings.

It would seem pretty unnecessary in Haskell, where you can just define whatever types you want without involving strings at all:

    data MyBool = Yes | No
Of course you'd need a trivial parser, though this is probably a good idea for any string type:

    parseMyBool :: String -> MyBool 
    parseMyBool "Yes" = Yes
    parseMyBool "No" = No
    parseMyBool _ = error "..."
Interestingly, dynamic languages which make use of symbols (Ruby, Elixir, Common Lisp) probably fall closer to Haskell than Python or TS. Elixir example:

    @type my_bool() :: :yes | :no

    @spec parse_my_bool(String.t()) :: my_bool()
    def parse_my_bool("Yes"), do: :yes
    def parse_my_bool("No"), do: :no
    def parse_my_bool(_), do: throw("...")
Where :yes and :no are memory-efficient symbols, not strings.
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...

    type Arr = [0, ...number[]];
    const a: Arr = [0, 1, 2, 3, 4]
Now try in Haskell.
Thank you for the lecture but I didn’t mean that. I meant `Either String String` is possible in Haskell and not in C# because… C# is strongly-typed.
You're welcome! Knowing is half the battle.

That Haskell snippet is just syntax sugar for Left(string) | Right(string), which is trivial in any language with unions.

Not clear why it would be an improvement over just naming the alternatives something meaningful, but if you're wedded to Left and Right, go for it.

You cant have a `type Foo = String | Strimg` in Haskell either.
But you can have an `Either String String` which is what GP was talking about.
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.
Well it is a type union. The union of string and string is just string.
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.

but can you define T1 and T2 of string, then use Either<T1, T2>?
Could you be clearer about what you mean, since string is a sealed type in C#, so what exactly do you mean T1 and T2 of string?
A record wrapping a string, indicating what the string represents, so you can't mix it up with a different thing also represented by a string.
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.

    type T1 string
    type T2 string
    type Meh Either<T1, T2>