4 ms·
I've been working with Elixir and Elm the last few years but recently have been working on a mobile app in Flutter. While the recent changes are a good step in
by ollysb 3y ago
I've been working with Elixir and Elm the last few years but recently have been working on a mobile app in Flutter. While the recent changes are a good step in the right direction (being able to express ADTs and records) it does feel like an iteration away from being ergonomic.
Regarding sealed types, I'm not really sure why you'd choose these over type unions, in practice they're often used to describe things like events for reducers and using sealed is incredibly verbose compared to the equivalent in for instance typescript.
type Message =
| { type: 'IncrementBy', value: number }
| { type: 'DecrementBy', value: number }
| { type: 'Set', value: number };
vs
sealed class Message {}
class IncrementBy extends Message {
final int value;
IncrementBy(this.value);
}
class DecrementBy extends Message {
final int value;
DecrementBy(this.value);
}
class Set extends Message {
final int value;
Set(this.value);
}
In a real app this noise really adds up making it difficult to get an overview of the types.
Record types are nearly there, but currently there isn't a way to update the records which really hobbles them in day to day use (it looks there will be record spreading coming though so this is hopefully temporary).
- mraleph 3y ago[disclaimer: I work on Dart, though not on the language team] With primary constructors this will become something along the lines of: sealed class Message(); class IncrementBy(final int value) extends Message; class DecrementBy(final int value) extends Message; class Set(final int value) extends Message; Which is considerably less repetitive, though `extends Message` is still there. I am fairly optimistic that the next step would be to eliminate that[1], though I think we would need to gather a bit more feedback from users as people are getting more and more reps in with Dart 3.0 features. I personally would prefer something along the lines of: sealed class Message() { case IncrementBy(final int value); case DecrementBy(final int value); case Set(final int value); } Current syntax is not all that bad if you are going to do OO and add various helper methods on `Message` and its subclasses, but if you just want to define your data and no behavior / helpers - then it is exceedingly verbose. [1]: https://github.com/dart-lang/language/issues/3021 https://github.com/dart-lang/language/issues/3021
- ollysb 3y agoThat syntax does look a lot nicer and makes sense if you're going to do OO anyway. I do wonder if it's trying to shoe horn OO into functional clothing but seems practical nonetheless. Primary constructors would definitely be a welcome change.
- danieldisu 3y agoIt is way easier for me to read than what you proposed in Typescript, more characters to write? sure but how many times do I have to write a piece of code and how many times do I have to read it? I guess ultimately it depends on which language you are more familiar with...
- ollysb 3y agoI'm more than happy with either of those variations. My favourite though would be Elm's syntax ;) type Message = IncrementBy Int | DecrementBy Int | Set Int
- munificent 3y agoOne 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.
- layer8 3y agoThe type-union syntax gets messy once the subclasses contain nontrivial amounts of code (like method definitions). The class syntax keeps the lexical context more local.