Skip to content

Extend existing class to use mixin? #2166

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

Open
AndyLuoJJ opened this issue Mar 23, 2022 · 13 comments
Open

Extend existing class to use mixin? #2166

AndyLuoJJ opened this issue Mar 23, 2022 · 13 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@AndyLuoJJ
Copy link

AndyLuoJJ commented Mar 23, 2022

Recently I'm using Dart to complete a small project, and I need to write an extension to an existing class. However, I'm not allowed to make this existing class to use a mixin with extension. The following code will not compile:

mixin CustomMixin {
    double value = 0.0;
    void sayName() {}
    int getNumber() => 0;
}

// This is a class defined by other people that I cannot modify
class MyClassOne {
    int count = 0;
}

// compile error, Unexpected text 'with'
extension UseMixinOnClass on MyClassOne with CustomMixin { }

I'm also an iOS devloper using Swift. In Swift, I can write something like this:

protocol CustomProtocol {
    var value: Double { get set }
    func sayName()
    func getNumber() -> Int
}

// This is a struct defined by other people that I cannot modify
struct MyStructOne {
    var count: Int
}

extension MyStructOne: CustomProtocol {
    var value: Double {
        get {
            return Double(count)
        }
        
        set {
            count = Int(newValue)
        }
    }

    func sayName() {
        print("This is MyStructOne")
    }

    func getNumber() -> Int {
        return 1
    }
}

let instance: CustomProtocol = MyStructOne(count: 10)
print(instance.value) // 10.0
print(instance.getNumber()) // 1
instance.sayName() // This is MyStructOne

Now MyStructOne acts as a CustomProtocol.

I think this feature is useful and provides more flexibility. Wondering whether this 'extension with mixin' syntax can be added as a feature of Dart?

Thanks!

@AndyLuoJJ AndyLuoJJ added the feature Proposed language feature that solves one or more problems label Mar 23, 2022
@lrhn
Copy link
Member

lrhn commented Mar 23, 2022

The "add API to existing class" sounds similar to Rust traits as well. There are several proposals for something like that (like #2122).
It's not related to Dart extension (static extension members), which do not change the interface or API of classes.

@AndyLuoJJ
Copy link
Author

The "add API to existing class" sounds similar to Rust traits as well. There are several proposals for something like that (like #2122). It's not related to Dart extension (static extension members), which do not change the interface or API of classes.

Hi @lrhn and thanks for responding. I checked the following documentation: https://doc.rust-lang.org/book/ch10-02-traits.html. As mentioned in the documentation:

Trait definitions are a way to group method signatures together to define a set of behaviors necessary to accomplish some purpose.

It seems what I want is quite similar to the following Rust sample code:

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

Is it possible to do the same thing in current version of Dart?

@eernstg
Copy link
Member

eernstg commented Mar 24, 2022

In this particular case where CustomMixin provides behavior but not state, you can do the following:

// Class that we cannot edit, in library L1.
class MyClassOne {
  int count = 0;
}

// Extension in some library L2 that imports L1.
extension CustomMixinExt on MyClassOne {
  double get value => count.toDouble();
  set value(double v) => count = v.toInt();
  void sayName() => print('This is MyStructOne');
  int getNumber() => 1;
}

This means that any expression of type MyClassOne will support the given members (methods sayName() and getNumber(), plus the getter/setter value) as long as CustomMixinExt is imported into the current library.

It is a well-known issue that the methods are not available when CustomMixinExt is not imported into the current library, because you may have expressions of type MyClassOne in a library that doesn't import L2 (or L1, for that matter), but as long as you're willing to add import L2; as needed it will work.

You will not be able to invoke the added methods dynamically (extension methods are always resolved statically), the method implementations cannot be overridden by subclasses of MyClassOne (OK, they can do that, but we will still call the one from the extension unless the statically known type of the receiver is a suitable subtype of MyClassOne), and there is no subtyping relationship from MyClassOne to CustomMixin. So it's significantly less general/complete than actually adding the methods to the class MyClassOne (but the starting point was that we couldn't do that).

You don't get to reuse a set of member declarations in a mixin like CustomMixin, but if the whole point of CustomMixin was to use it to create this extension then that shouldn't matter. Otherwise you'll have to duplicate the member declarations (at least the headers, because the mixin could actually be able to obtain its implementation by forwarding to the extension methods, if the mixin has a suitable on-type).

@Levi-Lesches
Copy link

Here is a workaround:

mixin CustomMixin {
  double value = 0.0;
  void sayName() {}
  int getNumber() => 0;
}

/// This is a class defined by other people that I cannot modify
class MyClassOne {
  final int count;
  MyClassOne(this.count);
}

/// Combines the code in [CustomMixin] with [MyClassOne].
class MyClassTwo extends MyClassOne with CustomMixin {
  MyClassTwo(int count) : super(count);
}

/// Easy conversion from [MyClassOne] to [MyClassTwo]
extension UseMixinOnClass on MyClassOne {
  MyClassTwo get custom => MyClassTwo(count);
}

void main() {
  final object = MyClassOne(0);
  print(object.custom.getNumber());  // method from CustomMixin
}

And in cases where you're the one creating instances of MyClassOne, you can just create an instance of MyClassTwo instead.

@AndyLuoJJ
Copy link
Author

@Levi-Lesches Thanks for providing a possible solution. I also figure out another way to solve my problem using abstract class. Here is my sample code:

/// There are two classes defined by other people that I cannot modify
class MyClassOne {
  final int count;
  MyClassOne({required this.count});
}

class MyClassTwo {
  final int value;
  MyClassTwo({required this.value});
}

/// This is something I want to use as a protocol
abstract class CustomProtocol {
    static CustomProtocol? convertFromRawData(dynamic param) {
        // convert to different subclass
        if (param is MyClassOne) {
            return ConvertedMyClassOne(rawData: param);
        } else if (param is MyClassTwo) {
            return ConvertedMyClassTwo(rawData: param);
        }
        print("param type ${param.runtimeType} is not supported, cannot be converted into concrete CustomProtocol");
        return null;
    }

    // subclass must provide implementations of these protocol methods
    void sayName() => throw UnimplementedError("CustomProtocol.sayName is not implemented");
    int getNumber() => throw UnimplementedError("CustomProtocol.getNumber is not implemented");
}

class ConvertedMyClassOne extends CustomProtocol {
  ConvertedMyClassOne({required this.rawData});
  final MyClassOne rawData;
  
  @override
  void sayName() => print("I'm ConvertedMyClassOne");
  
  @override
  int getNumber() => rawData.count;
}

class ConvertedMyClassTwo extends CustomProtocol {
  ConvertedMyClassTwo({required this.rawData});
  final MyClassTwo rawData;
  
  @override
  void sayName() => print("I'm ConvertedMyClassTwo");
  
  @override
  int getNumber() => rawData.value;
}

void main() {
  // Instead of using MyClassOne and MyClassTwo directly, use ConvertedMyClassOne and ConvertedMyClassTwo to treat them as CustomProtocol
  // To create instance of class ConvertedMyClassOne or ConvertedMyClassTwo, use CustomProtocol.convertFromRawData
  final myList = [
    CustomProtocol.convertFromRawData(MyClassOne(count: 1)),
    CustomProtocol.convertFromRawData(MyClassTwo(value: 2)),
  ];
  
  for (final item in myList) {
    item?.sayName();
    print("getNumber == ${item?.getNumber()}");
  }
}

@Levi-Lesches
Copy link

While that certainly works, I'd suggest going with my approach if possible since

  1. the only conversion logic is from MyClassOne --> ConvertedMyClassOne using a simple constructor
  2. The conversion can even be made a method by using an extension (or a custom constructor)
  3. You can access MyClassOne.count directly instead of converting first to ConvertedMyClassOne
  4. You don't need to wrap MyClassOne within ConvertedMyClassOne

However, I see that MyClassOne and MyClassTwo have different field names, so I would adapt your solution like this:

// ----- Classes you don't control -----

class MyClassOne {
  final int count;
  MyClassOne({required this.count});
}

class MyClassTwo {
  final int value;
  MyClassTwo({required this.value});
}

// ----- Classes you do control -----

abstract class CustomProtocol {
  void sayName();
  int get number;
}

class ConvertedOne extends CustomProtocol {
  MyClassOne object;
  ConvertedOne(this.object);
  
  @override
  int get number => object.count;
  
  @override
  void sayName() => print("I'm a ConvertedOne");
}

class ConvertedTwo extends CustomProtocol {
  MyClassTwo object;
  ConvertedTwo(this.object);
  
  @override
  int get number => object.value;
  
  @override
  void sayName() => print("I'm a ConvertedTwo");
}

// ----- Logic -----

void main() {
  final one = MyClassOne(count: 1);
  final two = MyClassTwo(value: 2);
  final List<CustomProtocol> myList = [
    ConvertedOne(one), 
    ConvertedTwo(two),
  ];
  
  for (final item in myList) {
    item.sayName();
    print("item.number == ${item.number}");
  }
}

That would be helpful if you still need to access the original object's methods and fields. If you're only interested in a few fields, then you can make it even simpler by copying them into your own class and discarding the original:

// ----- Classes you don't control -----

class ClassOne {
  final int count;
  ClassOne({required this.count});
}

class ClassTwo {
  final int value;
  ClassTwo({required this.value});
}

// ----- Classes you do control -----

class CustomProtocol {
  final int number;
  final String name;
  CustomProtocol.fromOne(ClassOne obj) : 
    number = obj.count,
    name = "ConvertedOne";
  
  CustomProtocol.fromTwo(ClassTwo obj) : 
    number = obj.value,
    name = "ConvertedTwo";
  
  void sayName() => print("I'm a $name");
}

// ----- Logic -----

void main() {
  final one = ClassOne(count: 1);
  final two = ClassTwo(value: 2);
  final myList = [
    CustomProtocol.fromOne(one), 
    CustomProtocol.fromTwo(two),
  ];
  
  for (final item in myList) {
    item.sayName();
    print("item.number == ${item.number}");
  }
}

@kuyazee
Copy link

kuyazee commented Nov 20, 2024

Looking for this as well, for now I solve it by creating Union types. Here's an example

Note: I know toString() can already do this, I just used String as an example cause it's easy lol

@freezed
class RawString with _$RawString {
  const factory RawString.string(String value) = StringRawString;
  const factory RawString.integer(int value) = IntRawString;
  const factory RawString.double(double value) = DoubleRawString;
  const factory RawString.person(Person value) = PersonRawString;

  const RawString._();

  String toRawString() {
    return map(
      string: (value) => value.value,
      integer: (value) => value.value.toString(),
      double: (value) => value.value.toString(),
      person: (value) => 'Person(name: ${value.value.name}, age: ${value.value.age})',
    );
  }
}

// Extensions for easy conversion
extension StringToRawString on String {
  RawString toRawString() => RawString.string(this);
}

extension IntToRawString on int {
  RawString toRawString() => RawString.integer(this);
}

extension DoubleToRawString on double {
  RawString toRawString() => RawString.double(this);
}

extension PersonToRawString on Person {
  RawString toRawString() => RawString.person(this);
}

// Example Person class
class Person {
  final String name;
  final int age;
  Person(this.name, this.age);
}

// Display function
void display(RawString raw) {
  print(raw.toRawString());
}

// Example usage:
void main() {
  final person = Person('John', 30);
  final number = 42;
  final text = 'Hello';
  final decimal = 3.14;

  // Using extensions
  display(text.toRawString());    // Outputs: Hello
  display(number.toRawString());  // Outputs: 42
  display(decimal.toRawString()); // Outputs: 3.14
  display(person.toRawString());  // Outputs: Person(name: John, age: 30)

  // Or using constructors directly
  display(const RawString.string('Hello'));
  display(const RawString.integer(42));
}

@Wdestroier
Copy link

It seems what I want is quite similar to the following Rust sample code:

pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}

impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
Is it possible to do the same thing in current version of Dart?

You should be able to write the following code when augmentations are released:

augment class NewsArticle with Summary {
  String summarize() => '$headline, by $author ($location)';
}

@lrhn
Copy link
Member

lrhn commented Feb 27, 2025

Augmentations, dppending on which precise featureset they end up with, cannot add anything to a declaration from another library. They're only for creating one final declaration using parts in different places inside the same library (for example parts that were generated by a code-generator).
It's a code management feature for use internally in a single library, other libraries should not be able to see that an augmentation was used at all.

If the code is inside the same library, you could also just add the with Summary and String summarize() =>... to the original declaration.

@AndyLuoJJ
Copy link
Author

It seems what I want is quite similar to the following Rust sample code:
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
Is it possible to do the same thing in current version of Dart?

You should be able to write the following code when augmentations are released:

augment class NewsArticle with Summary {
String summarize() => '$headline, by $author ($location)';
}

Not that familiar with Rust, but seems this is exactly what I want in Dart.

@darkstarx
Copy link

OMG. What's the problem just support mixin on extension in Dart? It'll help to avoid copypaste in the code where there are to extensions on different classes which should have the same method implemented in some mixin.

@lrhn
Copy link
Member

lrhn commented May 12, 2025

The problem is that it won't (necessarily) work.

A mixin is an interface type. Extensions are not even type.
If an extension applied a mixin, then presumably it would mean that all concrete instance members of the mixin becomes extension members of the extension.

Variables cannot be extension members, so that rules out every mixin which declares an instance variable.
Extension methods cannot be abstract, so no abstract instance members in the mixin either.

The this reference of these methods will be theon type of the mixin, which won't implement the mixin's interface, or any interfaces added by an implements clause of the mixin declaration.

So let's assume that you cannot mix a mixin onto an extension unless the on type of the extension satisfies all the on clause requirements of the mixin, if the mixin has any implements clause, or it declares any instance variable or abstract instance members.

Then the extension's this type still doesn't implement the mixin type.

Take:

mixin Sortable<T> on List<T> {
  // Default ordering used by [sort]. Can be overridden in subclasses.
  int defaultOrder(T a, T b) => (a as Comparable).compareTo(b);
  @override
  void sort([int Function(T, T)? compare]) => _sortSortable(this, compare);
  // Helper function.
  void _sortSortable<T>(Sortable<T> sortable, int Function(T, T)? compare) {
    super.sort(compare ?? sortable.defaultOrder);
  }
}

extension MyList<T> on List<T> with SortableList<T> {}

This code still won't work, for a number of reasons:

  • The sort will be shadowed by the List.sort member, it doesn't override it. (Might get an error for invalid @override, but @override is optional, so let's assume it wasn't there.)
  • The _sortSortable cannot be called because this doesn't implement Sortable.

So more restrictions: The mixin must not refer to itself as a type. Nor must anyone else, probably.
So we've created as specialized extension mixin with many more restrictions, including not inctroducing a type. Or an interface.
Might as well make it not work as a normal mixin then, and then it's a new feature, an extension mixin, which adds extension methods to an extension (and maybe an extension type too. Maybe!)

So about:

What's the problem just support mixin on extension in Dart?

Many. It's basically a completely new feature. Maybe reconsider that "OMG" 😉 .

@darkstarx
Copy link

Yeah! Now a new OMG has appeared for me))) That's really a big problem, now I see, thank you!

But the problem of sharing methods between several extensions still exists. If the lang doesn't allow to resolve this anyhow, a new feature request really comes up.

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