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

Value classes #2331

Closed
cedvdb opened this issue Jul 6, 2022 · 3 comments
Closed

Value classes #2331

cedvdb opened this issue Jul 6, 2022 · 3 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@cedvdb
Copy link

cedvdb commented Jul 6, 2022

Scala has value classes which can help statically type properties, also known as solving "primitive obsession".

Here is a nice article on them to understand why they can be useful and how they work. The gist of it is that they solve this not being caught as well as self validation, without having to use a real class wrapper:

class Product {
  final String id;
  final String name;
}

abstract class ProductRepository {
  Product find(String id);
  Product findByName(String name);
}

// somewhere
final productName = 'x';
productRepository.find(productName);

They also do not suffer from performance issues enumerated here dart-lang/sdk#3888 but the "why" is outside the scope of my knowledge.

Because of Flutter, we care increasingly about compilation to native code. Part of that means having efficient support for unboxed value types. Being able to implement the primitive number types would interfere with that, so we don't intend to go in that direction.

Validation

It's important for those Value classes to be able to self validate to protect invariants, protecting against those domain rules that prevents values existing in an invalid state

Transformation

I'm not sure that is in the article above but there can also be a need to be able to transform inputs. Eg: formatting an email value class to be lower case, etc.

Proposed Syntax

class PositiveInt(int _value) {
   // validation
   PositiveInt(this._value) : assert(_value >= 0);
   // transformation
   PositiveInt.parsed(String value): this(int.parse(value));
}

void main() {
  final result = add(PositiveInt(3), PositiveInt.parsed('4'));
}

int add(int a, int b) => a + b;
@cedvdb cedvdb added the feature Proposed language feature that solves one or more problems label Jul 6, 2022
@lrhn
Copy link
Member

lrhn commented Jul 6, 2022

This sound very much like #1474.

@eernstg
Copy link
Member

eernstg commented Jul 6, 2022

This sound very much like #1474.

Right, it sounds like you're looking for a way to treat an existing representation (for example, certain objects of type int) according to a different static typing and possibly a different interface than the one which is available with the type of the representation (in the example: the interface of int)?

In that case you might be able to use an upcoming Dart feature known as views:

closed view PositiveInt on int {
  factory PositiveInt(int value) {
    if (value <= 0) throw ArgumentError('PositiveInt not positive');
    return value;
  }
  factory PositiveInt.parsed(String value) => PositiveInt(int.parse(value));

  PositiveInt operator +(PositiveInt other) => this + other;
}

void main() {
  final result = addPositives(PositiveInt(3), PositiveInt.parsed('4'));
}

PositiveInt addPositives(PositiveInt a, PositiveInt b) => a + b;

There are several things to note here:

  • The view is on int, which means that it is a declaration of a way to access instances of int using a different "view" on the object. In particular, the view declaration specifies which members are available for such objects, and how they are implemented.
  • The view is closed, which means that there is no subtyping relation between the view type and the underlying type (declared using on T for some type T which is known as the 'on-type' of the view). So you can't assign an expression of type int to a variable of type PositiveInt or vice versa. This means that you need to invoke one of the constructors of PositiveInt in order to obtain a value of type PositiveInt, and those constructors can do whatever they want in terms of validation.

I changed add to addPositives and changed the parameter type to PositiveInt. The point is that I wanted to show that we can declare and invoke operations like + on instances of PositiveInt (with a non-standard semantics if needed, because we can just write what we want in the body of operator +), and we're encapsulating the fact that the view is on int inside the view declaration.

The client code looks like PositiveInt were a regular class with an operator + whose instances would have an instance variable which is an int (that is, it looks like a real, physical wrapper of an int), but the wrapping is compiled away: At run time there is no wrapper object, we are just working on the underlying int instance, but all method invocations are working like extension methods.

So all member accesses on a view type are resolved statically, but they can of course call methods on the underlying object (an instance of the 'on-type', here: int), and this is the set up that allows us to enforce constraints on which instances of int we can encounter with the static type PositiveInt, and what we can do to it.

@cedvdb
Copy link
Author

cedvdb commented Jul 6, 2022

@eernstg Ah! I had read that proposal but it makes much more sens now 😋

Goddamn the upcoming features are exciting. Can't wait. Shipping patterns, static meta programming and view in the same release would be a bomb of a release. Dart 3 kind of thing.

@cedvdb cedvdb closed this as completed Jul 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

3 participants