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

Implicit Constructor proposal #108

Open
kevmoo opened this issue Nov 27, 2018 · 9 comments
Open

Implicit Constructor proposal #108

kevmoo opened this issue Nov 27, 2018 · 9 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@kevmoo
Copy link
Member

kevmoo commented Nov 27, 2018

Solution for #107, Related docs

This issue is for discussing the pros and cons of this particular proposal.
General discussion about the issue should go in #107 where everybody can see it.

I propose to add a new keyword – implicit that can be added to constructors and factories.

@kevmoo kevmoo added the feature Proposed language feature that solves one or more problems label Nov 27, 2018
@jmesserly
Copy link

jmesserly commented Nov 28, 2018

I think static conversion operators might work better (similar to C#). Example here: #105 (comment)

class Uri {
  ...
  // Add implicit conversions from String <-> Uri in both directions.
  static operator String(Uri uri) => uri.ToString();
  static operator Uri(String str) => Uri.parse(str);
}

The benefit is you can go in both directions. So the Uri class could define conversions to and from String. Makes it easier to support implicit conversions when you don't control both classes (e.g. similar to how extension methods let you add methods to a class you don't control).

@kevmoo
Copy link
Member Author

kevmoo commented Dec 13, 2018

The benefit is you can go in both directions.

Yup, that's a great analog.

I don't know why, but I'm drawn to a strict model here: "The only thing that can define implicit String -> Widget conversion is the Widget class".

It eliminate a lots of weirdness about which conversion is picked in a given context, etc.

...but certainly open to discuss!

@lrhn
Copy link
Member

lrhn commented Feb 25, 2020

Defining implicit conversion operations only on the source makes sense if you can use static extension methods to add them to any type. Ditto for only on the target type.

The difference is that on the source type, they can be treated as instance methods (like all other operators), but on the target type, they are constructors. Static extension methods would only work for instance methods.

Do we need an explicit way to invoke the implicit conversions?
Implicit conversions should happen only where there would otherwise be a compile-time error.
That measn that if I want to use the String to Uri conversion, I can't write "foo:bar" as Uri because the as does not perform an implicit cast. It's never a compile-time error.
So, do I have to write Uri x = "foo:bar"; instead, which means I can't do it in-line.

Consider the option of declaring normal instance members as cast members.
Any instance getter and any instance method which can be called with zero arguments can be marked as a cast. Assigning from a type to another incompatible type will check the type for accessible cast members which return a useful type (then try to pick the best one). You can always call the method or getter directly. Being a cast is automatically inherited by overriding members in subclasses.
You can also add cast extension methods.

@yanok
Copy link

yanok commented Aug 22, 2023

I wonder how resolution will work for implicit conversion functions? If I want to call a method that takes Uri argument but I don't have Uri class imported, will the implicit conversion trigger? If not, that would mean people would have to add imports just to get the implicits.

Or even worse, with subtyping there could be multiple suitable conversions:

class TA {
  implicit TA.fromSA(SA sa);
}
class TB extends TA {
  implicit TB.fromSB(SB sb);
}

class SA {}
class SB extends SA {}

now if you have SB sb in the context where TA is needed, you could either call TA.fromSA(sb) since sb is SA or call TB.fromSB(sb) since the result is TB which is also TA.

Now consider TA and TB are in different libraries. We would have to either:

  1. Complain about multiple possible implicit conversions. But that means adding an import could result in compilation error.
  2. Pick "the best one" silently. Now adding or removing an import could lead to completely different code being executed at runtime ;) Of course, Dart already have this with extension methods.

@eernstg
Copy link
Member

eernstg commented Aug 22, 2023

Here is a comment based on the proposal in this PR about implicit construction. In this proposal, an implicit constructor will only be enabled if it is imported and explicitly enabled in the import directive. (It is also enabled if it is declared in the current library).

This basically means that there is a per-library choice to make: "Which implicit constructors do we want to enable in this library?" and the chosen ones can be found easily by searching the list of imports for the word enable. Presumably, this would typically be a rather short list.

people would have to add imports just to get the implicits.

True. The assumption is that it is possible to read code that uses implicit construction if it involves a very well-known set of enabled implicit constructors. In contrast, it is assumed that the code would be unreadable if implicit constructors could easily and implicitly be enabled (say, if there was no need to enable them, or perhaps not even a need to import them).

So if you're going to use a given implicit constructor just a few times in a library then you'd import it and call it explicitly: In that situation this is probably a better trade-off than enabling that constructor for implicit invocations, thus enlarging the set of implicit constructors that every reader of the library must keep in mind.

About specificity: When there are multiple enabled implicit constructors in a situation where an expression has type Te and the context type is Tc, and Te is not assignable to Tc, the implicit constructor which has the most specific parameter type among the applicable ones is considered most specific.

In the TA, TB, SA, SB example, the implicit constructors are declared in the classes themselves, but we could easily move them out into a couple of static extension declarations (such that they can be expressed using #3040):

class TA {}
class TB extends TA {}

static extension ImplicitTA on TA {
  implicit factory TA.fromSA(SA sa) = TA;
}

static extension ImplicitTB on TB {
  implicit factory TB.fromSB(SB sb) = TB;
}

class SA {}
class SB extends SA {}

void f(SB sb) {
  TA ta = sb; // Implicitly invokes `TB.fromSB(sb)`.
}

The conceptual motivation for choosing the TB constructor is that it preserves the greatest amount of information about the object which is being implicitly converted.

adding or removing an import could lead to completely different code being executed at runtime
[... but we] already have this with extension methods

True again, this is one more construct in Dart where static analysis information will give rise to non-trivial run-time behavior choices.

However, the overall motivation for this mechanism is that it should be used in situations where the conversion from one type to another one is considered to be a tedious and trivial detail. The fact that implicit constructors are likely to be few and well-known in any given library is an important contributor to the realization of this goal. Yet, the fact that it is an implicit mechanism at all is the most common argument against the feature.

Finally, I think considerations similar to these are more or less applicable to other proposals about implicit constructors, not just the proposal in #3040.

@yanok
Copy link

yanok commented Aug 23, 2023

Thanks Erik for the detailed explanation and a link to #3040! Having to explicitly enable implicit conversions seems like a good trade off.

this is one more construct in Dart where static analysis information will give rise to non-trivial run-time behavior choices.

I guess what worries me here, is not the static analysis influencing run-time behavior, but specifically addition/removal of imports (which is often done by some tooling automatically) influencing run-time behavior. But again, this is already true for Dart with normal extension methods, so this proposal doesn't make things worse.

@eernstg
Copy link
Member

eernstg commented Aug 23, 2023

this proposal doesn't make things worse.

Alas, who can hope for more! 😁

@clragon
Copy link

clragon commented Dec 15, 2023

I believe this feature could solve the copyWith crisis in an unexpected way.

With implicit constructors, we could define something like this:

class CopyValue<T> {
  const CopyValue(this.value);

  final T value;
}

static extension CopyWithExtension<T> on CopyValue<T> {
  implicit const factory CopyValue.from(T value) = CopyValue;
}

This basically allows any value to be turned into a CopyValue.

Using this, we can then theoretically write:

class MyModel {
  const MyModel(
    required this.a,
    this.b,
  )

  final String a;
  final String? b;

  static MyModel copyWith(
    CopyValue<String>? a,
    CopyValue<String?>? b,
  )  {
    
    return MyModel(
      a: a?.value ?? this.a,
      b: b != null ? b.value : this.b,
    );
  }
}

which when used like this:

void main() {
  final x = MyModel(a: 'a', b: 'b');
  final y = x.copyWith(b: null);
  print(y.b == null); // true
}

which would then finally solve the problem of being unable to tell whether null was passed explicitly.

Because our copyWith takes both CopyValue and null we can tell whether a CopyValue was passed.
This is how some libraries already do things, by introducing a Value type with which passed values need to explicitly be wrapped in.

However, with implicit constructors, this wrapping process can be made completely implicit, making the experience for users outside essentially seamless.

In my opinion, such a solution would be extremely elegant, because it bypasses all the other problems raised in other proposals for "how do I tell whether null was passed explicitly or not".

if the dart language provides an object like CopyValue out of the box, this would be even better because it would standarize things accross the ecosystem. however, such an extension is not very difficult to write once we have this feature, so it probably is not all too important.

@nate-thegrate
Copy link

The value of union types seems pretty clear: at the time of writing, it's the 6th-most upvoted language issue, and since Dart is used for Flutter apps, allowing for improvements to Flutter APIs is highly valuable as well.

Based on conversations with a few folks, I've learned that the syntax proposed here allows for exactly the same benefits:

// union typedef
typedef ColorOrInt = Color | int;

// union class declaration
sealed class ColorOrInt {
  implicit const factory ColorOrInt.fromColor(Color color) = FromColor;
  implicit const factory ColorOrInt.fromInt(int i) = FromInt;
}

Implicit constructors could also apply to the extension_type_unions package, offering the same functionality with better performance.

typedef ColorOrInt = Union2<Color, int>;

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

7 participants