Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

One big difference between how Dart (and other OO languages that take a similar approach) and what Elm and other functional languages do is that in Dart, each of the cases is also its own fully usable type.

In Dart, if you do:

    sealed class Message {}

    class IncrementBy extends Message {
      final int amount;
      IncrementBy(this.amount);
    }

    class DecrementBy extends Message {
      final int amount;
      DecrementBy(this.amount);
    }

    class Set extends Message {
      final int amount;
      Set(this.amount);
    }
You can then write functions that accept specific cases, like:

    onlyOnIncrement(IncrementBy increment) {
      ...
    }
In Elm and friends, IncrementBy is just a type constructor, not a type. Further, we can use the class hierarchy to reuse shared state and behavior in a way that sum types don't let you easily do. In your example, each case has an int and it so happens that they reasonably represent roughly the same thing, so you could do:

    sealed class Message {
      final int amount;
      Message(this.amount);    
    }

    class IncrementBy extends Message {
      IncrementBy(super.amount);
    }

    class DecrementBy extends Message {
      DecrementBy(super.amount);
    }

    class Set extends Message {
      Set(super.amount);
    }
And now you can write code that works with the amount of any Message without having to pattern match on all of the cases to extract it:

    showAmount(Message message) {
      print(message.amount);
    }
So, yes, it's more verbose than a sum type (and we do have ideas to trim it down some), but you also get a lot more flexibility in return.


In an FP code base, it’s common for ADTs to be the backbone of all data structures from Maybe to Either, to anything you use to model data types. That being the case changing 4 lines into 13 scaled into a entire code base is a massive amount of sludge to wade thru & one of the best way to up the code quality is to increase readability for maintainers. More LoC is more LoC to maintain even if it seems like it’s just a few more lines.


> changing 4 lines into 13 scaled into a entire code base is a massive amount of sludge to wade thru

It's only more verbose for the code that is defining new types. Code that is simply defining behavior (either in functions or methods) is unaffected and my experience is that that's the majority of code.


But reading those definitions at the top of the file is usually one of the first places I’m going to go to understand what’s going on. The 4-line option to me is much easier to grok, not only because it’s more terse but the way the pipes are stacked to indicate visually that they are related to the same underlying structure. The `extends Message` bit is a level of indirection that requires the reader to juggle a lot more in their head (as is immutable `final` not being the default). The `class` keyword also carries a lot of baggage that it’s not clear what to expect (will this have methods or not?, will we be seeing `this`?, etc.).


I think you're evaluating this as a notation for defining sum types. But that's not what it is. It's a notation for defining a class hierarchy, with all of the additional affordances that gives you.

In practice, most real-world Dart code that uses sealed types also defines methods in those types, getters, and all sorts of useful stuff. Once you factor that in, the data definitions themselves are a relatively small part.

(Of course, you could argue that defining class hierarchies is already intrinscally bad. But Dart is an object-oriented language targeting people that like organizing their code using classes.)


This is definitely an FP vs OO thing. If you wanted to refer to the value in an ADT you'd introduce a new type to refer to it, which in Elm would be:

    type Message 
        = IncrementBy Amount
        | DecrementBy Amount
        | Set Amount

    type Amount = Amount Int
Obviously this is a contrived example - you wouldn't bother if you were dealing with an Int but once the message gets more complicated it can make sense.


That's a different thing.

Given the above ADT, how would you write a function that prints the amount of a Message, regardless of which case it is?


You'd use a switch over the ADT and extract the values as appropriate. This is method dispatch vs function dispatch. In practice you can do the same things with either, but they reflect the focus on behaviour (OO) vs data (FP).


That's the point of my last example. By building this on subtyping, you can hoist that shared property out of the cases and up to the supertype. That lets you define state and behavior that is case independent without having to write a bunch of brain-dead matches that go over every case to extract conceptually the same field from each one.

Of course, you can also reorganize you datatype so that the shared state is hoisted out into a record that also contains a sum typed field for the non-shared stuff. But that reorganization is a breaking change to any consumers of the type.

Modeling this on top of classes and subtyping lets you make those changes without touching the user-visible API.


I appreciate you taking the time to answer, but honestly the boilerplate is just a killer. I don't care about the additional flexibility when the code is so much harder to make sense of, and this is just a toy example.


fp-ts has this same unfortunate issue (https://gcanti.github.io/fp-ts/guides/purescript.html#data) where a PureScript one-liner becomes 11 lines of boilerplate TypeScript. Or (https://gcanti.github.io/fp-ts/guides/purescript.html#patter...) 3 lines of pattern matching becomes 10 where the matching is done on strings (ad-hoc _tag property).




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: