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

Extend existing class to use mixin? #2166

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

Extend existing class to use mixin? #2166

AndyLuoJJ opened this issue Mar 23, 2022 · 7 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));
}

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

5 participants