-
Notifications
You must be signed in to change notification settings - Fork 205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Algebraic Data Types (ADTs, Sealed Classes, Enum with associated values) #349
Comments
I also was initially put off by the lack of ADTs in Dart. But while there isn't language-level support, ADTs can be decently represented in Dart. For example, here's how I would translate your Kotlin example into Dart: abstract class LoginResponse {
// Not necessary when case classes are public,
// but I usually put factories and constants on the base class.
factory LoginResponse.success(authToken) = LoginSuccess;
static const invalidCredentials = const InvalidLoginCredentials();
static const noNetwork = const NoNetworkForLogin();
factory LoginResponse.unexpectedException(Exception exception) = UnexpectedLoginException;
}
class LoginSuccess implements LoginResponse {
final authToken;
const LoginSuccess(this.authToken);
}
class InvalidLoginCredentials implements LoginResponse {
const InvalidLoginCredentials();
}
class NoNetworkForLogin implements LoginResponse {
const NoNetworkForLogin();
}
class UnexpectedLoginException implements LoginResponse {
final Exception exception;
const UnexpectedLoginException(this.exception);
} And for the void on(LoginResponse loginResponse) {
if (loginResponse is LoginSuccess) {
// Dart has smart casting too, so you can use authToken w/o a cast
loginResponse.authToken;
} else if (loginResponse is InvalidLoginCredentials) {
} else if (loginResponse is NoNetworkForLogin) {
} else if (loginResponse is UnexpectedLoginException) {
// Dart has smart casting too, so you can use exception w/o a cast
loginResponse.exception;
} else {
// Dart doesn't have sealed classes
throw new ArgumentError.value(loginResponse, 'loginResponse', 'Unexpected subtype of LoginResponse');
}
} Does this satisfy your request for ADTs? I've been using this pattern myself for a while, and at this point I don't think that Dart needs language-level support for ADTs per se (though I wouldn't complain if it was added). Because Dart does not have sealed classes or a |
I'm looking for language level support, unfortunately. A modern programming language should make it easy to represent ADTs. At least Dart has smart casting |
Can you be specific about what you're looking for in language-level support that is not currently possible in Dart? Is it the more concise syntax for declaring ADTs? The static checking of |
Hey @avilladsen -- The features I would like (whether it's sealed classes or union types):
Sure, defining a class with some subclasses will work in combination with You can see an attempt that some of us in the community have worked on to achieve this: https://github.com/fluttercommunity/dart_sealed_unions You could also use Therefore, we can paper over these issues with libraries, but they're far more cumbersome and much harder to read than workin with sealed + data classes in Kotlin. In fact, I even prefer the Scala version with pattern matching and destructuring: https://docs.scala-lang.org/tour/pattern-matching.html |
This is the main source of boilerplate code for me. And this is a lot of boilerplate. Not sure if ADTs are supposed to address this, but It’s probably number 1 feature in my wishlist. |
Very similar to http://github.com/dart-lang/sdk/issues/31253. |
I've been developing in Flutter for two weeks and I've been really blown away by the incredibly well thought of framework and surrounding tooling! But I really miss the beautiful sum types of the ML-family (OCAML, F#, ReasonML, Haskell etc). abstract class LoginResponse {
factory LoginResponse.success(authToken) = LoginSuccess;
static const invalidCredentials = const InvalidLoginCredentials();
static const noNetwork = const NoNetworkForLogin();
factory LoginResponse.unexpectedException(Exception exception) = UnexpectedLoginException;
}
class LoginSuccess implements LoginResponse {
final authToken;
const LoginSuccess(this.authToken);
}
class InvalidLoginCredentials implements LoginResponse {
const InvalidLoginCredentials();
}
class NoNetworkForLogin implements LoginResponse {
const NoNetworkForLogin();
}
class UnexpectedLoginException implements LoginResponse {
final Exception exception;
const UnexpectedLoginException(this.exception);
} to how you would write the same in say F#: type LoginResponse =
| Success of AuthToken
| InvalidCredentials
| NoNetwork
| UnexpectedException of Exception (I use the same shorter names as in @ZakTaccardi's original Kotlin example) You get much better data/domain modelling inside your code when you have tools like this available in the language (@brianegan makes the same argument). It improves readability and quality of the code, not the least because it enables preventing many more illegal states at compile time. And since it is so easy to read and write, you do it right instead of often cheating like you would do in say Dart because you would have to write so much more code to do it "correctly". |
Apologies if this is a bit of a +1 response, but coming from Rust I really miss ADTs (called EDIT To full realise their power you also need some de-structuring support (sometimes called enum Error { // ADT
IoError { // 1st variant with named fields
errno: u32 // unsigned 32-bit int
}, // 2nd variant with named fields
ParseError {
position: usize // unsigned pointer-width int
msg: String
},
UnknownError, // 3rd variant with no fields
} then the destructuring would look like match e { // e is an error
Error::IoError { errno } => {
// I have access to the fields of the variant here
// handle the error...
}
Error::ParseError { position, msg } => {
// again I have access to fields
}
Error::UnknownError => {
// nothing to bind here
}
} |
@lrhn There may be similarities in the way in which it will be implemented in Dart, but other than that this is unrelated with data classes. |
ADTs are also known as "tagged unions". In typescript, you may represent the above example as: type Success = {
tag: "Success"
authToken: string;
};
type InvalidCredentials = {
tag: "InvalidCredentials"
};
type NoNetwork = {
tag: "NoNetwork"
};
type UnexpectedException = {
tag: "UnexpectedException"
error: Error
};
type LoginResponse =
Success
| InvalidCredentials
| NoNetwork
| UnexpectedException The
There is also the concept of an "untagged union", which could simply refer to having one object with nullable fields represent multiple scenarios: type Option<T> = {
isPresent: bool;
value?: T;
}; Untagged unions can also refer to syntax sugar for dealing with nullable types: _optionalVar?.foo(); Most languages with nullable types can support untagged unions, but Dart clearly has a special understanding since it supports the safe-access operators. In addition, dart (and similar style languages) provide smart casting (and "instance of" checking) on exceptions with try/catch statements. Pure OO practices would discourage the use of ADTs, since it involves "casting" an abstract class to a concrete type after doing an "instance of" check. The OO pattern that would replace the need for ADTs is the visitor pattern, but this adds so much overhead just to stay in the OO space that the benefit is often not worth the cost. I'm not super familiar with how the dart compiler works, but if we are able to identify all the implementation of an abstract class at compile time, it might be possible to remove the need for the |
@tchupp Is visitor pattern really a replacement? Doesn't it just move the "instance of" checks to the visitor (I may be mistaken, not super familiar with it)? Also, I wonder whether ADTs are just convenience for inheritance in OOP, or there's a deeper conceptual difference that makes the "instance of" not violate OOP practices in this case. It feels like may be a fundamental difference between if you're expecting Another take could be that the the "ADT" is just convenience to do do instance of checks in a type safe and concise way. Making the |
@i-schuetz I would say you are correct in that there is a deep conceptual difference. In a class (or struct or record), you have the "multiplication" construct of ADTs. What is lacking in Dart at the moment is the "sum" construct of ADTs. I think the sum is better viewed as something different than a special case of inheritance in OOP, although it can to some extent be "simulated" that way. Here is a bit old, but still very good article about ADTs: When you get used to sum types, it feels very limited not having them. |
Hey @kevmoo any plans on this? |
I'm really really missing this coming from Swift/iOS to Flutter development. When dealing with widget/component state, I find it that almost always the state is better described using an ADT rather than several nullable fields. The problem with nullable fields is that it often allows for combinations of values that are simply invalid. Here is an example from a commercial app I'm currently porting from Swift to Flutter: class _State {}
class _ChooseProviderState extends _State {}
class _LoadingState extends _State {}
class _ChooseAccountsState extends _State {
final List<String> accounts;
final Set<String> selected;
_ChooseAccountsState(this.accounts) : selected = Set();
}
class _ImportingState extends _State {
final List<String> imported;
final List<String> failed;
final List<String> processing;
final List<String> queue;
_ImportingState(this.imported, this.failed, this.processing, this.queue);
}
// ...
@override
Widget build(BuildContext context) {
if (_state is _ChooseProviderState) {
return _buildChooseProvider(context, _state);
}
if (_state is _LoadingState) {
return _buildLoading(context, _state);
}
if (_state is _ChooseAccountsState) {
return _buildChooseAccounts(context, _state);
}
if (_state is _ImportingState) {
return _buildImporting(context, _state);
}
throw Exception('Invalid state');
} Apart from being very verbose and cumbersome to type out, there is also no compiler checks that I'm 1) covering all possible states and that 2) there isn't every any other state. This forces me to add a runtime Now let's compare this to the Swift equivalent: enum State {
case chooseProvider,
case loading,
case chooseAccounts(accounts: [String], selected: Set<String>),
case importing(imported: [String], failed: [String], processing: [String], queue: [String]),
}
// ...
build(context: BuildContext) -> Widget {
switch state {
case .chooseProvider:
return buildChooseProvider(context)
case .loading:
return buildLoading(context)
case .chooseAccounts(let accounts, let selected):
return buildChooseAccounts(context, accounts, selected)
case .importing(let imported, let failed, let processing, let queue):
return buildImporting(context, imported, failed, processing, queue)
}
} Apart from being much much easier to work with, and quickly glance over, it gives me additional compiler guarantees. Here I know that I'm never missing any of the enum cases since the compiler will complain. I also don't have to add a runtime I would absolutely love to see this in Dart 😍 is there anything I can do to move this forward? |
CC @munificent |
@LinusU great example! Dart has the idea of a refinement/type test/smart cast. I wonder if this could work similar to how Kotlin does this. To re-use @LinusU's example: when (_state) {
is _ChooseProviderState -> ...
is _LoadingState -> ...
is _ChooseAccountsState -> ...
is _ImportingState -> ...
} Kotlin was originally designed on top of the JVM. if (_state instanceof _ChooseProviderState) {
_ChooseProviderState inner = (_ChooseProviderState) _state;
...
}
...etc. Dart already has the idea of smart casting, so I have a feeling that implementing this should not be the most difficult. Agreeing on a syntax will be the fun part 😅 (To steal what @LinusU said) Is there anything I could do to make this possible? |
@LinusU @tchupp I agree that native support for ADTs would be nice and that it's much easier to work with ADTs in Rust/Kotlin/Swift/F# etc! But at least the problem of not having compiler guarantees can be mostly worked around in Dart. To continue on the example @LinusU posted above, you can add a method to the common type, abstract class _State {
T use<T>(
T Function(_ChooseProviderState) useChooseProviderState,
T Function(_LoadingState) useLoadingState,
T Function(_ChooseAccountsState) useChooseAccountState,
T Function(_ImportingState) useImportingState) {
if (this is _ChooseProviderState) {
return useChooseProviderState(this);
}
if (this is _LoadingState) {
return useLoadingState(this);
}
if (this is _ChooseAccountsState) {
return useChooseAccountState(this);
}
if (this is _ImportingState) {
return useImportingState(this);
}
throw Exception('Invalid state');
}
} Now, your @override
Widget build(BuildContext context) {
return _state.use(
(chooseProvider) => _buildChooseProvider(context, chooseProvider),
(loading) => _buildLoading(context, loading),
(chooseAccounts) => _buildChooseAccounts(context, chooseAccounts),
(importing) => _buildImporting(context, importing));
} |
Looks great! I'd suggest using |
Just adding OCaml's variant types to the conversation here. A sum type in OCaml looks like: type FeedWidgetState =
| Loading
| Failed of exn * stack_trace
| Succeeded of tweet list
let build state context =
match state with
| Loading -> Flutter.circularProgressIndicator
| Failed e _ ->
let errMsg = Printexc.to_string e in
Flutter.center (Flutter.text errMsg)
| Succeeded accounts ->
let render_account account =
Flutter.listTile ~title: (Flutter.text account.handle)
in
Flutter.listView ~children: (List.map render_account accounts) Obviously, the syntax is pretty different from Dart, but theoretically a translation could still look pretty Dart-y: sum class FeedWidgetState {
Loading,
Failed(Object error),
Succeeded(List<Account> accounts)
}
Widget build(BuildContext context) {
// Or: match (await someComputation(2, "3")) as state
return match state {
Loading => CircularProgressIndicator(),
Failed => Center(child: Text(state.error.toString())),
Succeeded {
return ListView(
children: state.accounts.map(renderAccount).toList(),
);
}
}
} EDIT: It would also be cool to potentially even allow method/field definitions in sum class, though that might be a little too much to ask for... sum class FeedWidgetState {
Loading,
Failed(Object error),
Succeeded(List<Account> accounts)
String common() => "hello!"
void doIt() {
// Do it...
}
@override
String toString() {
return match this {
// ...
}
}
}
// OR...
class FeedWidgetState {
sum Foo(), Bar(String x), Baz(List<String> quux) |
|
How does this example look? https://gist.github.com/nodinosaur/adf4da0b5f6cddbdcac51c271db56465 |
ADTs as a whole can mostly be represented in dart currently using inheritance. The only thing that's limiting those approaches' success is their verbosity. However, I think this would largely be alleviated by the addition of data classes and the addition of a Take the Kotlin code in the OP, for example. If Dart had data classes and a
If Dart gets support for pattern matching in switch blocks, this might not even need a dedicated The only thing this doesn't cover is the exhaustive type matching, but IMO I'm not sure that's a bad thing. Having exhaustive type matching is nice for type safety or testing, but it also cuts into the ability of a type being extensible. |
Yes, I agree that we are missing
What I would like to see if there is an official Dart example of how we ought to be implementing |
|
I did something similar to what @avilladsen and @renatoathaydes mentioned above and was satisfied with the results. To avoid the boilerplate, I built a code generator with my colleague which generates these classes by annotating Enums. @superEnum
enum _MoviesResponse {
@Data(fields: [DataField('movies', Movies)])
Success,
@object
Unauthorized,
@object
NoNetwork,
@Data(fields: [DataField('exception', Exception)])
UnexpectedException
} where:-
Then it can be used easily with the generated moviesResponse.when(
success: (data) => print('Total Movies: ${data.movies.totalPages}'),
unauthorized: (_) => print('Invalid ApiKey'),
noNetwork: (_) => print(
'No Internet, Please check your internet connection',
),
unexpectedException: (error) => print(error.exception),
); |
Very cool @xsahil03x ... the existing solutions to this are probably not as nice. https://github.com/google/built_value.dart/blob/master/example/lib/enums.dart https://github.com/werediver/sum_types.dart Would be nice if one alternative became the standard though. |
Another alternative is https://github.com/factisresearch/sum_data_types. It offers both sum- and product-types. Unfortulately the Syntax for the constructors is a bit cumbersome. |
Everyone wants to solve this problem :D here's a few more existing libraries from #546 (comment)
|
Does Algebraic DT include monads like Either? |
@bubnenkoff check this pub https://pub.dev/packages/result_type it might help. |
I tend to use https://pub.dev/packages/dartz for functional programming types. Functional programming in Dart
|
thanks! But is there any plan to add something similar to Dart to get ADT
out of the box?
чт, 4 февр. 2021 г. в 12:50, George.M <notifications@github.com>:
… I tend to use https://pub.dev/packages/dartz for functional programming
types.
Functional programming in Dart
Type class hierarchy in the spirit of cats, scalaz and the standard
Haskell libraries
• Immutable, persistent collections, including IVector, IList, IMap,
IHashMap, ISet and AVLTree
• Option, Either, State, Tuple, Free, Lens and other tools for programming
in a functional style
• Evaluation, a Reader+Writer+State+Either+Future swiss army knife monad
• Type class instances (Monoids, Traversable Functors, Monads and so on)
for included types, as well as for several standard Dart types
• Conveyor, an implementation of pure functional streaming
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#349 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABRWNFXZ5PVZQEAIFZLHB5DS5JUUXANCNFSM4HMFEFPQ>
.
|
All of these external packages feel like bandaid solutions to something that would likely be able to optimize heavily if it were in the language as a standard feature. |
Sum types are not yet in the language funnel :( |
@adrianboyko Check again under "Patterns and other features". |
Another vote for this feature |
How long can I wait? To write secure code, you have to suffer. Sealed classes have been implemented in all modern languages for a long time. |
I'm not sure if commenting here will be helpful all, but I'm a big +1 to this feature request. When it comes to modeling a problem domain in a modern programming language, I'd say ADTs are very nearly as important as null safety. With null-safety implemented, I can't imagine any language feature that ought to be a higher priority than ADTs. It's similar to null-safety in the sense that you can't properly model your problem domain without it. Flutter being such an important use of Dart implicitly puts Dart in direct competition with Swift and Kotlin. Both of those languages not only support ADTs, but have a strong developer culture of using them heavily. That means that while there is a lot about the developer experience that improves when one switches from native to Flutter, the lack of ADTs in Dart is a glaring regression that is very noticeable to iOS and Android devs. |
@hepin1989
|
This is really awesome for Kotlin developers. You can say the current built-in types are enough for us but ENOUGH is far less than what we need. As a newer language, I believe we can do better in grammar sugar. I miss Kotlin very single time I write Flutter! I want data classes and removing the |
These are tracked on separate issues.
The next stable release of Dart will allow you to define fields, methods, and other members on enum types. It's out in the beta channel now. |
I think the overall experience writing dart is good but I do miss some things and by reading here it looks like I'm not alone. It would be great if we could have the equivalent of:
Packages like The good news is that it looks like there are issues for most of these and are in various levels of discussions already. It's great we are getting fields in enums in the next release! |
That was released in Dart 2.17 a few days ago. See the announcement: https://medium.com/dartlang/dart-2-17-b216bfc80c5d |
Closing this because records and patterns are now enabled on the main branch of the Dart SDK! 🎉 |
ADTs are a very important part of any modern programming language.
Imagine you have a network call that submits login credentials. This call can:
In Kotlin, the return value could be represented as the following
sealed class
Kotlin's
when
statement unwraps this beautifully, smart castingLoginResponse
.Sadly, languages like Java and Dart are unable to elegantly express this concept (ADTs). For a best case example, checkout out Retrofit's
Response
object. It is a single pojo that contains both:.body()
.errorBody()
Both these fields are nullable, but one must be non-null and the other must be null. To know which field can be accessed, you have to check the
.isSuccessful()
flag.Now imagine the verbosity if this best case scenario has to scale to 4 or more possible results. A whole new enum class would have to be added to work around it...it's not pretty.
Please add support for ADTs to Dart.
note: As an Android developer, I find that Flutter is an interesting proposition that is currently held back by the language.
The text was updated successfully, but these errors were encountered: