Skip to content
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

Closed
ZakTaccardi opened this issue May 8, 2018 · 62 comments
Closed
Labels
request Requests to resolve a particular developer problem

Comments

@ZakTaccardi
Copy link

ZakTaccardi commented May 8, 2018

ADTs are a very important part of any modern programming language.

Imagine you have a network call that submits login credentials. This call can:

  • log the user in
  • fail because the user entered incorrect wrong credentials
  • fail because the user doesn't have network connectivity
  • fail because of an unknown defect in your code or server broke API contract and the call failed

In Kotlin, the return value could be represented as the following sealed class

sealed class LoginResponse {
    data class Success(val authToken) : LoginResponse()
    object InvalidCredentials : LoginResponse()
    object NoNetwork : LoginResponse()
    data class UnexpectedException(val exception: Exception) : LoginResponse()
}

Kotlin's when statement unwraps this beautifully, smart casting LoginResponse.

fun on(loginResponse: LoginResponse) {
    return when (loginResponse) {
        is LoginResponse.Success -> {
            // direct access to auth token available w/o need to cast
            loginResponse.authToken
        }
        is LoginResponse.InvalidCredentials -> {

        }
        is LoginResponse.NoNetwork -> {

        }
        is LoginResponse.Unexpected -> {
            // direct access to exception available w/o need to cast
            loginResponse.exception
        }
    }
}

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:

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.

@avilladsen
Copy link

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 when statement:

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 when-like statement, you can have runtime type errors, which is the one thing I'm really not happy with. I'd be in favor of adding those to Dart. However, as has been pointed out elsewhere, those are really a separate, broader issue than ADTs.

@ZakTaccardi
Copy link
Author

ZakTaccardi commented May 14, 2018

ADTs can be decently represented in Dart

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

@avilladsen
Copy link

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 when-style statements?

@brianegan
Copy link

brianegan commented Jun 28, 2018

Hey @avilladsen --

The features I would like (whether it's sealed classes or union types):

  1. Exhaustive Checking with automatic casting inside when + is expressions. This provides static analysis to ensure you're handling each and every case. This eliminates the need for some types of tests.
  2. Constrained types. Not open for extension outside of the declaration. This makes reading the code a breeze, since you can see "Ok, this type encapsulates exactly these responsibilities".
  3. Concise syntax without ==, toString, and hashcode and copy boilerplate, although I think "data classes" are a separate issue. For example, none of your classes include these methods, and they need to be implemented for testing to work more easily.
  4. Convenient Domain Modeling: https://skillsmatter.com/skillscasts/4971-domain-driven-design-with-scott-wlaschin

Sure, defining a class with some subclasses will work in combination with is checks, but it's not as safe (lack of exhaustive checking, need to write more tests) and more verbose. I think you can see a great example of modeling state in this fashion here: https://github.com/freeletics/RxRedux#usage

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 built_value to generate the ==, toString etc, but that is also difficult to read and requires a build step.

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

@pulyaevskiy
Copy link

Concise syntax without ==, toString, and hashcode and copy boilerplate, although I think "data classes" are a separate issue.

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.

@lrhn
Copy link
Member

lrhn commented Dec 6, 2018

@jarlestabell
Copy link

jarlestabell commented Mar 9, 2019

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!
I think Flutter is the biggest progress when it comes to developer productivity for UI programming since Visual Basic 1.0 / Delphi 1.0 et al in the mid nineties.

But I really miss the beautiful sum types of the ML-family (OCAML, F#, ReasonML, Haskell etc).
Please compare the Dart example from @avilladsen :

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".
Dart is a nice language, and Flutter is a killer. For me, the biggest lack in Flutter is these data modelling capabilities (and pattern matching) of languages of the ML family. (Scala and Kotlin have similar capabilities, but more verbose syntax)
For non-trivial UI I quite often find finite state automata very useful and sum types (and the corresponding pattern matching) is perfect for this.

@richard-uk1
Copy link

richard-uk1 commented Mar 9, 2019

Apologies if this is a bit of a +1 response, but coming from Rust I really miss ADTs (called enums in rust). Rust's optional Option is vastly superior to everything being nullable - you don't need to annotate @required, the type tells you if it is or not! Rust's Result, which is like Either in Haskell is also a great way to represent fallible operations - quite often the error type is also an ADT with extra data depending on the type of the error. They are also really easy to implement and memory efficient - they are just typed unions from C (although I don't know dart's memory model, maybe everything is behind a pointer anyway).

EDIT To full realise their power you also need some de-structuring support (sometimes called switch/match/case). In rust say you have an ADT like

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
    }
}

@ivnsch
Copy link

ivnsch commented Mar 9, 2019

@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.

@tchupp
Copy link

tchupp commented Apr 15, 2019

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 tag property is shared by all the different structural types, which allows us to use a switch statement to identify which type we have.

Option<T> from Rust, which is referenced above, is another example of a tagged union.

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.
Also these languages usually provide exception type checking, which violates that pattern anyways.

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 else statement at the end a series of is checks.

@ivnsch
Copy link

ivnsch commented Apr 16, 2019

@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 Animal, which can be a Cat or Dog or Result, which can be Ok or Error. A manifestation of this may be that in most languages you can't share data between the types in ADTs, while using inheritance you can. Not sure about this take.

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 ifs exhaustive, in combination with smart casting, would be no different than just having "ADT"s, except with an uglier syntax.

@jarlestabell
Copy link

@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:
http://static.v25media.com/the-algebra-of-data-and-the-calculus-of-mutation.html

When you get used to sum types, it feels very limited not having them.
For me, TypeScript's tagged unions are quite lacking because pattern matching is missing, which gives you sort of only the first half of the power/convenience of sum types.

@Aetet
Copy link

Aetet commented May 10, 2019

Hey @kevmoo any plans on this?

@LinusU
Copy link

LinusU commented May 10, 2019

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 throw in my code.

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 throw, since the compiler can guarantee that execution will never continue after the switch statement.

I would absolutely love to see this in Dart 😍 is there anything I can do to move this forward?

@kevmoo kevmoo transferred this issue from dart-lang/sdk May 10, 2019
@kevmoo
Copy link
Member

kevmoo commented May 10, 2019

CC @munificent

@tchupp
Copy link

tchupp commented May 10, 2019

@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.
Java doesn't have the idea of smart casting, so this was a significant improvement:

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?

@renatoathaydes
Copy link

renatoathaydes commented May 31, 2019

@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, _State, which lets you use it in a type-safe manner, ensuring you don't forget any case, by moving the if/else chain into a single place (a technique I use in Java normally, but works in Dart too - as long as you keep constructors for the sub-types in the hierarchy private, so they can only be instantiated via the common type via static methods, or within the same file):

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 build method looks much nicer:

  @override
  Widget build(BuildContext context) {
    return _state.use(
        (chooseProvider) => _buildChooseProvider(context, chooseProvider),
        (loading) => _buildLoading(context, loading),
        (chooseAccounts) => _buildChooseAccounts(context, chooseAccounts),
        (importing) => _buildImporting(context, importing));
  }

Dart Pad Full Example

@ds84182
Copy link

ds84182 commented May 31, 2019

Looks great! I'd suggest using @required named parameters though, so you don't have to depend on a particular argument order. It also makes it possible to remove subtypes in the future without getting... lots of unreadable errors from the analyzer.

@thosakwe
Copy link

thosakwe commented May 31, 2019

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)

@nodinosaur
Copy link

nodinosaur commented Jun 21, 2019

@sealed annotation is now available as of meta ^1.1.7
merged in on #11 => meta.dart#L202

@nodinosaur
Copy link

nodinosaur commented Jun 22, 2019

How does this example look?
(Comments, correct alternative implementations welcomed)

https://gist.github.com/nodinosaur/adf4da0b5f6cddbdcac51c271db56465

@andrewackerman
Copy link

andrewackerman commented Jun 22, 2019

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 when construct which can be an extension of a switch block.

Take the Kotlin code in the OP, for example. If Dart had data classes and a when construct, the Dart equivalent of the Kotlin code might look something like this:

abstract data class LoginResponse;
data class Success(String authToken) extends LoginResponse;
data class InvalidCredentials extends LoginResponse;
data class NoNetwork extends LoginResponse;
data class Unexpected(Error error) extends LoginResponse;

// ...

on(LoginResponse loginResponse) {
  when (loginResponse) {
    is Success:
      // direct access to auth token available w/o need to cast
      return loginResponse.authToken;
    is InvalidCredentials:
      return null;
    is NoNetwork:
      return null;
    is Unexpected:
      return loginResponse.error;
  }
}

If Dart gets support for pattern matching in switch blocks, this might not even need a dedicated when construct as the type matching can be made in the case statements.

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.

@nodinosaur
Copy link

nodinosaur commented Jun 22, 2019

Yes, I agree that we are missing when in the example I have, the _when method is not exhaustive, nor does it catch any additional sub-types added to the parent class with the @sealed annotation.
For example, adding an isPaused does not give any IDE or Compiler hints:

class IsPaused extends ViewState {
  @override
  ViewState reduce(ViewState state) {
    return _when(
      (isIdle) => print('isIdle'),
      (isloading) => print('isloading'),
      (success) => print('${success.data}'),
      (error) => print('${error.message}'),
    );
  }
}

What I would like to see if there is an official Dart example of how we ought to be implementing @sealed? (cc. @srawlins \ @mit-mit)

@andrewackerman
Copy link

andrewackerman commented Jun 22, 2019

@sealed just means the class isn't supposed to be extensible, i.e. it can't be the superclass to any other class. The ADT behavior of sealed classes in Kotlin just takes advantage of the fact that sealed classes in Kotlin can have child classes made, but they must be in the same file as the sealed class. (So, in reality, Kotlin is also using inheritance to achieve ADT-like behavior. There are just a couple more features Kotlin offers [like exhaustive type checking] to make these "ADTs" more tidy to use overall.)

@xsahil03x
Copy link

xsahil03x commented Nov 16, 2019

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:-

@Data() marks an enum value to be treated as a Data class.

  • One should supply a list of possible fields inside the annotation.

  • If you don't want to add fields, use @object annotation instead.

  • Fields are supplied in the form of DataField objects.

  • Each DataField must contain the name and the type of the field.

  • If the field type needs to be generic use Generic type and annotate the enum value with @generic annotation.

@object marks an enum value to be treated as an object.

Then it can be used easily with the generated when function

 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),
  );

@renatoathaydes
Copy link

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.

@tvh
Copy link

tvh commented Nov 16, 2019

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.

@renatoathaydes
Copy link

renatoathaydes commented Nov 16, 2019

Everyone wants to solve this problem :D here's a few more existing libraries from #546 (comment)

If existing packages are interesting to see, here's a bunch of them:

@bubnenkoff
Copy link

Does Algebraic DT include monads like Either?

@minikin
Copy link

minikin commented Feb 4, 2021

@bubnenkoff check this pub https://pub.dev/packages/result_type it might help.

@nodinosaur
Copy link

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

@bubnenkoff
Copy link

bubnenkoff commented Feb 6, 2021 via email

@flukejones
Copy link

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.

@adrianboyko
Copy link

Sum types are not yet in the language funnel :(

@Abion47
Copy link

Abion47 commented Mar 11, 2021

@adrianboyko Check again under "Patterns and other features".

@Nikitae57
Copy link

Another vote for this feature

@aleksei-b-codexoptimus
Copy link

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.

@jjoelson
Copy link

jjoelson commented Apr 14, 2022

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.

@lrhn lrhn added the request Requests to resolve a particular developer problem label Apr 15, 2022
@Abion47
Copy link

Abion47 commented Apr 15, 2022

@hepin1989

@JarvanMo
Copy link

JarvanMo commented Apr 22, 2022

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 ;. And also enum class bring nothing but string and index! I can't say it can not work but I have to say I have to write more branches.

@munificent
Copy link
Member

I want data classes and removing the ;.

These are tracked on separate issues.

And also enum class bring nothing but string and index!

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.

@marcguilera
Copy link

marcguilera commented May 20, 2022

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:

  • Sealed interfaces.
  • Type matching in switch (or some other keyword like when) which could be exhaustive for sealed interfaces.
  • Data classes or records to represent data with automated copyWith, equals and hashCode. I think the main use case is for immutable data, but maybe it doesn't need to be restricted to it.

Packages like freezed are great but having to rely on code generation for this common cases feels sub optimal in my opinion.

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!

@renatoathaydes
Copy link

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

@munificent
Copy link
Member

Closing this because records and patterns are now enabled on the main branch of the Dart SDK! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests