Skip to content

Instantly share code, notes, and snippets.

@eernstg
Last active February 25, 2025 14:52
Show Gist options
  • Save eernstg/efd05f8c257e33c10cb413d0ac52c236 to your computer and use it in GitHub Desktop.
Save eernstg/efd05f8c257e33c10cb413d0ac52c236 to your computer and use it in GitHub Desktop.
'Traits for Dart': How would we do it using 'more capable type objects'? See the first comment for some background.
/*
This is the basic approach, showing how the examples in 'Traits for Dart'
can be written using 'More capable type objects'. The main differences are
that the 'traits' approach can add an implementation to an existing type
(say, `String`) whereas the 'type objects' approach supports late binding.
*/
// --- Glue classes.
class Writer {
void writeString(String s) => print('"$s"');
void writeList<X extends Serializable>(List<X> xs) {
for (final x in xs) x.writeTo(this);
}
}
class Reader {
final List<String> strings;
var _index = 0;
Reader(this.strings);
bool get hasNext => _index < strings.length;
String readString() => strings[_index++];
}
// --- Example code from the document 'Traits in Dart'.
abstract class Serializable {
void writeTo(Writer w);
}
abstract class Deserializable<Self> {
Self readFrom(Reader r);
}
class B<T extends Serializable> implements Serializable
static implements Deserializable<B<T>> {
final T value;
B(this.value);
factory B.readFrom(Reader r) => B(T.readFrom(r));
@override
void writeTo(Writer w) => value.writeTo(w);
@override
String toString() => 'B<$T>($value)';
}
// Use a `Serializable` wrapper class to handle strings.
class String2 implements Serializable
/*static implements Deserializable<String2>*/ {
final String value;
const String2(this.value);
void writeTo(Writer w) => w.writeString('"$value"');
String toString() => '"$value"';
}
// Use a `Serializable` wrapper class to handle lists.
class List2<E extends Serializable> implements Serializable
/*static implements Deserializable<List2<E>>*/ {
final List<E> value;
const List2(this.value);
void writeTo(Writer w) {
for (final element in value) {
element.writeTo(w);
}
}
String toString() => value.toString();
}
// --- Use it!
void main() {
final source1 = ['Hello, world!'];
final object1 = B<String2>.readFrom(Reader(source1));
print('Source: "$source1"');
print('Deserialized: $object1');
print('Reserialized:');
object1.writeTo(Writer());
final source2 = ['one', 'two'];
final object2 = B<List2<String2>>.readFrom(Reader(source2));
print('Source: "$source2"');
print('Deserialized: $object2');
print('Reserialized:');
object2.writeTo(Writer());
}
/*
Same as 'traits_as_capable_type_objects.dart', but desugared such that
the details of the semantics can be inspected. Running code.
*/
// --- Glue classes.
class Writer {
void writeString(String s) => print('"$s"');
void writeList<X extends Serializable>(List<X> xs) {
for (final x in xs) x.writeTo(this);
}
}
class Reader {
final List<String> strings;
var _index = 0;
Reader(this.strings);
bool get hasNext => _index < strings.length;
String readString() => strings[_index++];
}
// --- Example code from the document 'Traits in Dart'.
// Single-line comments show code from the document, code which is
// not compilable uses /*...*/.
// trait Trait on Object {
// T0 method(T1 .. Tn) {...} // May use `Self`.
// }
/*
abstract class Trait<Self extends Object> {
T0 method(T1 .. Tn) {...}
}
late List<Trait> x; // OK.
late Trait t; // OK.
class B<T extends Trait> {...} // OK.
void foo<T extends Trait> {...} // OK.
*/
// trait Serializable {
// void writeTo(Writer w);
// static Self readFrom(Reader r);
// }
abstract class Serializable {
void writeTo(Writer w);
}
abstract class Deserializable<Self> {
Self readFrom(Reader r);
}
// class B<T implements Serializable> {
// final T value;
// B(this.value);
// factory B.readFrom(Reader r) => B(T.readFrom(r));
// void writeTo(Writer r) => value.writeTo(w);
// }
// // Provide implementation of Serializable for String
// impl Serializable for String {
// void writeTo(Writer w) => w.writeString(this);
// static String readFrom(Reader r) => r.readString();
// }
// // Provide implementation of Serializable for any List<E>,
// // such that E has an implementation of Serializable.
// impl<E implements Serializable> Serializable for List<E> {
// void writeTo(Writer w) => w.writeList(this, (e) => e.writeTo(w));
// }
class B<T extends Serializable> implements Serializable
/*static implements Deserializable<B<T>>*/ {
final T value;
B(this.value);
factory B.readFrom(Reader r) => B(emulateFeature<T>().readFrom(r));
@override
void writeTo(Writer w) => value.writeTo(w);
@override
String toString() => 'B<$T>($value)';
}
class ReifiedTypeForB<T extends Serializable>
implements Type, Deserializable<B<T>> {
B<T> readFrom(Reader r) => B.readFrom(r);
}
// Use a `Serializable` wrapper class to handle strings.
class String2 implements Serializable
/*static implements Deserializable<String2>*/ {
final String value;
const String2(this.value);
void writeTo(Writer w) => w.writeString('"$value"');
String toString() => '"$value"';
}
class ReifiedTypeForString2 implements Type, Deserializable<String2> {
String2 readFrom(Reader r) => String2(r.readString());
}
// Use a `Serializable` wrapper class to handle lists.
class List2<E extends Serializable> implements Serializable
/*static implements Deserializable<List2<E>>*/ {
final List<E> value;
const List2(this.value);
void writeTo(Writer w) {
for (final element in value) {
element.writeTo(w);
}
}
String toString() => value.toString();
}
class ReifiedTypeForList2<E extends Serializable>
implements Type, Deserializable<List2<E>> {
List2<E> readFrom(Reader r) {
final contents = <E>[];
final result = List2(contents);
final reifiedType = emulateFeature<E>();
while (r.hasNext) {
contents.add(reifiedType.readFrom(r));
}
return result;
}
}
// Emulate the proposed feature by replacing the given reified type
// by an instance that has the members that the feature would add.
Deserializable<X> emulateFeature<X>() {
Object reifiedType = X;
reifiedType = switch (X) {
const (String2) => ReifiedTypeForString2(),
const (B<String2>) => ReifiedTypeForB<String2>(),
const (List2<String2>) => ReifiedTypeForList2<String2>(),
_ => throw "Unsupported type $reifiedType",
};
return reifiedType as Deserializable<X>;
}
// --- Use it!
void main() {
final source1 = ['Hello, world!'];
final object1 = B<String2>.readFrom(Reader(source1));
print('Source: "$source1"');
print('Deserialized: $object1');
print('Reserialized:');
object1.writeTo(Writer());
final source2 = ['one', 'two'];
final object2 = B<List2<String2>>.readFrom(Reader(source2));
print('Source: "$source2"');
print('Deserialized: $object2');
print('Reserialized:');
object2.writeTo(Writer());
}
/*
Variant of 'traits_as_capable_type_objects.dart' that uses extensions to
provide support for `Deserializable<...>`, even with `String` and `List`
that we can't edit.
*/
// --- Glue classes.
class Writer {
void writeString(String s) => print('"$s"');
void writeList<X extends Serializable>(List<X> xs) {
for (final x in xs) x.writeTo(this);
}
}
class Reader {
final List<String> strings;
var _index = 0;
Reader(this.strings);
bool get hasNext => _index < strings.length;
String readString() => strings[_index++];
}
abstract class Serializable {
void writeTo(Writer w);
}
abstract class Deserializable<Self> {
Self readFrom(Reader r);
}
class B<T> implements Serializable static implements Deserializable<B<T>> {
final T value;
B(this.value);
factory B.readFrom(Reader r) =>
B(Type.reify<T, Deserializable<T>>()!.readFrom(r));
@override
void writeTo(Writer w) => _doWriteTo(w, value);
@override
String toString() => 'B<$T>($value)';
}
// Manually support some well-known types.
extension ListDeserialization<E> on List<E> {
static extends _DeserializableForList<E>;
}
class _DeserializableForList<E> implements Type, Deserializable<List<E>> {
List<E> readFrom(Reader r) {
final result = <E>[];
final deserializable = Type.reify<E, Deserializable<E>>()!;
while (r.hasNext) result.add(deserializable.readFrom(r));
return result;
}
}
extension StringDeserialization on String {
static extends _DeserializableForString;
}
class _DeserializableForString implements Type, Deserializable<String> {
String readFrom(Reader r) => r.readString();
}
// Handle `writeTo` even with `String` and `List`.
void _doWriteTo(Writer w, Object? o) {
if (o is Serializable) {
o.writeTo(w);
} else if (o is String) {
w.writeString(o);
} else if (o is List) {
for (final element in o) {
_doWriteTo(w, element);
}
} else {
throw "The object $o of type ${o.runtimeType} is not Serializable";
}
}
// --- Use it!
void main() {
final source1 = ['Hello, world!'];
final object1 = B<String>.readFrom(Reader(source1));
print('Source: "$source1"');
print('Deserialized: $object1');
print('Reserialized:');
object1.writeTo(Writer());
final source2 = ['one', 'two'];
final object2 = B<List<String>>.readFrom(Reader(source2));
print('Source: "$source2"');
print('Deserialized: $object2');
print('Reserialized:');
object2.writeTo(Writer());
}
/*
Same as 'traits_as_capable_type_objects_extension.dart', but desugared
such that the details of the semantics can be inspected. Running code.
*/
// --- Glue classes.
class Writer {
void writeString(String s) => print('"$s"');
void writeList<X extends Serializable>(List<X> xs) {
for (final x in xs) x.writeTo(this);
}
}
class Reader {
final List<String> strings;
var _index = 0;
Reader(this.strings);
bool get hasNext => _index < strings.length;
String readString() => strings[_index++];
}
// --- Example code from the document 'Traits in Dart'.
// Single-line comments show code from the document, code which is
// not compilable uses /*...*/.
// trait Trait on Object {
// T0 method(T1 .. Tn) {...} // May use `Self`.
// }
/*
abstract class Trait<Self extends Object> {
T0 method(T1 .. Tn) {...}
}
late List<Trait> x; // OK.
late Trait t; // OK.
class B<T extends Trait> {...} // OK.
void foo<T extends Trait> {...} // OK.
*/
// trait Serializable {
// void writeTo(Writer w);
// static Self readFrom(Reader r);
// }
abstract class Serializable {
void writeTo(Writer w);
}
abstract class Deserializable<Self> {
Self readFrom(Reader r);
}
// class B<T implements Serializable> {
// final T value;
// B(this.value);
// factory B.readFrom(Reader r) => B(T.readFrom(r));
// void writeTo(Writer r) => value.writeTo(w);
// }
// // Provide implementation of Serializable for String
// impl Serializable for String {
// void writeTo(Writer w) => w.writeString(this);
// static String readFrom(Reader r) => r.readString();
// }
// // Provide implementation of Serializable for any List<E>,
// // such that E has an implementation of Serializable.
// impl<E implements Serializable> Serializable for List<E> {
// void writeTo(Writer w) => w.writeList(this, (e) => e.writeTo(w));
// }
class B<T> implements Serializable /*static implements Deserializable<B<T>>*/ {
final T value;
B(this.value);
factory B.readFrom(Reader r) =>
B(emulateFeature<T, Deserializable<T>>().readFrom(r));
@override
void writeTo(Writer w) => _doWriteTo(w, value);
@override
String toString() => 'B<$T>($value)';
}
// Implicitly induced by the `static implements` clause on `B`.
class ReifiedTypeForB<T> implements Type, Deserializable<B<T>> {
B<T> readFrom(Reader r) => B.readFrom(r);
}
// Manually support some well-known types.
extension ListDeserialization<E> on List<E> {
/*static extends _DeserializableForList<E>>;*/
}
class _DeserializableForList<E> implements Type, Deserializable<List<E>> {
List<E> readFrom(Reader r) {
final result = <E>[];
final deserializable = emulateFeature<E, Deserializable<E>>();
while (r.hasNext) result.add(deserializable.readFrom(r));
return result;
}
}
// Implicitly induced by the `static extends` clause above.
class ListDeserialization_ReifiedTypeForList<E>
extends _DeserializableForList<E> {}
extension StringDeserialization on String {
/*static extends _DeserializableForString;*/
}
class _DeserializableForString implements Type, Deserializable<String> {
String readFrom(Reader r) => r.readString();
}
class StringDeserialization_ReifiedTypeForString
extends _DeserializableForString {}
// Handle `writeTo` even with `String` and `List`.
void _doWriteTo(Writer w, Object? o) {
if (o is Serializable) {
o.writeTo(w);
} else if (o is String) {
w.writeString(o);
} else if (o is List) {
for (final element in o) {
_doWriteTo(w, element);
}
} else {
throw "The object $o of type ${o.runtimeType} is not Serializable";
}
}
// Emulate the proposed feature by replacing the given reified type
// by an instance that has the members that the feature would add.
// We're only covering the cases that actually occur, because the general
// case cannot be expressed in Dart.
R emulateFeature<X, R>() {
Never die() => throw "The type $X is not deserializable";
final replacement = switch (X) {
const (B<String>) => ReifiedTypeForB<String>(),
const (B<List<String>>) => ReifiedTypeForB<List<String>>(),
const (String) => StringDeserialization_ReifiedTypeForString(),
const (List<String>) => ListDeserialization_ReifiedTypeForList<String>(),
_ => null,
} as R?;
// The feature not available so `X is Deserializable<X>` is false.
if (replacement != null) return replacement; else die();
}
// --- Use it!
void main() {
final source1 = ['Hello, world!'];
final object1 = B<String>.readFrom(Reader(source1));
print('Source: "$source1"');
print('Deserialized: $object1');
print('Reserialized:');
object1.writeTo(Writer());
final source2 = ['one', 'two'];
final object2 = B<List<String>>.readFrom(Reader(source2));
print('Source: "$source2"');
print('Deserialized: $object2');
print('Reserialized:');
object2.writeTo(Writer());
}
/*
Variant of 'traits_as_capable_type_objects.dart' that special cases
certain well-known classes. This implies that the bounds `Serializable`
and `Deserializable<_>` are not used. Instead, it is detected at run
time whether a given object has those types (which allows for using the
members of that interface) or it is one of the special cased types (in
which case that particular type is again special cased).
*/
// --- Glue classes.
class Writer {
void writeString(String s) => print('"$s"');
void writeList<X extends Serializable>(List<X> xs) {
for (final x in xs) x.writeTo(this);
}
}
class Reader {
final List<String> strings;
var _index = 0;
Reader(this.strings);
bool get hasNext => _index < strings.length;
String readString() => strings[_index++];
}
abstract class Serializable {
void writeTo(Writer w);
}
abstract class Deserializable<Self> {
Self readFrom(Reader r);
}
class B<T> implements Serializable static implements Deserializable<B<T>> {
final T value;
B(this.value);
factory B.readFrom(Reader r) => B(_getDeserializable<T>().readFrom(r));
@override
void writeTo(Writer w) => _doWriteTo(w, value);
@override
String toString() => 'B<$T>($value)';
}
class ReifiedTypeForB<T> implements Type, Deserializable<B<T>> {
B<T> readFrom(Reader r) => B.readFrom(r);
}
// Assume `List<E static extends CallWithOneTypeArgument<E>>`.
abstract class CallWithTypeArguments {
int get numberOfTypeArguments;
R callWithTypeArgument<R>(int number, R Function<X>() callback);
}
abstract class CallWithOneTypeArgument<E> implements CallWithTypeArguments {
int get numberOfTypeArguments => 1;
R callWithTypeArgument<R>(int index, R Function<X>() callback) {
if (index != 1) {
throw ArgumentError("Index 1 expected, got $index");
}
return callback<E>();
}
}
// Manually support some well-known types.
class DeserializableForString implements Deserializable<String> {
String readFrom(Reader r) => r.readString();
}
class DeserializableForList<E> implements Deserializable<List<E>> {
List<E> readFrom(Reader r) {
final result = <E>[];
final deserializable = _getDeserializable<E>();
while (r.hasNext) result.add(deserializable.readFrom(r));
return result;
}
}
typedef ListOfString = List<String>; // Can be used as an expression.
Deserializable<X> _getDeserializable<X>() {
Never die() => throw "The type $X is not deserializable";
final Object reifiedType = X;
if (reifiedType is Deserializable<X>) return reifiedType;
if (X == String) {
return DeserializableForString() as Deserializable<X>;
} if (<X>[] is List<List>) {
return X.callWithTypeArgument(1, <E>() => DeserializableForList<E>())
as Deserializable<X>;
} else {
die();
}
}
// Handle `writeTo` even with `String` and `List`.
void _doWriteTo(Writer w, Object? o) {
if (o is Serializable) {
o.writeTo(w);
} else if (o is String) {
w.writeString(o);
} else if (o is List) {
for (final element in o) {
_doWriteTo(w, element);
}
} else {
throw "The object $o of type ${o.runtimeType} is not Serializable";
}
}
// --- Use it!
void main() {
final source1 = ['Hello, world!'];
final object1 = B<String>.readFrom(Reader(source1));
print('Source: "$source1"');
print('Deserialized: $object1');
print('Reserialized:');
object1.writeTo(Writer());
final source2 = ['one', 'two'];
final object2 = B<List<String>>.readFrom(Reader(source2));
print('Source: "$source2"');
print('Deserialized: $object2');
print('Reserialized:');
object2.writeTo(Writer());
}
/*
Same as 'traits_as_capable_type_objects_pragmatic.dart', but desugared
such that the details of the semantics can be inspected. Running code.
*/
// --- Glue classes.
class Writer {
void writeString(String s) => print('"$s"');
void writeList<X extends Serializable>(List<X> xs) {
for (final x in xs) x.writeTo(this);
}
}
class Reader {
final List<String> strings;
var _index = 0;
Reader(this.strings);
bool get hasNext => _index < strings.length;
String readString() => strings[_index++];
}
// --- Example code from the document 'Traits in Dart'.
// Single-line comments show code from the document, code which is
// not compilable uses /*...*/.
// trait Trait on Object {
// T0 method(T1 .. Tn) {...} // May use `Self`.
// }
/*
abstract class Trait<Self extends Object> {
T0 method(T1 .. Tn) {...}
}
late List<Trait> x; // OK.
late Trait t; // OK.
class B<T extends Trait> {...} // OK.
void foo<T extends Trait> {...} // OK.
*/
// trait Serializable {
// void writeTo(Writer w);
// static Self readFrom(Reader r);
// }
abstract class Serializable {
void writeTo(Writer w);
}
abstract class Deserializable<Self> {
Self readFrom(Reader r);
}
// class B<T implements Serializable> {
// final T value;
// B(this.value);
// factory B.readFrom(Reader r) => B(T.readFrom(r));
// void writeTo(Writer r) => value.writeTo(w);
// }
// // Provide implementation of Serializable for String
// impl Serializable for String {
// void writeTo(Writer w) => w.writeString(this);
// static String readFrom(Reader r) => r.readString();
// }
// // Provide implementation of Serializable for any List<E>,
// // such that E has an implementation of Serializable.
// impl<E implements Serializable> Serializable for List<E> {
// void writeTo(Writer w) => w.writeList(this, (e) => e.writeTo(w));
// }
class B<T> implements Serializable /*static implements Deserializable<B<T>>*/ {
final T value;
B(this.value);
factory B.readFrom(Reader r) => B(_getDeserializable<T>().readFrom(r));
@override
void writeTo(Writer w) => _doWriteTo(w, value);
@override
String toString() => 'B<$T>($value)';
}
class ReifiedTypeForB<T> implements Type, Deserializable<B<T>> {
B<T> readFrom(Reader r) => B.readFrom(r);
}
// Assume that `List` has `<E static extends CallWithOneTypeArgument<E>>`.
abstract class CallWithTypeArguments {
int get numberOfTypeArguments;
R callWithTypeArgument<R>(int number, R Function<X>() callback);
}
abstract class CallWithOneTypeArgument<E> implements CallWithTypeArguments {
int get numberOfTypeArguments => 1;
R callWithTypeArgument<R>(int index, R Function<X>() callback) {
if (index != 1) {
throw ArgumentError("Index 1 expected, got $index");
}
return callback<E>();
}
}
class ReifiedTypeForList<E> extends CallWithOneTypeArgument<E>
implements Type {}
// Manually support some well-known types.
class DeserializableForString implements Deserializable<String> {
String readFrom(Reader r) => r.readString();
}
class DeserializableForList<E> implements Deserializable<List<E>> {
List<E> readFrom(Reader r) {
final result = <E>[];
final deserializable = _getDeserializable<E>();
while (r.hasNext) result.add(deserializable.readFrom(r));
return result;
}
}
typedef ListOfString = List<String>; // Can be used as an expression.
Deserializable<X> _getDeserializable<X>() {
Never die() => throw "The type $X is not deserializable";
Deserializable<X>? deserializable = emulateFeatureForDeserializable<X>();
if (deserializable != null) return deserializable;
if (X == String) {
return DeserializableForString() as Deserializable<X>;
} if (<X>[] is List<List>) {
final CallWithTypeArguments? caller =
emulateFeatureForCallWithTypeArguments<X>();
if (caller == null) die();
return caller.callWithTypeArgument(
1,
<E>() => DeserializableForList<E>()
) as Deserializable<X>;
} else {
die();
}
}
// Handle `writeTo` even with `String` and `List`.
void _doWriteTo(Writer w, Object? o) {
if (o is Serializable) {
o.writeTo(w);
} else if (o is String) {
w.writeString(o);
} else if (o is List) {
for (final element in o) {
_doWriteTo(w, element);
}
} else {
throw "The object $o of type ${o.runtimeType} is not Serializable";
}
}
// Emulate the proposed feature by replacing the given reified type
// by an instance that has the members that the feature would add.
// We're only covering the cases that actually occur, because the general
// case cannot be expressed in Dart.
Deserializable<X>? emulateFeatureForDeserializable<X>() => switch (X) {
const (B<String>) => ReifiedTypeForB<String>(),
const (B<List<String>>) => ReifiedTypeForB<List<String>>(),
_ => null,
} as Deserializable<X>?;
CallWithTypeArguments? emulateFeatureForCallWithTypeArguments<X>() {
return switch (X) {
const (List<String>) => ReifiedTypeForList<String>(),
_ => null,
};
}
// --- Use it!
void main() {
final source1 = ['Hello, world!'];
final object1 = B<String>.readFrom(Reader(source1));
print('Source: "$source1"');
print('Deserialized: $object1');
print('Reserialized:');
object1.writeTo(Writer());
final source2 = ['one', 'two'];
final object2 = B<List<String>>.readFrom(Reader(source2));
print('Source: "$source2"');
print('Deserialized: $object2');
print('Reserialized:');
object2.writeTo(Writer());
}
@eernstg
Copy link
Author

eernstg commented Feb 20, 2025

I explored the relationship between the approach taken in the document 'Traits for Dart' and the approach which is proposed in 'More capable type objects', illustrated with code in this gist.

It consists of two separate approaches, each of which has a version that uses the proposed feature plus a version which contains code which expressed using current Dart (emulating the special cases of the feature which are actually used in the example).

In the first approach, all type parameters are explicitly declared to satisfy the constraints needed in order to know statically that the objects support Serializable (so they can do void writeTo(Writer)) and the reified types satisfy Deserializable<T> for some T (such that they can do T.readFrom(Reader)). This implies that we can't use system provided types like String and List, because we can't edit those classes such that this is true. So we're using wrapper classes String2 and List2 in order to ensure that the wrapper classes are Serializable and Deserializable<T> as needed.

This approach is used in traits_as_capable_type_objects.dart, and traits_as_capable_type_objects_desugared.dart shows how the former will work using an emulation of the feature which is possible in the current version of Dart.

In the second approach, type parameters are not constrained (that is, they may or may not have a type argument that static implements Deserializable<T> for any T, and they may also not be a subtype of Serializable). This allows us to include system provided types like String and List in the data structure which is being serialized or the type which is being used to guide a deserialization.

Consequently, the second approach relies on run-time type queries (like value is Serializable) in order to determine whether or not a given object or type as opted in to the serialization or deserialization protocol. If they have indeed opted in then the code is just as simple as it was in approach 1. Otherwise, it is tested using a run-time type query whether the given object is one of the well-known special cases (here: String and List). If this is true then each special class is handled directly. If it is not true then an exception is thrown.

The second approach is used in traits_as_capable_type_objects_pragmatic.dart, and traits_as_capable_type_objects_pragmatic_desugared.dart shows how the former will work using an emulation of the feature which is possible in the current version of Dart.

I expect that the second approach is going to be more realistic, because even serialization is probably not going to be important enough to justify that String and List depend on it.

@eernstg
Copy link
Author

eernstg commented Feb 21, 2025

Added a version (*_extension*.dart) which shows how the proposed extension mechanism (see dart-lang/language#4200, section 'More capable type objects as an extension') can be used to equip String and List with the ability to deserialize, even though we don't have edit rights to those classes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment