dart-lang / language

Design of the Dart language
Other
2.67k stars 205 forks source link

add function overloading #1741

Closed insinfo closed 1 year ago

insinfo commented 3 years ago

First of all, what is function overloading? Function overloading is a feature of a programming language that allows one to have many functions with same name but with different signatures. This feature is present in most of the Object Oriented Languages such as C++, Java, C# ...

Function overloading is a feature of object oriented programming where two or more functions can have the same name but different parameters.

When a function name is overloaded with different jobs it is called Function Overloading.

In Function Overloading “Function” name should be the same and the arguments should be different.

Function overloading can be considered as an example of polymorphism feature in C++.

Following is a simple C++ example to demonstrate function overloading.

#include <iostream>
using namespace std;

void print(int i) {
  cout << " Here is int " << i << endl;
}
void print(double  f) {
  cout << " Here is float " << f << endl;
}
void print(char const *c) {
  cout << " Here is char* " << c << endl;
}

int main() {
  print(10);
  print(10.10);
  print("ten");
  return 0;
}

I’ll post a real code example here. When I was working on Golang’s Selenium binding, I needed to write a function that has three parameters. Two of them were optional. Here is what it looks like after the implementation:

func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error {
    // the actual implementation was here
}

func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error {
    return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval)
}

func (wd *remoteWD) Wait(condition Condition) error {
    return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval)
}

I had to implement three different functions because I couldn’t just overload the function—Go doesn’t provide it by design. I have to admit that sometimes function overloading can result in messy code. On the other hand, because of it, programmers need to write more code.

Dart currently has the same limitation as Go, that is, no function overload, I believe that function overload speeds up and makes software development much easier, I believe it's more in favor than against.

mateusfccp commented 3 years ago

Duplicate of #1122.

Levi-Lesches commented 3 years ago

I needed to write a function that has three parameters. Two of them were optional.

I had to implement three different functions because I couldn’t just overload the function

But Dart does have optional parameters, with default values. I don't use Go, but here's what I think you want:

void wait(Conditioncondition, [Duration timeout = DefaultWaitTimeout, Duration interval = DefaultWaitInterval]) {
  // implementation here
}
insinfo commented 3 years ago

Optional parameter is not the same thing as function overloading, I believe that code is cleaner with function overloading, as it avoids putting a bunch of optional parameter in addition to other issues, it is much more difficult to port java/c#/c++ code to dart without the function overload. I have a large C# and java code base that I'm porting to dart and it's very difficult to do this without function overloading.

Optional parameters has to be last. So you can not add an extra parameter to that method unless its also optional.

I believe they serve different purposes. Optional parameters are for when you can use a default value for a parameter, and the underlying code will be the same:

