Last active
September 10, 2025 08:19
-
-
Save eernstg/27aa5023d6bfbc1a68cd1ef78daf970a to your computer and use it in GitHub Desktop.
Emulate the monoid example from the talk by Brian Goetz about Java type classes (JVMLS 2025)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // This library shows how metaobjects in Dart can be used to achieve a | |
| // similar effect as the Java type class proposal in the talk by Brian | |
| // Goetz. The point is that we can use types (no need to have an instance) | |
| // to work with "static-like" properties like the `zero` value and the | |
| // `plus` operation of a `Monoid`. | |
| // | |
| // No attempt is made to include the syntactic sugar than allows | |
| // `witness.plus(a, b)` to be written as `a + b`. That's a separate | |
| // consideration, and it might be useful with metaobjects as well as | |
| // in other contexts. So that's a topic for another day, and the code | |
| // below simply uses the syntax `witness.plus(a, b)`. | |
| // | |
| // So what does this example achieve? It shows that it is possible to write | |
| // generic code like `computeSum` where the type argument is used to get | |
| // access to an instance of `Monoid<X>` which has the defining elements of | |
| // the monoid (that is, `zero` and `plus`), which allows us to perform | |
| // computations in the monoid without depending on which monoid it is. | |
| // As examples, we're using this to add two strings in the first invocation | |
| // of `computeSum` in `main`, and add two `Optional<String>` instances in | |
| // the second invocation. | |
| // | |
| // The underlying mechanism is the `metaobjects` proposal, see | |
| // https://github.com/dart-lang/language/blob/main/working/4200-metaobjects/feature-specification.md, | |
| // which is used to obtain a "metaobject" from the given type `X`, which | |
| // is then also an instance of `Monoid<X>`. The crucial point is that | |
| // we're able to obtain a `Monoid<Y>` based on the type `Optional<Y>`, | |
| // which means that we have to decompose the type `Optional<Y>` and | |
| // get access to the type argument `Y`, and use that to obtain a | |
| // `Monoid<Y>`, and then use that to build a `Monoid<Optional<Y>>`. | |
| // This is not possible in current Dart, but it is at the core of the | |
| // `metaobjects` proposal. | |
| // | |
| // One thing should be noted: This example does _not_ assume that we will | |
| // be able to change the `String` class (to give it a suitable clause of | |
| // the form `static implements T` where `T` is a subtype of `Monoid<String>`). | |
| // On the contrary, it demonstrates that we can handle a set of "well-known" | |
| // types (including `String` and other widely used types), and we can handle | |
| // combinations of those well-known types and other types (and the other | |
| // types do have the `static implements` clause such that they can be used | |
| // directly based on metaobjects). The support for "well-known" types causes | |
| // the types to be less safe (because we can't just require that the types | |
| // and type arguments always satisfy constraints like | |
| // `static extends Monoidable<T>` for any particular `T`). So that's a trade-off: | |
| // `getMonoid` can return null because of this; if we had not supported `String` | |
| // and its well-known brethren then the types could have been fully checked | |
| // statically. | |
| // ------------------------------ Optional. | |
| sealed class Optional<X> static extends OptionalMonoidBuilder<X> { | |
| const Optional(); | |
| const factory Optional.some(X x) = Some; | |
| const factory Optional.none() = None; | |
| } | |
| final class Some<X> extends Optional<X> { | |
| final X value; | |
| const Some(this.value); | |
| String toString() => 'Some($value)'; | |
| } | |
| final class None extends Optional<Never> { | |
| const None(); | |
| String toString() => "None"; | |
| } | |
| class OptionalMonoidBuilder<X> extends MonoidBuilder<Optional<X>, X> { | |
| Monoid<Optional<X>> monoid(Monoid<X> subMonoid) => MonoidImpl<Optional<X>>( | |
| Optional.some(subMonoid.zero), | |
| (Optional<X> a, Optional<X> b) => switch ((a, b)) { | |
| (None(), _) || (_, None()) => const None(), | |
| (Some<X>(value: final aa), Some<X>(value: final bb)) => | |
| Optional.some(subMonoid.plus(aa, bb)), | |
| }, | |
| ); | |
| } | |
| // ------------------------------ Monoid. | |
| sealed class Monoidable<X> {} | |
| abstract class Monoid<X> implements Monoidable<X> { | |
| X get zero; | |
| X plus(X a, X b); | |
| } | |
| class MonoidImpl<X> implements Monoid<X> { | |
| final X zero; | |
| final X Function(X, X) _plus; | |
| const MonoidImpl(this.zero, this._plus); | |
| X plus(X a, X b) => _plus(a, b); | |
| } | |
| abstract class MonoidBuilder<X, Y /*static extends Monoidable<Y>*/> | |
| implements Monoidable<X> { | |
| Monoid<X> monoid(Monoid<Y> y); | |
| Monoid<Y>? get subMonoid => getMonoid<Y>(); | |
| } | |
| Monoid<X>? getMonoid<X /*static extends Monoidable<X>*/>() { | |
| // Well-known types. | |
| if (X == String) { | |
| return stringMonoid as Monoid<X>; | |
| } else if (<X>[] is List<Iterable<Object?>>) { | |
| // ... somehow extract the type argument. | |
| } else { | |
| // ... whatever else is "well-known". | |
| } | |
| // Types with built-in support. | |
| final meta = X(); | |
| if (meta is Monoid<X>) return meta; | |
| if (meta is MonoidBuilder<X, Object?>) { | |
| final meta2 = meta.subMonoid; | |
| return meta2 == null ? null : meta.monoid(meta2); | |
| } | |
| return null; | |
| } | |
| // ------------------------------ "Well-known" types. | |
| String _stringPlus(String a, String b) => '$a$b'; | |
| const Monoid<String> stringMonoid = MonoidImpl("", _stringPlus); | |
| // ------------------------------ Example. | |
| X computeSum<X static extends Monoidable<X>>(X a, X b) { | |
| final monoid = getMonoid<X>()!; | |
| return monoid.plus(a, b); | |
| } | |
| void main() { | |
| final x = computeSum("Hello, ", "world!"); | |
| print(x); // 'Hello, world!'. | |
| final y = computeSum( | |
| Optional.some("Hello, "), | |
| Optional.some("world!"), | |
| ); | |
| print(y); // 'Some(Hello, world!)'. | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Same example as `monoid.dart`, but modified such that it can be | |
| // executed (`monoid.dart` cannot run because it relies on the metaobjects | |
| // feature, which hasn't been added to the language yet). | |
| // ------------------------------ Emulate metaobject. | |
| // Can't do the general case, just cover the cases we actually have. | |
| Monoidable<X> emulateMeta<X>() { | |
| switch (X) { | |
| case const (String): | |
| return stringMonoid as Monoid<X>; | |
| case const (Optional<String>): | |
| return OptionalMonoidBuilder<String>() as Monoidable<X>; | |
| default: | |
| throw ArgumentError("Unsupported case: $X"); | |
| } | |
| } | |
| // ------------------------------ Optional. | |
| sealed class Optional<X> /*static extends OptionalMonoidBuilder<X>*/ { | |
| const Optional(); | |
| const factory Optional.some(X x) = Some; | |
| const factory Optional.none() = None; | |
| } | |
| final class Some<X> extends Optional<X> { | |
| final X value; | |
| const Some(this.value); | |
| String toString() => 'Some($value)'; | |
| } | |
| final class None extends Optional<Never> { | |
| const None(); | |
| String toString() => "None"; | |
| } | |
| class OptionalMonoidBuilder<X> extends MonoidBuilder<Optional<X>, X> { | |
| Monoid<Optional<X>> monoid(Monoid<X> subMonoid) => MonoidImpl<Optional<X>>( | |
| Optional.some(subMonoid.zero), | |
| (Optional<X> a, Optional<X> b) => switch ((a, b)) { | |
| (None(), _) || (_, None()) => const None(), | |
| (Some<X>(value: final aa), Some<X>(value: final bb)) => | |
| Optional.some(subMonoid.plus(aa, bb)), | |
| }, | |
| ); | |
| } | |
| // ------------------------------ Monoid. | |
| sealed class Monoidable<X> {} | |
| abstract class Monoid<X> implements Monoidable<X> { | |
| X get zero; | |
| X plus(X a, X b); | |
| } | |
| class MonoidImpl<X> implements Monoid<X> { | |
| final X zero; | |
| final X Function(X, X) _plus; | |
| const MonoidImpl(this.zero, this._plus); | |
| X plus(X a, X b) => _plus(a, b); | |
| } | |
| abstract class MonoidBuilder<X, Y /*static extends Monoidable<Y>*/> | |
| implements Monoidable<X> { | |
| Monoid<X> monoid(Monoid<Y> y); | |
| Monoid<Y>? get subMonoid => getMonoid<Y>(); | |
| } | |
| Monoid<X>? getMonoid<X /*static extends Monoidable<X>*/>() { | |
| // Well-known types. | |
| if (X == String) { | |
| return stringMonoid as Monoid<X>; | |
| } else if (<X>[] is List<Iterable<Object?>>) { | |
| // ... import `dart_internal` and extract the type argument. | |
| } else { | |
| // ... whatever else is "well-known". | |
| } | |
| // Types with built-in support. | |
| final meta = /*X()*/ emulateMeta<X>(); | |
| if (meta is Monoid<X>) return meta; | |
| if (meta is MonoidBuilder<X, Object?>) { | |
| final meta2 = meta.subMonoid; | |
| return meta2 == null ? null : meta.monoid(meta2); | |
| } | |
| return null; | |
| } | |
| // ------------------------------ "Well-known" types. | |
| String _stringPlus(String a, String b) => '$a$b'; | |
| const Monoid<String> stringMonoid = MonoidImpl("", _stringPlus); | |
| // ------------------------------ Example. | |
| X computeSum<X /*static extends Monoidable<X>*/>(X a, X b) { | |
| final monoid = getMonoid<X>()!; | |
| return monoid.plus(a, b); | |
| } | |
| void main() { | |
| final x = computeSum("Hello, ", "world!"); | |
| print(x); // 'Hello, world!'. | |
| final y = computeSum( | |
| Optional.some("Hello, "), | |
| Optional.some("world!"), | |
| ); | |
| print(y); // 'Some(Hello, world!)'. | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment