dart-lang / language

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

Interface support for extensions #736

Open ykmnkmi opened 4 years ago

ykmnkmi commented 4 years ago

Like in #281 based on extensions

abstract class Serializable { dynamic toJson(); }
class Explicit implements Serializable { toJson() => 'Explicit'; }
class Implicit { toJson() => 'Implicit'; }
class Person { String name; }

extension PersonSerializable on Person implements Serializable {
  toJson() => { 'name': name };
}

void main() {
  var data = <Serializable>[Explicit(), Implicit(), Person()];
  print(json.encode(data));
}
lrhn commented 4 years ago

That's a very different feature than static extension methods.

For this to work, the call to toJson inside the JSON encoder (part of dart:convert), which is a dynamic call, must somehow recognize that someone somewhere in the program has added a toJson extension. That's not realistic without actually adding the toJson method to the class itself, which is not what static extensions methods do. (It also makes it very easy to have conflicts because such a class extension would not be scoped)..

What you can do is pass an toEncodable to the encoder:

print(json.encode(data,toEncodable: (dynamic o) {
  if (o is Implicit) return o.toJson();
  return o.toJson();
});
ykmnkmi commented 4 years ago

It will be convenient for implementing interfaces in type checking condition expression, whereType, ... .

import 'dart:convert';

abstract class Serializable {
  Map<String, Object> toMap();
}

class Explicit implements Serializable {
  @override
  Map<String, Object> toMap() => {'type' : 'Explicit'};
}

class Implicit {
  Map<String, Object> toMap() => {'type' : 'Implicit'};
}

extension ImplicitSerializable on Implicit implements Serializable {
  @override
  Map<String, Object> toMap() => this.toMap();
}

class NoImplicit {}

extension NoImplicitSerializable on NoImplicit implements Serializable {
  @override
  Map<String, Object> toMap() => ... ;
}

void main() {
  var data = <Serializable>[Explicit(), Implicit(), NoImplicit()];
  print(json.encode(data, toEncodable: (dynamic o) {
    if (o is Serializable) return o.toMap();
    return o.toJson();
  }));
}
lrhn commented 4 years ago

@tatumizer This is not on-topic for this question, but ... Did consider it, do not intend to make it an actual wrapper object. The idea with static extensions is that there is no wrapper. Even if we allow you to use the extension as a type (#397), which would allow you to write ExtensionName(object) as an expression, but just meaning the same as object as ExtensionName, it would still just be a static alias which changes method lookup, not a run-time wrapper that affects run-time type or dynamic invocations. If you need a wrapper, you have to write one. (We could then discuss adding syntax for more easily creating wrappers which forward methods).

eernstg commented 4 years ago

Tempted. ;-)

We could use something like #631 to add an implementation of each member that isn't implemented otherwise, such that it will invoke the extension methods:

template mixin Forward<X> {
  final X forwardee;
  template R get g => forwardee.g;
  template R m<T>(P) => forwardee.m<T>(P);
  template set s(P) => forwardee.s = P;
}

class Serializable {...}
class Implicit {...}
extension ImplicitSerializable on Implicit {...}

class WrapImplicitAsSerializable with Forward<Implicit>
    implements Implicit, Serializable {
  final Implicit forwardee;
  implicit WrapImplicitAsSerializable(this.forwardee);
  // All unimplemented members are generated according to the templates.
}

An instance of WrapImplicitAsSerializable would invoke any methods in the interface of Implicit by a forwarding call, and it would call extension methods for the rest, if available. Otherwise there's a compile-time error that we could eliminate by adding missing members to ImplicitSerializable, or by adding yet another extension on a type that Implicit matches.

[Edit Dec 13] We could use an implicit constructor (taking a tiny snippet of the support for implicits from Scala: When type T is expected and an expression e of type S provided, if S is not a subtype of T and not dynamic, we search for constructors marked implicit yielding an object of a type T1 <: T, disambiguate if there are several, and then choose one, say C(_), and then replace e by C(e)). This would allow any Implicit to be used where a Serializable is expected, and it would automatically be wrapped such that it actually implements Serializable.

So even though a static extension does not involve wrapper objects, we could have some other mechanism that allows us to specify wrapper objects.

eernstg commented 4 years ago

should be as simple as adding implements Foo

That would be convenient indeed. If we have template mixins we may still want to specify that this usage of implements Foo is syntactic sugar for a declaration like WrapImplicitAsSerializable, to maintain semantic consistency (as opposed to having two subtly different mechanisms for generating missing methods).

Syntactic consistency won't hurt either. :-)

Adding it up, we'd have consistent consistency! We can't allow that, of course. ;-)

roman-vanesyan commented 4 years ago

Same question was also discussed in #475

stephenbunch commented 4 years ago

Swift can do it!

protocol TextRepresentable {
    var textualDescription: String { get }
}

extension Dice: TextRepresentable {
    var textualDescription: String {
        return "A \(sides)-sided dice"
    }
}

https://docs.swift.org/swift-book/LanguageGuide/Protocols.html#ID277

I'm running into this problem because Dart protos have poor mixin support, so it's very difficult to abstract above the GeneratedMessage interface without resorting to wrappers everywhere.

If extensions could implement interfaces, I'd be able to define a common interface and attach the GeneratedMessage specific implementation as an extension while being able to write other kinds of objects that implement the same interface, but aren't actually a GeneratedMessage.

rmtmckenzie commented 4 years ago

Just thought I'd chime in with a potential use-case for this:

abstract class Freeable {
  void free();
}

extension on FreeableMemoryArray on MemoryArray with Freeable {
 // MemoryArray already has a 'free' function, it would be nice to allow this to work
}

extension on Pointer with Freeable {
  void free() {
    allocation.free(this);
  }
}

// then I could write something like this:
R autoFree<T extends Freeable, R>(T t, R Function(T t) operation) {
  try {
    return operation(t);
  } finally {
    t.free();
  }
}

(of course, if dart supported some sort of native autoFree/using/etc syntax which I think there's another issue about, this wouldn't be necessary, but there's other use-cases where it would still be useful).

kasperpeulen commented 3 years ago

Aren't those called type classes? :)

https://en.wikipedia.org/wiki/Type_class

Guang1234567 commented 3 years ago

@dart-dev

comment at 2021/08 ....๐Ÿ˜“

Today, i just want to implement the Quick Check(also named random test in fact) library for dart, but due to the limit of the dart's extension, so can't implement it !

please support


// example
// the `StringArbitraryExt` (extension name) is redundant.
extension StringArbitraryExt on String implements Arbitrary<String> {
       //....
}

class Arbitrary<T> {
    static T arbitrary();
}

extension StringArbitraryExt on String implements Arbitrary<String> {

static String arbitrary() {
        return "TODO: generate random string for testing here !";
}

// usage
void check<A extends Arbitrary<A>>({required String message, required int size= 100, required bool Function(A) prop})  {
      for (int i = 0; i < size; i++) {
        A value = A.arbitrary();
        if (!prop(value)) {
            A smallerValue = iterateWhile(
                   condition: (A randomA) => !prop(randomA),
                   initialValue: value,
                   next: (A randomA) => randomA.smaller()
            );
            print("\"${message}\" doesn't hold: ${smallerValue}");
            return;
        }
    }
    print("\"${message}\" passed ${size} tests.");
}

// x is a random String
check(message: "XXX should behave like YYY",  prop: (String x) =>  qSort(x) == stdLib.sort(x) );

BTW, swift can do it. Below code is reimplemented by swiflang, protocol just like a interface.

import Foundation

func tabulate<A>(times: Int, f: (Int) -> A) -> [A] {
    Array(0..<times).map(f)
}

func iterateWhile<A>(condition: (A) -> Bool, initialValue: A,
                     next: (A) -> A?) -> A {
    if let x = next(initialValue) {
        if condition(x) {
            return iterateWhile(condition: condition, initialValue: x, next: next)
        }
    }
    return initialValue
}

extension Character {
    func toInt() -> Int {
        var intFromCharacter: Int = 0
        for scalar in String(self).unicodeScalars {
            intFromCharacter = Int(scalar.value)
        }
        return intFromCharacter
    }
}

public protocol Smaller {
    func smaller() -> Self?
}

public protocol Arbitrary {
    static func arbitrary() -> Self
}

extension Int: Arbitrary {
    public static func arbitrary() -> Int {
        Int.random(in: Int.min...Int.max)
    }
}

extension Int: Smaller {
    public func smaller() -> Int? {
        self == 0 ? nil : self / 2
    }
}

extension Character: Arbitrary {
    public static func arbitrary() -> Character {
        let start: Int = ("A" as Character).toInt()
        let end: Int = ("Z" as Character).toInt()
        return Character(UnicodeScalar(Int.random(in: start...end)) ?? "A")
    }
}

extension Character: Smaller {
    public func smaller() -> Character? {
        nil
    }
}

extension String: Arbitrary {
    public static func arbitrary() -> String {
        let randomLength = Int.random(in: 0...40)
        let randomCharacters = tabulate(times: randomLength) { _ in
            Character.arbitrary()
        }

        return randomCharacters.reduce("") {
            $0 + String($1)
        }
    }
}

extension String: Smaller {
    public func smaller() -> String? {
        self.isEmpty ? nil : String(self.dropFirst())
    }
}

extension Array: Arbitrary where Element: Arbitrary {
    public static func arbitrary() -> [Element] {
        let randomLength = Int.random(in: 0...50)
        return tabulate(times: randomLength) { _ in
            Element.arbitrary()
        }
    }
}

extension Array: Smaller where Element: Arbitrary {
    public func smaller() -> [Element]? {
        self.isEmpty ? nil : Array(self.dropFirst())
    }
}

public func check<A: Arbitrary & Smaller>(message: String, size: Int = 100, prop: (A) -> Bool) -> () {
    for _ in 0..<size {
        let value = A.arbitrary()
        if !prop(value) {
            let smallerValue = iterateWhile(condition: { !prop($0) }, initialValue: value) {
                $0.smaller()
            }
            print("\"\(message)\" doesn't hold: \(smallerValue)")
            return
        }
    }
    print("\"\(message)\" passed \(size) tests.")
}

public func check<A: Arbitrary & Smaller, B: Arbitrary & Smaller>(message: String, size: Int = 100, prop: (A, B) -> Bool) -> () {
    for _ in 0..<size {
        let value0 = A.arbitrary()
        let value1 = B.arbitrary()
        if !prop(value0, value1) {
            let smallerValue0 = iterateWhile(condition: { !prop($0, value1) }, initialValue: value0) {
                $0.smaller()
            }

            let smallerValue1 = iterateWhile(condition: { !prop(smallerValue0, $0) }, initialValue: value1) {
                $0.smaller()
            }

            print("\"\(message)\" doesn't hold: (\(smallerValue0), \(smallerValue1))")
            return
        }
    }
    print("\"\(message)\" passed \(size) tests.")
}

public func check<A: Arbitrary & Smaller, B: Arbitrary & Smaller, C: Arbitrary & Smaller>(message: String, size: Int = 100, prop: (A, B, C) -> Bool) -> () {
    for _ in 0..<size {
        let value0 = A.arbitrary()
        let value1 = B.arbitrary()
        let value2 = C.arbitrary()
        if !prop(value0, value1, value2) {
            let smallerValue0 = iterateWhile(condition: { !prop($0, value1, value2) }, initialValue: value0) {
                $0.smaller()
            }

            let smallerValue1 = iterateWhile(condition: { !prop(smallerValue0, $0, value2) }, initialValue: value1) {
                $0.smaller()
            }

            let smallerValue2 = iterateWhile(condition: { !prop(smallerValue0, smallerValue1, $0) }, initialValue: value2) {
                $0.smaller()
            }

            print("\"\(message)\" doesn't hold: (\(smallerValue0), \(smallerValue1), \(smallerValue2))")
            return
        }
    }
    print("\"\(message)\" passed \(size) tests.")
}

public func check<A: Arbitrary & Smaller, B: Arbitrary & Smaller, C: Arbitrary & Smaller, D: Arbitrary & Smaller>(message: String, size: Int = 100, prop: (A, B, C, D) -> Bool) -> () {
    for _ in 0..<size {
        let value0 = A.arbitrary()
        let value1 = B.arbitrary()
        let value2 = C.arbitrary()
        let value3 = D.arbitrary()
        if !prop(value0, value1, value2, value3) {
            let smallerValue0 = iterateWhile(condition: { !prop($0, value1, value2, value3) }, initialValue: value0) {
                $0.smaller()
            }

            let smallerValue1 = iterateWhile(condition: { !prop(smallerValue0, $0, value2, value3) }, initialValue: value1) {
                $0.smaller()
            }

            let smallerValue2 = iterateWhile(condition: { !prop(smallerValue0, smallerValue1, $0, value3) }, initialValue: value2) {
                $0.smaller()
            }

            let smallerValue3 = iterateWhile(condition: { !prop(smallerValue0, smallerValue1, smallerValue2, $0) }, initialValue: value3) {
                $0.smaller()
            }

            print("\"\(message)\" doesn't hold: (\(smallerValue0), \(smallerValue1), \(smallerValue2), \(smallerValue3))")
            return
        }
    }
    print("\"\(message)\" passed \(size) tests.")
}

public func qsort(_ array: [Int]) -> [Int] {
    if array.isEmpty {
        return []
    }
    var arr = array
    let pivot = arr.removeFirst()
    let lesser = arr.filter {
        $0 < pivot
    }
    let greater = arr.filter {
        $0 >= pivot
    }
    return qsort(lesser) + [pivot] + qsort(greater)
}

check(message: "qsort should behave like sort") { (x: [Int]) in
    qsort(x) == x.sorted(by: <)
}
MarcelGarus commented 3 years ago

@Guang1234567 You can work around that problem by passing a value argument additionally to the type argument to functions.

For example, if you have an interface and a function that look like this:

abstract class Bar {
  void bar();
}
void foo<T: Bar>(T a) {
  a.bar();
}

In languages like Rust, you can implement Bar even for built-in or library-external types like int. In Dart, you can instead emulate this behavior by passing an implementation for the type for Bar into the function as well:

abstract class BarImpl<T> {
  void bar(T a);
}
void foo<T>(T a, BarImpl<T> impl) {
  impl.bar(a);
}

This code example is equivalent to the first except that you can also implement BarImpl for other types as well:

class IntBarImpl extends BarImpl<int> {
  void bar(int a) => ...;
}

And then use the function like this:

foo(42, IntBarImpl());

Btw: Purely dynamic functional languages usually work like this. Because there's no type system for associating code with values, behavior needs to be passed around as values as well.

Btw 2: I wrote a property-based testing framework called glados. You might be interested in how I implemented this pattern there.

Guang1234567 commented 3 years ago

I donโ€˜t like the traditional unit test framework or library, event though it puts on the FP's new clothes.

introduce of QuickyCheck is here.

And above QuickyCheck library example just in purpose to explain the requirement of

MarcelGarus commented 3 years ago

@Guang1234567

My glados package is in fact a property-based testing framework like QuickCheck, not a traditional unit testing framework. Have a look at it here.

And as the existence of glados proves, it's certainly possible to (imo beautifully) work around the problem of not having interfaces and static methods on extensions.

Also, I described above how every use case that involves an intricate type system like that can be solved without one. So there is no use case that's impossible to solve currently, only perhaps extra parameters need to be passed at the place of code where the type is specified.

Guang1234567 commented 3 years ago

@marcelgarus

I see, thanks a lot.

Just as glados#how-to-write-generators said:

class User {
  final String name;
  final int age;
}

extension AnyUser on Any {
  Generator<User> get user => combine3(any.string, any.int, (name, age) {
    return User(name, age);
  });
}

enum Ripeness { ripe, unripe }

extension AnyRipeness on Any {
  Generator<Ripeness> get ripeness => choose(Ripeness.values);
}

Create a global instance Any any and extension Generator<User> get xxx => ... on Any to provide Arbitrary for built-in type and custom type.

It's a wonderful solution for working around the problem of not having interfaces and static methods on extensions ๐Ÿ‘ .