public CreditScore CheckCredit( bool useHistoricalData = false,
bool useStrongHeuristics = true) { // ... } Method overloads are for when you have mutually-exclusive (subsets of) parameters. That normally means that you need to preprocess some parameters, or that you have different code altogether for the different "versions" of your method (note that even in this case, some parameters can be shared, that's why I mentioned "subsets" above):

public void SendSurvey(IList customers, int surveyKey) {
// will loop and call the other one } public void SendSurvey(Customer customer, int surveyKey) {
...
}

Optional parameters should be used if the parameters can have a default value. Method overloading should be used when the difference in signature goes beyond not defining parameters that could have default values (such as that the behavior differs depending on which parameters are passed, and which are left to the default).

// this is a good candidate for optional parameters public void DoSomething(int requiredThing, int nextThing = 12, int lastThing = 0)

// this is not, because it should be one or the other, but not both public void DoSomething(Stream streamData = null, string stringData = null)

// these are good candidates for overloading public void DoSomething(Stream data) public void DoSomething(string data)

// these are no longer good candidates for overloading public void DoSomething(int firstThing) { DoSomething(firstThing, 12); } public void DoSomething(int firstThing, int nextThing) { DoSomething(firstThing, nextThing, 0); } public void DoSomething(int firstThing, int nextThing, int lastThing) { ... }

Levi-Lesches commented 3 years ago

Method overloading should be used when the difference in signature goes beyond not defining parameters that could have default values (such as that the behavior differs depending on which parameters are passed, and which are left to the default).

(Emphasis added)

If you want to make two methods that have different behavior based on the types of the parameters passed to them, consider making differently-named methods? For example, the "do X with one object"/"do X with this list of objects" pair is often named operation/operationAll. So, taking from your example:

public void SendSurvey(IList customers, int surveyKey) {
// will loop and call the other one
}
public void SendSurvey(Customer customer, int surveyKey) {
// ...
}
void sendSurveyAll(List<Customer> customers, int surveyKey) {
  for (final Customer customer in customers) sendSurvey(customer, surveyKey);
}

void sendSurvey(Customer customer, int surveyKey) { /* ... */ }

Dart works on a philosophy that you can tell the meaning of a line of code simply by looking at the types involved. Method overloading makes this relationship ambiguous, as some values/types may be determined by logic. By using different method names, you make your intentions clear to the reader.

jodinathan commented 3 years ago

Dart works on a philosophy that you can tell the meaning of a line of code simply by looking at the types involved. Method overloading makes this relationship ambiguous, as some values/types may be determined by logic. By using different method names, you make your intentions clear to the reader.

That is true when you have proper names for functions, however, there are cases that method overloading makes sense and, from what we know, there is no other way to express in Dart or at least in a sound way.

In our case we have a builder that generates different arguments for a method that the intention is the same: searching.
For example:

Future search({String foo, int bar});
Future search({Date daz, bool baz});

So we don't have proper names for those methods, they are indeed the same but with different arguments.
We could use a Map, but then we lose the soundness.

Levi-Lesches commented 3 years ago

So we don't have proper names for those methods, they are indeed the same but with different arguments.

Future searchForString({String pattern, int count});
Future searchForDate({DateTime date, bool includeLeapYears});

Something like that. If you think that's repetitive, you should see:

Not saying this is the perfect naming system, just making the point that there already is a convention in place for these kinda things.

jodinathan commented 3 years ago
Future searchForString({String pattern, int count});
Future searchForDate({DateTime date, bool includeLeapYears});

Unfortunately this is not possible for us. That was just a very simple example.
There can be a search function with 20 arguments and other with 2 arguments.
As a Cassandra database should be modeled over queries, so do our models/search package.

  • replaceAll, replaceAllMapped, replaceFirst, replaceFirstMapped, replaceRange

  • split, splitMapJoin

    • Set
  • add, addAll

  • contains, containsAll

  • remove, removeAll, removeWhere

  • retainAll, retainWhere

Not saying this is the perfect naming system, just making the point that there already is a convention in place for these kinda things.

All those functions have proper names. In our case, as stated above, we don't have those names.

We thought of some options:

rrousselGit commented 3 years ago

I think it's important to differentiate "merging multiple functions into one name" from "trying to make incompatible parameters of the same function compile-safe"

I believe what many say here is, we don't want to support the former. But the latter is a good use-case.

The goal being, we should be able to remove the asserts from the Positioned widget constructor, where you can specify 3-4 params from either height/width/top/bottom/right/left

This widget allows:

Positioned(top: x, bottom: x, left: x, right: x)
Positioned(top: x, bottom: x, left: x, width: x)
Positioned(top: x, left: x, width: x, height: x)
...

but does not allow:

Positioned(top: x, bottom: x, left: x, right: x, width: x)
Positioned(top: x, bottom: x, left: x, right: x, height: x)
...

This is not a case where the function would usually be split into multiple smaller ones. Rather we want to express a union of the different parameters.

This could be defining Positioned as:

Positioned({required int top, required int bottom, required int left, required int right});
Positioned({required int top, required int bottom, required int left, required int width});
Positioned({required int top, required int left, required int width});

where the implementation would be a single function with the prototype:

Positioned({int? top, int? bottom, int? left, int? width, int? height});
jodinathan commented 3 years ago

@rrousselGit from the developer view that is using the Positioned widget in your example, it wouldn't make any difference if it is one single function or not. Sure the compiler doesn't have to infer the correct method to call but it can still add some pain points to the language because the dev still sees many options to one method.

I understand that method overloading can be a pain to read the code and it makes autocomplete a mess.
Take TypeScript for example. You just can't rely on autocomplete because it is just way too hard to read. It is easier for you to dig the source-code of what you are trying to use than reading the autocomplete. We use TypeScript a lot and this is a great pain.

IMO we could a strict method overloading by not allowing core type arguments with same name, ie:

void foo(int bottom); 
void foo(String bottom); // do not allow this

If the dev changes the name of the argument but uses a simple parse to call another method, disallow or lint over it:

void walk(int steps) => _walk(steps);
void walk(String strSteps) =>  _walk(int.parse(strSteps)); // lint this: avoid overloading core types. Or maybe disallow this at all

I think if we avoid somehow the overuse of method overload, then it is a very good addition to the language.

insinfo commented 3 years ago

I understand that method overloading can be a pain to read the code and it makes autocomplete a mess. I don't agree with that statement.

In C# autocomplete works perfectly with code fill with overloaded functions. I don't see any code reading problems.

@Levi-Lesches I understand your point of view on dart philosophy, but I see a big headache doing a bunch of functions for the same purpose with a bunch of different names or a bunch of optional parameters, function overloading makes it all cleaner .

jodinathan commented 3 years ago

In C# autocomplete works perfectly with code fill with overloaded functions. I don't see any code reading problems.

That is really dependent on the library you are consuming.
Few months ago we delivered a C# app and the libraries we consumed literally had at least one overloaded version of a function that just accepted a String instead of int and parsed it in the end.
The autocomplete was a mess because of that and we also identified that the documentation of overloaded functions were sparse in comparison to simpler methods.

For that reason I fully understand the Dart team and I think we could have a stricter kind of overloading.

insinfo commented 3 years ago

I'm not very much in favor of adding reserved words in the language, but I think to solve this situation, and have a more rigid (stricter) function overload, I could have a reserved word "over" for overloaded functions and maybe restrict it to only class methods

class Vector4
{
  Vector4(double x, double y, double z, double w)
  {
      this.x = x;
      this.y = y;
      this.z = z;
      this.w = w;
  }

  over Vector4(Vector2 v)
  {
      x = v.x;
      y = v. y;
      z = 0.0f;
      w = 0.0f;
  }

  over Vector4(Vector3 v)
  {
      x = v.x;
      y = v.Y;
      z = v.Z;
      w = 0.0f;
  }

  static Vector4 transform(Vector4 vec, Matrix4X4 mat)
  {
      Vector4 result;
      ...
      return result;
  }
  static over Vector4 transform(Vector4 vec, Quaternion quat)
  {
      Vector4 result;
      ...
      return result;
  }
}
Levi-Lesches commented 3 years ago

Why use over? The compiler is smart enough to know when you're defining a method with the same name as another.

Diegovsky commented 2 years ago

I use a pattern that works in Rust, but would be great to have in Dart:

abstract class Receiver<T> {
  void handleMessage(T msg);
}

class ListPage extends StatefulWidget {
  const ListPage({ Key? key }) : super(key: key);

  @override
  _ListPageState createState() => _ListPageState();
}

class _ListPageState extends State<ListPage> implements Receiver<EntryMessage>, Receiver<UserEditMessage> {
   @override
  void handleMessage(EntryMessage msg) {
    // handle entry message type
  }
  @override
  void handleMessage(UserEditMessage msg) {
    // handle user edit message
  }

}

Rust doesn't have function/method overloading but allows types to implement generic traits (interfaces) like this. I think that looks really neat

jodinathan commented 2 years ago

@Diegovsky that looks like method overloading to me

Diegovsky commented 2 years ago

@Diegovsky that looks like method overloading to me

Kinda. I'm not sure if you have experience with Rust, but trait (interface) implementations are a bit like Dart's extensions.

The compiler treats two trait methods from different traits which have the same name but different signatures as different things. I think (not a rust expert) the compiler sees a method (e.g handleMessage), acertains it comes from a trait (in dart's case could be a class/interface/mixin), tries to resolve the origin (Receiver<EntryMessage> or Receiver<UserEditMessage>), notices it is ambiguous and then looks at the method's arguments types.

I think it might not be too hard for dart to implement it because it knows what type a lambda argument is based on the method you're giving it to, e.g:

["foo", "bar"].forEach((e) => print(e))). // Can also be (String e) => print(e)

@tatumizer Well, if you don't use handler for anything nor specify it's type, the compiler can't know which you mean and throws an error. However, if you do one of the following it works fine:

var handler = pageState.handleMessage;
handler(EntryMessage()); // Using type inference
Function(EntryMessage) handler = pageState.handleMessage; // Specifying the type
Diegovsky commented 2 years ago

Indeed, I'm not proposing function overloading like C++, java and C# have. I'm not knowleadgable enough on language design to say how that's different from normal function overloading but I've seen people saying that doing it with traits it's better than C++'s way.

I would argue that in the specific case I showed the methods being overriden come from different sources so it could be possible since dart does not do type erasure. I focused on rust as it allows this but I don't think rust's implementation of it would be fit for dart either. Something like F#'s resolution might work better in dart? I'm not sure anymore tbh. This mechanism is more complex than I expected :/

eernstg commented 2 years ago

@Diegovsky, I suppose your example would correspond to something like the following in Rust?:

trait Receiver<T> {
  fn handle_message(&self, msg: T);
}

struct EntryMessage;
struct UserEditMessage;

struct ListPageState;

impl Receiver<EntryMessage> for ListPageState {
  fn handle_message(&self, _msg: EntryMessage) {
    println!("handle entry message");
  }
}

impl Receiver<UserEditMessage> for ListPageState {
  fn handle_message(&self, _msg: UserEditMessage) {
    println!("handle user edit message");
  }
}

fn main() {
  let x: ListPageState = ListPageState {};
  x.handle_message(UserEditMessage {});
  x.handle_message(EntryMessage {});
}

As you mentioned here, Dart extension methods are quite similar to Rust impl declarations of how to implement a specific trait for a specific struct.

The main difference is that Rust (apparently) uses the actual arguments' types to determine which implementation is appropriate for the given call, whereas Dart immediately reports an ambiguity error when it has been determined that there are multiple extension methods which are available and applicable, and none of them is more specific than all the others.


In Dart, we can actually disambiguate the invocations:

// ----------------------------------------------------------------------
// Empty declarations for missing parts.

class StatefulWidget {
  const StatefulWidget({Key? key});
}
class Key {}
class State<X> {}

// Marker interface, used to guide extension method resolution.
abstract class Message implements EntryMessage, UserEditMessage {}
class EntryMessage {}
class UserEditMessage {}

// ----------------------------------------------------------------------

abstract class Receiver<T> {}

class ListPage extends StatefulWidget {
  const ListPage({ Key? key }) : super(key: key);
  _ListPageState createState() => _ListPageState('');
}

class _ListPageState extends State<ListPage> implements Receiver<Message> {
  String id;
  _ListPageState(this.id);
}

extension on Receiver<EntryMessage> {
  void handleMessage(EntryMessage message) {
    print('Handle entry message.');
  }
}

extension on Receiver<UserEditMessage> {
  void handleMessage(UserEditMessage message) {
    print('Handle user edit message.');
  }
}

void foo(Receiver<EntryMessage> x1, Receiver<UserEditMessage> x2) {
  x1.handleMessage(EntryMessage());
  x2.handleMessage(UserEditMessage());
}

void main() {
  var x = _ListPageState('MyId');
  foo(x, x);
  // x.handleMessage(EntryMessage()); // Error, ambiguous.
}

The invocations in foo are applied to the single instance of _ListPageState, and they are guided by the static type (Receiver<EntryMessage> respectively Receiver<UserEditMessage>) to run one or the other handleMessage method. Those methods are not just overriding implementations of a shared method signature, they are completely unrelated, and they could have arbitrarily different signatures. So this approach yields complete freedom to declare the handleMessage with any desired signature for each case, and then we can use the static type of the receiver to decide on which one to get (and we can distinguish between types that only differ in their type arguments like Receiver<T1> and Receiver<T2>, but we could of course also have receiver types that differ in other ways).

Note that this mechanism is powerful enough to allow methods with the same signature to coexist, because it is the static type of the receiver that determines which implementation to run. So, for example, we can use this to have several different versions of a getter int get foo, and we can still make sure that y.foo will run exactly the one which is associated with the static type of y. Normal static overloading does not support this: If you have several methods with the same signature then they are simply a compile-time error, because we'd never know how to choose one of them over the others.

The downside is that it may be inconvenient to have to make this choice by ensuring that the receiver has a specific static type. For instance, at the end of main above, the invocation of x.handleMessage(...) is a compile-time error, because the receiver type is _ListPageState, and that's a subtype of both Receiver<EntryMessage> and Receiver<UserEditMessage>, and hence the invocation is ambiguous.

We could do (x as Receiver<EntryMessage>).handleMessage(...), but that's a lot less convenient than just using different names in the first place: x.handleEntryMessage(...) vs. x.handleUserEditMessage(...).


We could also re-shape the invocations such that the choice of method is determined by the object that actually matters here, that is, the argument:

class ListPage {}
class Key {}
class State<X> {}
class EntryMessage {}
class UserEditMessage {}

class _ListPageState extends State<ListPage> {
  String id;
  _ListPageState(this.id);
}

extension on EntryMessage {
  void passMessageTo(_ListPageState state) {
    print('Handle entry message.');
  }
}

extension on UserEditMessage {
  void passMessageTo(_ListPageState state) {
    print('Handle user edit message.');
  }
}

void main() {
  var x = _ListPageState('MyId');
  EntryMessage().passMessageTo(x);
  UserEditMessage().passMessageTo(x);
}

You could say that this is a "reverse method invocation": The argument is provided as the syntactic receiver, and the receiver is provided as the syntactic argument. It would generalize to multiple arguments if we had tuples: (pass, several, arguments).passArgumentsTo(receiver).

This would allow for completely unrelated signatures for passMessageTo (respectively passArgumentsTo) with different "reverse parameter types", and the ability to choose type arguments in an extension method invocation would correspond to having a "generic reverse method".

Of course, this is not really an option in real software design: If we use this approach then the extension method named passMessageTo (passArgumentsTo) has now been "taken" for the types EntryMessage and UserEditMessage (or for the given tuple type), and we can't create a similar set up for a "reverse receiver" with a different type, unless we can keep the two sets of extension declarations completely separate (that is, they are never imported into the same library).

We might be able to use (receiver, arg1, arg2, ...).runFoo, as a way to encode receiver.foo(arg1, arg2, ...). This would allow for separate declarations of foo methods for different receiver types, and they could have arbitrarily different signatures, but they would have to be called using the unfamiliar syntax (receiver, arg1, arg2, ...).runFoo rather than receiver.foo(arg1, arg2, ...).

Nevertheless, the whole "reverse method" idea is kind of fun. :smile:

eernstg commented 2 years ago

a much simpler method of disambiguation: by parameter name

I agree that we can use the function signature to determine applicability.

If two function types F1 and F2 taking named parameters (actually, any function types would do) are in a subtyping relationship (say, F2 <: F1) then every type correct invocation of a function of type F1 is also guaranteed to be a type correct invocation of a function of type F2. Conversely, if the two types are not the same type then there will be invocations of a function of type F2 that aren't correct for F1.

So we can always use a type check on the invocation to at least determine which subset of the given statically overloaded variants would admit any given list of actual arguments, and then we can check whether that set of candidates has a most specific element.

The fact that a declaration that doesn't accept a named parameter with the name n can't possibly be a candidate for an invocation of the form o.m(... n: e) is just one of the things that fall out if we use a check like this.

So disambiguation isn't that much of a problem in itself. The problem is more likely to be that it is difficult to get a good understanding of which invocations would be ambiguous, for any given set of static overloads.

If we require that every member of a given set of static overloadings must have a required named argument which is unique for that static overloading then we can of course immediately conclude that "when the call passes n: e, and static overload number 3 is the only one that accepts a named parameter named n, then this must be an invocation of number 3". However, I would expect that approach to be too inflexible in practice...

Diegovsky commented 2 years ago

@Diegovsky, I suppose your example would correspond to something like the following in Rust?:

Yes, that's the example I was trying to give. I'm still not very good with words so I couldn't put it so well like you did.

subzero911 commented 2 years ago

+1 for function overloading You've added rather complicated language concepts like strong typing, covariants, enum classes, null-safety, and you even heading to add ADT. But you didn't add such a basic OOP feature like function overloading because "it will make Dart difficult", that's just ridiculous!

Andrflor commented 2 years ago

Having function overloading would resolve a lot of problems that now can only by solved using dynamic or separated interfaces.

Using dynamic is pretty bad because it allow user to call the function with a type we don't want. But since we have neither type composition nor function overloading there is no other way... Except of course providing type dependent interfaces.

Solutions liked freezed or other are not at all solutions since they produce an overlay over the data... This is in my humble opinion the most lacking feature of dart.

The only one that makes me loose my mind figuring out workarounds that in fact do not exist.

mateusfccp commented 2 years ago

Having function overloading would resolve a lot of problems that now can only by solved using dynamic or separated interfaces.

@Andrflor Could you give us a concrete example? The only case where I feel like overload really matters is for operators, because for regular methods we can simply give each of them a different (and more meaningful) name.

As much as you may think it is a hassle, it is at most an "aesthetic" preference of using the same name for different function signatures vs using a different name for each signature.

Thus, I don't see it as a limitation that requires workarounds, unless I am missing something important here.


Also, isn't this issue a duplicate of #1122?

jodinathan commented 2 years ago

more meaningful name

firstOrNull firstWhereOrNull

Even a simple try catch uses overloading by allowing catch(e) and catch(e, st) but you can't do that with regular methods.

To understand how weird is not to have method overloading, imagine if we changed the try catch clause to fit the aesthetic preference:

try {
} catch (e) {}

try {
} catchWithStackTrace (e, st) {
}
mateusfccp commented 2 years ago

Even a simple try catch uses overloading by allowing catch(e) and catch(e, st) but you can't do that with regular methods.

I can't say if catch is overloaded internally, but it doesn't have to. The same method could be easily modeled in terms of optional arguments.

As I said, you may find a hassle to not have method overload, but @Andrflor said that it makes it impossible to implement some constructs without using workarounds and dynamic, but I can't remember a case that it is true, so I asked for a concrete example.

jodinathan commented 2 years ago

The same method could be easily modeled in terms of optional arguments.

No you can't.

void Function([String]) foo = () {}; 
// Error: A value of type 'void Function()' can't be assigned to a variable of type 'void Function([String])'.

If try catch was modeled without overloading, it should aways have the st as an optional argument:

try {

} catch (e, [st]) {

}
Andrflor commented 2 years ago

Let's imagine two classes that have the same real interface but two different programmatic interfaces.

For example from one package

class Rx<T> {
   Stream<T> stream;
}

From a differnet package

class Obs<T> {
   Stream<T> stream;
}

In real life they share the same interface. But for dart they are unrelated.

I would like to do

bind(Obs obs) {
    bindStream(obs.stream);
}
bind(Rx obs) {
    bindStream(obs.stream);
}

or

bind(Obs or Rx obs ) {
    obs is Obs? bindStream(obs.stream) : bindStream(obs.stream);
                 // First part is cast as Obs        // This is cast as Rx
}

I'm forced to do

bind<T extends Object>(T obs) {
    try {
       bindStream((obs as dynamic).stream);
    } on Exception catch (_) {
      throw '$T has not method [stream]';
    }
}

Function overloading will be way better and much more simple. It would avoid runtime errors.

@mateusfccp I don't really see why bindObs and bindRx would make more sense since they have the common interface. I can not just bindStream and leave obs.stream to the user side because on my real problem some streams need complex transformations before bind.

Wdestroier commented 2 years ago

@Andrflor you could write bind(Rx | Obs obs) => bindStream(obs.stream); with union types.

mateusfccp commented 2 years ago

@Andrflor

Considering you own those classes, you could model them this way:

abstract class WithStream<T> {
  Stream<T> get stream;
}

class Rx<T> with WithStream<T> {
   @override
   final Stream<T> stream;
}

class Obs<T> with WithStream<T> {
  @override
  final Stream<T> stream;
}

void bind<T>(WithStream<T> withStream) {
  bindStream(withStream.stream);
}

If you don't own this classes, you could model it with a trait-like, which Dart unfortunately doesn't have (see #1612), or, as @Wdestroier suggested, something like untagged unions (i.e. typedef StreamUnion = Obs | Rx; see #83, #145), which Dart also doesn't have. Depending on the union implementation, Dart could infer the common interface between the types and allow you to use them directly without having to promote/cast the parameter.

Regardless, this doesn't seem to be a problem to be solved by overload.

Levi-Lesches commented 2 years ago

@andrflor, even if you don't own the class, you can still do this with a little trick called mixins:

// The original code that you don't own
class Rx<T> { Stream<T> stream; Rx(this.stream); }  // package1.dart
class Obs<T> { Stream<T> stream; Obs(this.stream); }  // package2.dart

// Your code:
// Import the original classes under a prefix...
import "package1.dart" as p1;
import "package2.dart" as p2;

// ...redeclare them under a new interface, StreamGetter...
mixin StreamGetter<T> { Stream<T> get stream; }
class Rx<T> = p1.Rx<T> with StreamGetter<T>;
class Obs<T> = p2.Obs<T> with StreamGetter<T>;

// ... and write your code with StreamGetter
void bindStream<T>(Stream<T> stream) { }
void bind<T>(StreamGetter<T> obj) {
  if (obj is Rx) { /* some complex work here */ }
  else if (obj is Obs) { /* some complex work here */ }
  bindStream(obj.stream);
}

// Your user's code:
Stream<int> getInts() async* {
  for (final int value in [1, 2, 3]) { yield value; }
}

void main() {
  StreamGetter rx = MyRx<int>(getInts());
  bind(rx);
}

This fundamental problem here isn't overloading, it's the fact that you don't control the original code. If these two objects are indeed related. then they should be declared to do so, but if you don't own the original code, you can't make that very simple change. This workaround may seem clunky on your side, but it simplifies the API for your users, who see Rx and Obs the same way you do: as two objects that share an interface.

Also, yes, this should probably be closed as a duplicate of #1122

Andrflor commented 2 years ago

@Levi-Lesches This is not working since it removes the factory constructors and static functions. Obs only has factory constructors public. So now i cannot instantiate Obs anymore.

eernstg commented 2 years ago

@Andrflor, I agree with several others that the discussion about your example here is mostly about member set management (we have subtyping, and we could have structural types or union/intersection types). Those discussions should be taken in an issue about structural typing, #1612, or about union types, #83 or #145.

However, we do have a particular kind of static overloading with extension methods, and this mechanism can be used as follows (using the example from @Levi-Lesches as a starting point):

// ----- Library 'lib.dart'.
// This is the original code that you don't own.

class Rx<X> {
  Stream<X> stream;
  Rx(this.stream);
}

class Obs<X> {
  Stream<X> stream;
  Obs(this.stream);
}

// ----- Library 'framework.dart'.
// Your code.
// import 'lib.dart';

void bindStream<T>(Stream<T> stream) {}

extension BindStreamRx<X> on Rx<X> {
  void bind() {
    /* some complex work here */
    bindStream(stream);
  }
}

extension BindStreamObs<X> on Obs<X> {
  void bind() {
    /* some complex work here */
    bindStream(stream);
  }  
}

// ----- Library 'user.dart'.
// Your user's code.
// import 'framework.dart';

Stream<int> getInts() async* {
  for (final int value in [1, 2, 3]) {
    yield value;
  }
}

void main() {
  var rx = Rx<int>(getInts());
  rx.bind();
  var obs = Obs<int>(getInts());
  obs.bind();
}

This won't allow you to avoid the duplication of the code bindStream(stream).

However, a major reason why Dart is using nominal subtyping is that the explicit subtype relation implies substitutability, and we only have a subtype relation when some developer has chosen to declare that subtype relationship, implying that a family of declarations with the same name will "do the same thing", and hence it's OK to invoke a member with the given name even though we have no knowledge (at compile time) about which one will actually be executed at run time.

In the example above, there is no reason to assume that the purpose and behavior of Rx.stream is in any way similar to the purpose and behavior of Obs.stream. At one call site we're calling the former in the expression bindStream(stream), and at the other call site we call the latter in the expression bindStream(stream).

This means that the two invocations are considered to be fundamentally different, and hence there is no way to abstract over the difference, and combine those two invocations at the same call site, ..

Except, of course, if we use a receiver of type dynamic. But that's just another way to say to the compiler "Yes, this is crazy, but I know what I'm doing". ;-)

There is very little difference between the approach shown above, and using separate bind functions:

void bindRx<X>(Rx<X> rx) {
  /* some complex work here */
  bindStream(rx.stream);
}

void bindObs<X>(Obs<X> obs) {
  /* some complex work here */
  bindStream(obs.stream);
}

Finally, if you do not want to invoke the two different getters named stream based on the given unrelated receiver types then you have to establish a relationship involving those two receiver types, and that's exactly what several other comments have described, e.g., the one from @mateusfccp that introduces a common supertype WithStream. Even if we do introduce union types, it is unlikely that we will allow them to abstract over multiple unrelated declarations with the same name, and then you still have to cast the receiver to dynamic, or you need to have two call sites—one for Rx.stream, and another one for Obs.stream.

Andrflor commented 2 years ago

@mateusfccp if I owned the classes the problem would not exists..

@eernstg Well that's my bad Since i tried to cite as few line of code as possible the context is lost.

class Rx<T> {
  void bind(Obs<T> obs) => bindStream(obs.stream);
  void bind(Rx<T> obs) => bindStream(obs.stream);

  void bindStream(Stream stream) {
    // Complex Work here
   }
}

For the user binding with Rx or binding with Obs does not make any difference... In a sense it's more or less syntactic sugar.

This is also not working as extension

extension BindRx<T> on Rx<T> {
  void bind(Rx<T> obs) => bindStream(obs.stream);
}

extension BindObs<T> on Rx<T> {
  void bind(Obs<T> obs) => bindStream(obs.stream);
}

This will only allow the first one.

You're right this is more about structural types or union/intersection types. Having those would enable this without dynamic.

eernstg commented 2 years ago

I'm not sure what Rx {...} would mean. Is it intended to be class Rx<X> {...}?

This is also not working as extension

You don't have static overloading on parameter types using extensions (or using anything in Dart), but you do have a kind of static overloading on the receiver type using extensions. So you could do this (which is what I mentioned in an earlier example):

extension BindRx<X> on Rx<X> {
  void bind() => bindStream(this.stream);
}

extension BindObs<X> on Obs<X> {
  void bind() => bindStream(this.stream);
}

You would have to do myRx.bind() rather than bind(myRx) respectively myObs.bind() rather than bind(myObs), but that's all it takes.

Also, we'll have records soon (for some value of 'soon' ;-), so if you want to do something similar for several types you can just dispatch on a record:

extension HandleIntInt on (int, int) {
  void handle() {/*complex stuff on int and int*/}
}

extension HandleIntDouble on (int, double) {
  void handle() {/*complex stuff on int and double*/}
}

// other combinations that need their own implementations.

void main() {
  (1, 2).handle(); // Runs the code for `int` and `int`.
  (1, 2.5).handle(); // Runs the code for `int` and `double`.
}

This is similar to a static overloading mechanism with cases void handle(int, int), void handle(int, double), etc.

Andrflor commented 2 years ago

I meant class Rx<T> I edited the post.

I don't try to bind Rx or Obs to itself. I try to bind Rx<T> to another object that as a property stream which is a Stream<T>.
My use case is indeed more related to #148 which is in fact union type.

@mateusfccp in a sense I agree that the given example is more about aesthetic but in a sense not really. But like you said, there is still the problem of operator overloading.

extension RxIntExt on Rx<int> {
  bool operator <(int other) => value < other;
  bool operator <(Rx<int> other) => value < other.value;
}

This won't work because it has the same name. If i declare this with distinct extensions for each one, the compiler will yell at me cause none are more specific.

Am I wrong or there is no operator overloading in dart, but just operator overriding?

Because even if I just declare

extension RxIntExt on Rx<int> {
  bool operator <(int other) => value < other;
}

I've lost the symmetrical property because i can write

extension Operator on int {
  bool operator <(Rx<int> other) => this < other.value;
}

But it's complete dead code since it won't do anything. It's at the same time allowed, but just has no effect.

Btw, like already mentioned it seems to be a duplicate of #1122 It has already a post about operator overloading but it seems to have fade in the limbs.

eernstg commented 2 years ago

OK, I should have made the "use extensions to get static overloading on the receiver" example more complete. Here we go:

// ----- Glue code.

class Obs<X> {
  final Stream<X> stream;
  Obs(this.stream);
}

class Both implements Rx<Never>, Obs<Never> {
  @override
  noSuchMethod(Invocation i) => throw 0;
}

Both get any => Both(); // Just so we can write `any` where we mean `/* some expression here */`.

// ----- First version: 
// Pretend that we have static overloading on method parameters.

class Rx<X> {
  final Stream<X> stream;
  Rx(this.stream);

  void bind(Obs<X> obs) => bindStream(obs.stream);
  void bind(Rx<X> rx) => bindStream(rx.stream);

  void bindStream(Stream stream) {
    // Complex Work here
   }
}

void useIt() {
  Rx<int> rx = any, rx2 = any;
  Obs<int> obs = any;
  rx.bind(rx2); rx2.bind(obs); // OK.
}

// ----- Second version:
// Just use different names.

class Rx<X> {
  final Stream<X> stream;
  Rx(this.stream);

  void bindObs(Obs<X> obs) => bindStream(obs.stream);
  void bindRx(Rx<X> rx) => bindStream(rx.stream);

  void bindStream(Stream stream) {
    // Complex Work here
   }
}

void useIt() {
  Rx<int> rx = any, rx2 = any;
  Obs<int> obs = any;
  rx.bindRx(rx2); rx2.bindObs(obs); // OK.
}

// ----- Third version:
// Use static overloading on receiver.

extension GetBoundByRx<X> on Rx<X> {
  void getBoundBy(Rx<X> rx) => rx.bindStream(stream);
}

extension GetBoundByObs<X> on Obs<X> {
  void getBoundBy(Rx<X> rx) => rx.bindStream(stream);
}

class Rx<X> {
  final Stream<X> stream;
  Rx(this.stream);

  void bindStream(Stream stream) {
    // Complex Work here
   }
}

void useIt() {
  Rx<int> rx = any, rx2 = any;
  Obs<int> obs = any;
  rx2.getBoundBy(rx); obs.getBoundBy(rx2); // OK.
}
mit-mit commented 1 year ago

Closing as a duplicate of https://github.com/dart-lang/language/issues/1122

esoros commented 3 weeks ago

For what it's worth, overloading is a problem that's in NP-complete and can cause slow build times. Probably not worth it imo if you want fast builds in any language. https://web.cs.ucla.edu/~palsberg/paper/dedicated-to-kozen12.pdf

esoros commented 3 weeks ago

you can also duplicate parameters (because it's funny, as well....) so void foo(int bottom); void foo(String bottom, String _ignore); // pass the second parameter, but ignore in the implementation

mraleph commented 3 weeks ago

@esoros

For what it's worth, overloading is a problem that's in NP-complete and can cause slow build times. Probably not worth it imo if you want fast builds in any language. https://web.cs.ucla.edu/~palsberg/paper/dedicated-to-kozen12.pdf

That only applies to a very specific variant where type inference and overloading are interconnected (the proof is given in context of λ-calculus with overloading). This does not apply to most mainstream programming languages which usually require you to specify signatures of methods rather then inferring these signatures. Indeed the paper covers this in Section 8 where it explains that overload resolution in Java is not NP-complete.

Wdestroier commented 3 weeks ago

I remember a performance issue in C# related to type inference and overloading. I couldn't find the resolved issue in YouTrack, but I found another one which looks like the same problem. https://youtrack.jetbrains.com/issue/RIDER-77533/Rider-freezing-when-analyzing-nested-LINQ-queries.

image

I still think it's a good feature for a few cases. @esoros isn't it better to continue the discussion to support function and method overloads in the open issue?

esoros commented 3 weeks ago

That's basically the same problem....but sure that's fine. The output parameter is a formal parameter too, so when people have languages where you can write "var x = "asdasdasd" and overloads you get slow builds, because not all of the formal parameters are present.

In the enterprise or for large projects people like types such as IDomainLoader<Dictionary<IMappedUserLoader<CustomerDomainStrategy, CusomerUserLoaderStrategy>>>> and then don't want to type that in repeatedly. If languages don't support any type inference, then overloading is fine.....that' s a tradeoff.