HackerTrans
TopNewTrendsCommentsPastAskShowJobs

sunnydiskincali

no profile record

comments

sunnydiskincali
·há 11 meses·discuss
> You wrote a lot of words to say very little.

Substantiate this.

> weird name choice but whatever

I don't think this kind of snarky potshot is in line with the commentary guidelines. Perhaps you could benefit from a refresher?

https://news.ycombinator.com/newsguidelines.html#comments

> Thanks to subtyping, no contortion needed

I see the same degree of contortion, actually. Far more noisy, at that.

> No need to use 7 more lines to create a separate, unrelated type.

You're still creating a type, because you understand that a sum-type with a different set of cases is fundamentally a different type. Just like a class with a different set of inheritance is a different type. And while it's very cute to compress it all into a single line, it's really not compelling in the context of readability and "write once, use many". Which is the point you were making, although it was on an entirely different part of the grammar.

> Great analogy, except for the fact that someone from the Java team explicitly said they're drawing inspirations from ML.

ML didn't invent ADTs, and I think you know it's more than disingenuous to imply the quotation means that the type-system in Java which hasn't undergone any fundamental changes in the history of the language (nor could it without drastically changing the grammar of the language and breaking the close relationship to the JVM) was lifted from ML.
sunnydiskincali
·há 11 meses·discuss
> With your shape example, you cannot express in the type system "this function won't return a point".

Sure you can, that's just subtyping. If it returns a value that's not a point, the domain has changed from the shape type and you should probably indicate that.

  structure Shape = struct
    datatype shape =
        Circle of real
      | Rectangle of real * real
      | Point
  end

  structure Bound = struct
    datatype shape =
        Circle of real
      | Rectangle of real * real
  end
This is doing things quick and dirty. For this trivial example it's fine, and I think a good example of why making sum-types low friction is a good idea. It completely changes how you solve problems when they're fire and forget like this.

That's not to say it's the only way to solve this problem, though. And for heavy-duty problems, you typically write something like this using higher-kinded polymorphism:

  signature SHAPE_TYPE = sig
    datatype shape =
        Circle of real
      | Rectangle of real * real
      | Point

    val Circle : real -> shape
    val Rectangle : real * real -> shape
    val Point : shape
  end

  functor FullShape () : SHAPE_TYPE = struct
    datatype shape =
        Circle of real
      | Rectangle of real * real
      | Point

    val Circle = Circle
    val Rectangle = Rectangle
    val Point = Point
  end

  functor RemovePoint (S : SHAPE_TYPE) :> sig
    type shape
    val Circle : real -> shape
    val Rectangle : real * real -> shape
  end = struct
    type shape = S.shape
    val Circle = S.Circle
    val Rectangle = S.Rectangle
  end


  structure Shape = FullShape()
  structure Bound = RemovePoint(Shape)

This is extremely overkill for the example, but it also demonstrates a power you're not getting out of C# or Java without usage of reflection. This is closer to the system of inheritance, but it's a bit better designed. The added benefit here over reflection is that the same principle of "invalid program states are unrepresentable" applies here as well, because it's the exact same system being used. You'll also note that even though it's a fair bit closer conceptually to classes, the sum-type is still distinct.

Anyways, in both cases, this is now just:

  DoesNotReturnPoint : Shape.shape -> Bound.shape
Haskell has actual GADTs and proper higher kinded polymorphism, and a few other features where this all looks very different and much terser. Newer languages bake subtyping into the grammar.

> If they're not actual sum types but are pretty much the same, what good does the "actually" do?

Conflation of two different things here. The examples given are syntactically similar, and they're both treating the constituent part of the grammar as a tagged union. The case isn't any cleaner was the point.

However in the broader comparison between class hierarchies and sum-types? They're not similar at all. Classes can do some of the things that sum-types can do, but they're fundamentally different and encourage a completely different approach to problem-solving, conceptualization and project structure... in all but the most rudimentary examples. As I said, my 2nd example here is far closer to a class-hierarchy system than sum-types, though it's still very different. And again, underlining that because of the properties of sum-types, thanks to their specific formalization, they're capable of things class hierarchies aren't. Namely, enforcing valid program-states at a type-level. Somebody more familiar with object-oriented formalizations may be a better person to ask than me on why that is the case.

It's a pretty complicated space to talk about, because these type systems deviate on a very basic and fundamental level. Shit just doesn't translate well, and it's easy to find false friends. Like how the Japanese word for "name" sounds like the English word, despite not being a loan word.
sunnydiskincali
·há 11 meses·discuss
> This has the benefit of giving you the ability to refer to a case as its own type.

A case of a sum-type is an expression (of the variety so-called a type constructor), of course it has a type.

  datatype shape =
      Circle of real
    | Rectangle of real * real
    | Point

   Circle : real -> shape
   Rectangle : real * real -> shape
   Point : () -> shape
A case itself isn't a type, though it has a type. Thanks to pattern matching, you're already unwrapping the parameter to the type-constructor when handling the case of a sum-type. It's all about declaration locality. (real * real) doesn't depend on the existence of shape.

The moment you start ripping cases as distinct types out of the sum-type, you create the ability to side-step exhaustiveness and sum-types become useless in making invalid program states unrepresentable. They're also no longer sum-types. If you have a sum-type of nominally distinct types, the sum-type is contingent on the existence of those types. In a class hierarchy, this relationship is bizarrely reversed and there are knock-on effects to that.

> You declare the sum type once, and use it many times.

And you typically write many sum-types. They're disposable. And more to the point, you also have to read the code you write. The cost of verbosity here is underestimated.

> Slightly more verbose sum type declaration is worth it when it makes using the cases cleaner.

C#/Java don't actually have sum-types. It's an incompatible formalism with their type systems.

Anyways, let's look at these examples:

C#:

  public abstract record Shape;
  public sealed record Circle(double Radius) : Shape;
  public sealed record Rectangle(double Width, double Height) : Shape;
  public sealed record Point() : Shape;
  
  double Area(Shape shape) => shape switch
  {
      Circle c => Math.PI * c.Radius * c.Radius,
      Rectangle r => r.Width * r.Height,
      Point => 0.0,
      _ => throw new ArgumentException("Unknown shape", nameof(shape))
  };
ML:

  datatype shape =
      Circle of real
    | Rectangle of real * real
    | Point
  
  val result =
    case shape of
        Circle r => Math.pi * r * r
      | Rectangle (w, h) => w * h
      | Point => 0.0
They're pretty much the same outside of C#'s OOP quirkiness getting in it's own way.