Skip to content

Union Types #395

Open
Open

Description

@davidmarne

Along with data classes and enum classes, dart also lacks a support for defining custom union types. I think built_value could contain another generator for union types that followed the same principles as enum and data classes. The union class should be:

  • immutable
  • comparable
  • serializable
  • implement toString

I think it would look something like this:

/// how the user defines a union
@union
const myUnion = const <Type>[int, String];

generated in a .g.dart file:

/// the generated union class. It should be immutable
/// but probably not a built_value since it only takes
/// 1 parameter to build a new value.
@immutable
class MyUnion {
  /// every union type has `value` and `type` accessors
  /// `value` must be dynamic, and be casted to `type`
  final dynamic value;

  final Type type;

  /// a typed constructor is generated for each possible type
  MyUnion.int(int value)
      : value = value,
        type = int;

  MyUnion.string(String value)
      : value = value,
        type = String;

  /// a convience function for switching on type
  /// similiar switching on types using Kotlin's when statement.
  T when<T>(
    T intCallback(int value),
    T stringCallback(String value),
  ) {
    switch (type) {
      case int:
        return intCallback(value as int);
      case String:
        return stringCallback(value as String);
      default:
        throw Exception('wtf');
    }
  }

  String toString() => (newBuiltValueToStringHelper('MyUnion')
        ..add('value', value)
        ..add('type', type))
      .toString();

  int hashCode() => $jf($jc($jc(0, value.hashCode), type.hashCode));

  @override
  bool operator ==(dynamic other) {
    if (identical(other, this)) return true;
    if (other is! MyUnion) return false;
    return type == other.type && value == other.value;
  }
}

Activity

  1. davidmarne commented on Apr 14, 2018

    @davidmarne
    Author

    the method for defining a union might be up for debate as you couldn't use types with generics in the List method:

    /// does not compile
    @union
    const myUnion = const <Type>[String, int, Foo<int>, Foo<String>];

    Maybe an alternative could be:

    @union
    abstract class UnionDefinition {
      int get integer;
      String get string;
      Foo<int> get fooInt;
      Foo<String> get fooString;
    }

    But that is debatable. Not sure the use case is strong enough to merit the more awkward syntax.

  2. self-assigned this
    on Apr 16, 2018
  3. davidmorgan commented on Apr 16, 2018

    @davidmorgan
    Collaborator

    Interesting, thanks. Will give it some thought.

    Does value have to be dynamic? I think I'd prefer it to be Object. You could have asInt, asString getters?

    What if instead of union types, we allow built_value classes to have union fields?

    class Foo implements Built<Foo, FooBuilder> {
      @BuiltValueField(union: 'foo')
      int get foo;
      @BuiltValueField(union: 'foo')
      String get bar;
    }
    

    These would map to the same field internally. Not sure what the rest of the API would look like.

  4. davidmarne commented on Apr 17, 2018

    @davidmarne
    Author

    Yea i think it could be Object, not dynamic, and getters that return they typed value would be a great addition.

    Union fields is an interesting idea, but I'm not quite sure that will work. In your example, I assume at the very least you would want to generate:

    int get foo;
    String get bar;
    bool get isFoo;
    bool get isBar;

    in doing so there would be no way to access isFoo and isBar from an instance of Foo. There is no consistent interface to tell Foo to implement because the set of fields that get generated is dynamic.

    Also, I personally like the idea of a given union being self-contained in a single object rather than being included in a built_value that may include other fields or even other unions. However, that's just my opinion, and I understand going the union fields route if we could figure out how to make the codegen work.

  5. MikeMannox commented on Apr 24, 2018

    @MikeMannox
  6. amnykon commented on Nov 22, 2019

    @amnykon

    I miss swift enums with associated values... I feel that a union would fill the gap, though they can only hold 1 value. Multiple values can be achieved by using a class (hopefully immutable) containing the multiple values.

    I propose to use the following format:
    my_union.dart:

    import 'package:meta/meta.dart';
    import 'package:built_value/built_value.dart';
    
    part 'my_union.g.dart';
    
    class Foo<T> {
      T data;
    }
    
    @union class MyUnion extends _$MyUnion {
      MyUnion.integer(int value): super.integer(value);
      MyUnion.string(String value): super.string(value);
      MyUnion.fooInt(Foo<int> value): super.fooInt(value);
      MyUnion.fooString(Foo<String> value): super.fooString(value);
    }
    

    my_union.g.dart:

    enum _$MyUnionType {
      integer,
      string,
      fooInt,
      fooString,
    }
    
    abstract class _$MyUnion {
      final _$MyUnionType _type;
      final Object _value;
    
      _$MyUnion.integer(int value): _type = _$MyUnionType.integer, _value = value;
      _$MyUnion.string(String value): _type = _$MyUnionType.string, _value = value;
      _$MyUnion.fooInt(Foo<int> value): _type = _$MyUnionType.fooInt, _value = value;
      _$MyUnion.fooString(Foo<String> value): _type = _$MyUnionType.fooString, _value = value;
    
      bool get isInteger => _type == _$MyUnionType.integer;
      bool get isString => _type == _$MyUnionType.string;
      bool get isFooInt => _type == _$MyUnionType.fooInt;
      bool get isFooString => _type == _$MyUnionType.fooString;
    
      int get integer => isInteger ? _value as int : null;
      String get string => isString ? _value as String : null;
      Foo<int> get fooInt => isFooInt ? _value as Foo<int> : null;
      Foo<String> get fooString => isFooString ? _value as Foo<String> : null;
    
      T when<T>({
        @required T Function (int integer) integer,
        @required T Function (String integer) string,
        @required T Function (Foo<int> integer) fooInt,
        @required T Function (Foo<String> integer) fooString,
      }) {
        switch (_type) {
          case _$MyUnionType.integer:
            return integer(this.integer);
          case _$MyUnionType.string:
            return string(this.string);
          case _$MyUnionType.fooInt:
            return fooInt(this.fooInt);
          case _$MyUnionType.fooString:
            return fooString(this.fooString);
          default:
            throw Exception("unknown type");
        }
      }
    
      String toString() => (newBuiltValueToStringHelper('MyUnion')
        ..add('value', _value)
        ..add('type', _typeToString)
      ).toString();
    
      int get hashCode => $jf($jc($jc(0, _value.hashCode), _type.hashCode));
    
      @override
      bool operator ==(dynamic other) {
        if (identical(other, this)) return true;
        if (other is! MyUnion) return false;
        return _type == other._type && _value == other._value;
      }
    
      String _typeToString() => when(
          integer: (_) => "integer",
          string: (_) => "string",
          fooInt: (_) => "fooInt",
          fooString: (_) => "fooString",
      );
    }
    

    I did not include serialization in my solution. This should be added.

    Initializers should be used to determine the generated code as initializers are not inherited and therefore must be specified anyway.

    I did consider having a class for each type, but chose a different approach because each type should not include Union members.

    Support for a variable amount of associated types could be added (including 0) at a later time, removing the need of the Foo class.

  7. MikeMannox commented on Nov 26, 2019

    @MikeMannox

    But you can do that with Sealed Unions?

  8. realcr commented on Jan 16, 2020

    @realcr

    I am also very much interested in this feature. I want to be able to get an interop with my Rust code through json serialization. Here is an example for a Rust enum (Which is like the proposed union here):

    pub enum ServerToUser {
        ResponseOpenNode(ResponseOpenNode),
        NodesStatus(NodesStatus),
        Node(NodeId, CompactToUser), // (node_id, compact_to_user)
    }

    Note that the Rust example above also requires the implementation of tuples, which we currently don't have, but I can definitely live without it for now (The last variant, Node, contains a tuple of (NodeId, CompactToUser)).

    What are the obstacles to getting a union in built_value?
    I looked at what we currently have with enum in built_value, and it seems pretty close to what I would like to have in a union. The only thing missing is the associated types. Maybe we can even extend enum without breaking existing code, to contain associated types?

    Some inspiration from related projects:

    I played with the code of the projects above for the whole day, trying to make them work with built_value, but I didn't get very far. For example, with sum-types, the design of the automatic code-gen goes in a very different direction from the code-gen in built_value, which makes interoperability somewhat difficult.

  9. davidmorgan commented on Jan 17, 2020

    @davidmorgan
    Collaborator

    Enums in built_value aren't a particularly good fit as they don't carry data; I think a built_value class is closer.

    But I don't think either can be directly adapted, it would be an entirely new feature with new support.

    I think the only obstacle is that this would take quite a big time investment :)

  10. realcr commented on Jan 21, 2020

    @realcr

    @davidmorgan : I am willing to give it a try, but I need some help.

    I looked at the code and began some small attempt to implement the suggestion of @amnykon.
    My very general plan is as follows:

    • Add union_source_class.dart and union_source_variant.dart files.
    • Add serializer
    • Add tests
    • Do something with the analyzer plugin? (I am not sure what the analyzer plugin does, and whether or not I should add anything there).

    Of course most of the work goes in the first bullet.

    I believe that there are some major things I don't understand:

    1. I noticed that the built_value_generator package uses its own code generation capabilities to generate .g.dart files for itself. How is this actually working? My guess would be that the code generation has to be done in a certain order, with some bootstrapping hack in the end. I couldn't spot it anywhere in the code.

    2. I am not sure about the correct workflow to make incremental progress. I began by copying:

    • value_source_class.dart -> union_source_class.dart
    • value_source_field.dart -> union_source_variant.dart
      And then began with renaming some classes, just to make sure pub run build_runner build works without errors. This worked at first, but after a while it broke with this error:
    $ pub run build_runner build
    [INFO] Generating build script completed, took 386ms
    [INFO] Creating build script snapshot... completed, took 8.1s
    [SEVERE] Failed to snapshot build script .dart_tool/build/entrypoint/build.dart.
    This is likely caused by a misconfigured builder definition.
    [SEVERE] lib/src/enum_source_library.dart:16:6: Error: Error when reading 'lib/src/enum_source_library.g.dart': No such file or directorypart 'enum_source_library.g.dart';     ^lib/src/value_source_class.dart:20:6: Error: Error when reading 'lib/src/value_source_class.g.dart': No such file or directorypart 'value_source_class.g.dart';     ^lib/src/serializer_source_library.dart:18:6: Error: Error when reading 'lib/src/serializer_source_library.g.dart': No such file or directorypart 'serializer_source_library.g.dart';     ^lib/src/enum_source_class.dart:16:6: Error: Error when reading 'lib/src/enum_source_class.g.dart': No such file or directorypart 'enum_source_class.g.dart';     ^lib/src/fixes.dart:7:6: Error: Error when reading 'lib/src/fixes.g.dart': No such file or directorypart 'fixes.g.dart';     ^lib/src/memoized_getter.dart:9:6: Error: Error when reading 'lib/src/memoized_getter.g.dart': No such file or directorypart 'memoized_getter.g.dart';     ^lib/src/value_source_field.dart:19:6: Error: Error when reading 'lib/src/value_source_field.g.dart': No such file or directorypart 'value_source_field.g.dart';     ^lib/src/serializer_source_class.dart:21:6: Error: Error when reading 'lib/src/serializer_source_class.g.dart': No such file or directorypart 'serializer_source_class.g.dart';     ^lib/src/enum_source_field.dart:14:6: Error: Error when reading 'lib/src/enum_source_field.g.dart': No such file or directorypart 'enum_source_field.g.dart';     ^lib/src/serializer_source_field.dart:17:6: Error: Error when reading 'lib/src/serializer_source_field.g.dart': No such file or directorypart 'serializer_source_field.g.dart';     ^

    What is the usual thing you would do to debug this kind of error? I have no idea where the error comes from, and at this point my only option is to revert to the previous state with my source control, starting all over.

    Is there a way for me to test only the part I am working on? Can I add some debug/log prints to aid with debugging the code-gen I am working on?

    1. Why are the *.g.dart autogenerated files checked into the repository?

    EDIT: I expanded on the build issue above in #807

  11. davidmorgan commented on Jan 21, 2020

    @davidmorgan
    Collaborator

    I suggest comitting locally a lot, so you can easily revert when you run into trouble. Yeah, it's a bit tricky.

    A few tips: 1) run tool/use_local_deps so that the packages depend on your local versions; you'll need this to run the end to end tests. And, 2) use the end to end tests :) ... they're the best way to specify what the generation is supposed to be achieving.

    You're very welcome to look at this, but I'd like to caution that this is likely to be a hard PR to write/accept; I don't have a lot of spare time for built_value, so I can give some pointers to help you but not much more than that; and even if it works, I'm a little hesitant about accepting the change because I'll need to maintain it. e.g. it'll soon need updating to work with None Null By Default.

    That warning aside, I'm very interested to see how it goes :)

    Thanks.

  12. realcr commented on Jan 21, 2020

    @realcr

    I just managed to remove the dependency of built_value_generator on its own codegen. I did this by changing all the Built<...> statements from built_value_generator. I suspect that you might not like the result, but at least at this point I can change things in the code base without being affected by issue #807.

    If you are interested to see what it looks like, you can see this branch:
    https://github.com/freedomlayer/built_value.dart/tree/real/feat/union-class

    Even if you choose not to merge those changes eventually, I will still be happy to use it with my code, because I am very interested in having sum-types. In addition, it could be an interesting experiment about how to implement sum-types that you might be able to utilize later if you ever want to add this feature.

  13. realcr commented on Jan 22, 2020

    @realcr

    I spent some time reading the proposals in this issue, and thinking about reasonable design that will go in harmony with the rest of built_value. I include here my current idea, I haven't yet written the code generator, but the code below actually does compile and pass tests.

    @davidmorgan , @amnykon, @davidmarne , @werediver: Comments and ideas are highly appreciated, it is very likely that I might be missing something.

    EDIT: Updated Builder setters in autogenerated code to discourage the use of .. when building unions. Using a void method instead.

    User code
    library union;
    
    import 'package:built_value/built_value.dart';
    import 'package:built_value/serializer.dart';
    import 'package:meta/meta.dart';
    
    part 'union_g.dart';
    
    class Foo<T> {
      T data;
      Foo(data);
    }
    
    // @Union (?)
    abstract class SimpleUnion implements Built<SimpleUnion, SimpleUnionBuilder> {
      // TODO: Serializer
    
      T match<T>({
          @required T Function (int) integer,
          @required T Function (String) string,
          @required T Function (Foo<int>) fooInt,
          @required T Function (Foo<String>) fooString,
        });
    
      factory SimpleUnion([void Function(SimpleUnionBuilder) updates]) =
          _$SimpleUnion;
      SimpleUnion._();
    }
    Example test code
    // Unions
    var foo = Foo<int>(3);
    final unionValue1 = SimpleUnion((b) => b.fooInt(foo));
    
    var descString = unionValue1.match(
        integer: (_) => 'integer',
        string: (_) => 'string',
        fooInt: (_) => 'fooInt',
        fooString: (_) => 'fooString');
    
    assert(descString == 'fooInt');
    
    // Check equality operator:
    final unionValue2 = SimpleUnion((b) => b.integer(3));
    final unionValue3 = SimpleUnion((b) => b.integer(3));
    
    assert(unionValue2 == unionValue3);
    Automatically generated code
    // GENERATED CODE - DO NOT MODIFY BY HAND
    
    part of union;
    
    // TODO: Serializer
    
    enum _$SimpleUnionType {
      integer,
      string,
      fooInt,
      fooString,
    }
    
    class _$SimpleUnion extends SimpleUnion {
      final _$SimpleUnionType _type;
      final Object _value;
    
      factory _$SimpleUnion([void Function(SimpleUnionBuilder) updates]) =>
          (new SimpleUnionBuilder()..update(updates)).build();
    
      _$SimpleUnion._(this._type, this._value) : super._() {
        if (_type == null) {
          // TODO: Better exception to throw here?
          throw Exception('SimpleUnion: null type');
        }
        if (_value == null) {
          // TODO: Better exception to throw here?
          throw Exception('SimpleUnion: null value');
        }
      }
    
      @override
      T match<T>({
          @required T Function (int) integer,
          @required T Function (String) string,
          @required T Function (Foo<int>) fooInt,
          @required T Function (Foo<String>) fooString,
        }) {
        switch (_type) {
          case _$SimpleUnionType.integer:
            return integer(_value);
          case _$SimpleUnionType.string:
            return string(_value);
          case _$SimpleUnionType.fooInt:
            return fooInt(_value);
          case _$SimpleUnionType.fooString:
            return fooString(_value);
          default:
            // TODO: Better exception to throw here?
            throw Exception('unknown type');
        }
      }
    
      @override
      SimpleUnion rebuild(void Function(SimpleUnionBuilder) updates) =>
          (toBuilder()..update(updates)).build();
    
      @override
      SimpleUnionBuilder toBuilder() => new SimpleUnionBuilder()..replace(this);
    
      @override
      bool operator ==(Object other) {
        if (identical(other, this)) return true;
        return other is SimpleUnion &&
            other.match(
              integer: (otherValue) => (_type == _$SimpleUnionType.integer && _value == otherValue),
              string: (otherValue) => (_type == _$SimpleUnionType.string && _value == otherValue),
              fooInt: (otherValue) => (_type == _$SimpleUnionType.fooInt && _value == otherValue),
              fooString: (otherValue) => (_type == _$SimpleUnionType.fooString && _value == otherValue),
            );
      }
    
      @override
      int get hashCode {
        return $jf($jc($jc(0, _type.hashCode), _value.hashCode));
      }
    
      @override
      String toString() => (newBuiltValueToStringHelper('SimpleUnion')
            ..add('value', _value)
            ..add('type', _type))
          .toString();
    }
    
    class SimpleUnionBuilder implements Builder<SimpleUnion, SimpleUnionBuilder> {
      _$SimpleUnion _$v;
    
      void integer(int value) {
        _$v = _$SimpleUnion._(_$SimpleUnionType.integer, value);
      }
      void string(String value) {
        _$v = _$SimpleUnion._(_$SimpleUnionType.string, value);
      }
      void fooInt(Foo<int> value) {
        _$v = _$SimpleUnion._(_$SimpleUnionType.fooInt, value);
      }
      void fooString(Foo<String> value) {
        _$v = _$SimpleUnion._(_$SimpleUnionType.fooString, value);
      }
    
      SimpleUnionBuilder();
    
      @override
      void replace(SimpleUnion other) {
        if (other == null) {
          throw new ArgumentError.notNull('other');
        }
        _$v = other as _$SimpleUnion;
      }
    
      @override
      void update(void Function(SimpleUnionBuilder) updates) {
        if (updates != null) updates(this);
      }
    
      @override
      _$SimpleUnion build() {
        if (_$v == null) {
          throw new ArgumentError.notNull('SimpleUnion');
        }
        final _$result = _$v;
        replace(_$result);
        return _$result;
      }
    }
  14. davidmorgan commented on Jan 22, 2020

    @davidmorgan
    Collaborator

    Thanks! Looks promising. Just a few notes:

    • I think operator== can be simpler: just check that _type is equal to other._type and _value is equal to other._value
    • the builder could offer nested builders for types that are built_value or union types ... like built_value does. This would complicate things quite a bit, but is interesting to think about :)
  15. 1 remaining item

  16. realcr commented on Jan 22, 2020

    @realcr

    Hi, thanks for the feedback!

    @davidmorgan : Regarding the operator==: I wish so too, but it seems like I can not access other._type, because other is of type SimpleUnion, and not of type _$SimpleUnion.

    About the nested builders: Could you point me to an example of what it means? I have seen this expression in the documentation and repository more than once, but couldn't find it. Maybe it will be possible to add this.

    @amnykon :
    I understand that your main concern is that we should have chosen the opposite design and invert the inheritance. I actually began from your proposed code yesterday, but at some point I realized I do not know how to express something like empty variant or tuple variant using the syntax proposed in your example.

    I made some progress, and this is the current state of user code:

    // @union
    abstract class SimpleUnion implements Built<SimpleUnion, SimpleUnionBuilder> {
      static Serializer<SimpleUnion> get serializer => _$simpleUnionSerializer;
    
      T match<T>({
          @required T Function () empty,
          @required T Function (int) integer,
          @required T Function (int, String) tuple,
          @required T Function (String) string,
          @required T Function (Foo<int>) fooInt,
          @required T Function (Foo<String>) fooString,
        });
    
      factory SimpleUnion([void Function(SimpleUnionBuilder) updates]) =
          _$SimpleUnion;
      SimpleUnion._();
    }

    Note that above, empty means an empty variant, and tuple is a tuple of two items: integer and a string. I remember that you also cared about having something like this. I was wondering if you know of a reasonable way to represent tuple variants and empty variants in the proposed "constructor design".

    EDIT: Thinking about it, maybe it is not so difficult to do, and using the "constructor design" should add the wanted extra ergonomics for is{TypeName} etc. @davidmorgan : Are you cool with a design of inverted inheritance? I am not sure I have enough view at this point to understand the advantages of extending Built<...> over using the inverted inheritance idea proposed by @amnykon.

    As an appendix, I include the full user code, example code and generated code.
    We have everything compiling and the tests pass.

    user code
    library union;
    
    // import 'package:built_collection/built_collection.dart';
    import 'package:built_value/built_value.dart';
    // import 'package:built_value/json_object.dart';
    import 'package:built_value/serializer.dart';
    import 'package:meta/meta.dart';
    
    part 'union_example_g.dart';
    
    
    class Foo<T> {
      T data;
      Foo(data): data = data;
    
      @override
      bool operator ==(Object other) {
        if (identical(other, this)) {
          return true;
        }
        return other is Foo<T> &&
            data == other.data;
      }
    }
    
    final fooSerializer = FooSerializer<int>();
    
    class FooSerializer<T> implements StructuredSerializer<Foo<T>> {
      @override
      final Iterable<Type> types = const [Foo];
      @override
      final String wireName = 'Foo';
    
      @override
      Iterable<Object> serialize(Serializers serializers, Foo<T> object,
          {FullType specifiedType = FullType.unspecified}) {
        return <Object>[serializers.serialize(object.data, specifiedType: FullType(T))];
      }
    
      @override
      Foo<T> deserialize(Serializers serializers, Iterable<Object> serialized,
          {FullType specifiedType = FullType.unspecified}) {
    
        final iterator = serialized.iterator;
        iterator.moveNext();
        return Foo(serializers.deserialize(iterator.current, specifiedType: FullType(T)));
      }
    }
    
    
    
    /// Example of how to use built_value.
    ///
    /// The value class must implement [Built]. It must be abstract, and have
    /// fields declared as abstract getters. Finally, it must have a particular
    /// constructor and factory, as shown here.
    // @union
    abstract class SimpleUnion implements Built<SimpleUnion, SimpleUnionBuilder> {
      /// Example of how to make a built_value type serializable.
      ///
      /// Declare a static final [Serializer] field called `serializer`.
      /// The built_value code generator will provide the implementation. You need
      /// to do this for every type you want to serialize.
      static Serializer<SimpleUnion> get serializer => _$simpleUnionSerializer;
    
      T match<T>({
          @required T Function () empty,
          @required T Function (int) integer,
          @required T Function (int, String) tuple,
          @required T Function (String) string,
          @required T Function (Foo<int>) fooInt,
          @required T Function (Foo<String>) fooString,
        });
    
      factory SimpleUnion([void Function(SimpleUnionBuilder) updates]) =
          _$SimpleUnion;
      SimpleUnion._();
    }
    test code
      // Unions
      var foo = Foo<int>(3);
      final unionValue1 = SimpleUnion((b) => b.fooInt(foo));
    
      var descString = unionValue1.match(
          empty: () => 'empty',
          integer: (_) => 'integer',
          tuple: (_1, _2) => 'tuple',
          string: (_) => 'string',
          fooInt: (_) => 'fooInt',
          fooString: (_) => 'fooString');
    
      assert(descString == 'fooInt');
    
      // Check equality operator:
      final unionValue2 = SimpleUnion((b) => b.integer(3));
      final unionValue3 = SimpleUnion((b) => b.integer(3));
    
      assert(unionValue2 == unionValue3);
    
      // Check tuple:
      final unionValue4 = SimpleUnion((b) => b.tuple(4, 'four'));
    
      var res = unionValue4.match(
          empty: () => [],
          integer: (_) => [],
          tuple: (res_num, res_string) => [res_num, res_string],
          string: (_) => [],
          fooInt: (_) => [],
          fooString: (_) => []);
    
      assert(res[0] == 4);
      assert(res[1] == 'four');
    
      final unionValue5 = SimpleUnion((b) => b.empty());
    Autogenerated code
    // GENERATED CODE - DO NOT MODIFY BY HAND
    
    part of union;
    
    // **************************************************************************
    // BuiltValueGenerator
    // **************************************************************************
    
    Serializer<SimpleUnion> _$simpleUnionSerializer = new _$SimpleUnionSerializer();
    
    class _$SimpleUnionSerializer implements StructuredSerializer<SimpleUnion> {
      @override
      final Iterable<Type> types = const [SimpleUnion, _$SimpleUnion];
      @override
      final String wireName = 'SimpleUnion';
    
      @override
      Iterable<Object> serialize(Serializers serializers, SimpleUnion object,
          {FullType specifiedType = FullType.unspecified}) {
        final result = object.match(
            empty: () => <Object>['empty'],
            integer: (value0) => <Object>[
                  'integer',
                  serializers.serialize(value0, specifiedType: const FullType(int))
                ],
            tuple: (value0, value1) => <Object>[
                  'tuple',
                  <Object>[serializers.serialize(value0, specifiedType: const FullType(int)),
                  serializers.serialize(value1,
                      specifiedType: const FullType(String))]
                ],
            string: (value0) => <Object>[
                  'string',
                  serializers.serialize(value0,
                      specifiedType: const FullType(String))
                ],
            fooInt: (value0) {
              print('value0 = $value0');
              print(value0.data);
              return <Object>[
                'fooInt',
                serializers.serialize(value0, specifiedType: const FullType(Foo))
              ];
            },
            fooString: (value0) => <Object>[
                  'fooString',
                  serializers.serialize(value0, specifiedType: const FullType(Foo))
                ]);
    
        return result;
      }
    
      @override
      SimpleUnion deserialize(Serializers serializers, Iterable<Object> serialized,
          {FullType specifiedType = FullType.unspecified}) {
        final builder = new SimpleUnionBuilder();
    
        final iterator = serialized.iterator;
        iterator.moveNext();
        final key = iterator.current as String;
        iterator.moveNext();
        final Object value = iterator.current;
        switch (key) {
          case 'empty':
            builder.empty();
            break;
          case 'integer':
            iterator.moveNext();
            builder.integer(serializers.deserialize(value,
                specifiedType: const FullType(int)));
            break;
          case 'tuple':
            final iterator = (value as Iterable<Object>).iterator;
            iterator.moveNext();
            final dynamic value0 = iterator.current;
            iterator.moveNext();
            final dynamic value1 = iterator.current;
    
            builder.tuple(
                serializers.deserialize(value0, specifiedType: const FullType(int)),
                serializers.deserialize(value1,
                    specifiedType: const FullType(String)));
            break;
          case 'string':
            builder.integer(serializers.deserialize(value,
                specifiedType: const FullType(String)));
            break;
          case 'fooInt':
            builder.fooInt(serializers.deserialize(value,
                specifiedType: const FullType(Foo)));
            break;
          case 'fooString':
            builder.fooInt(serializers.deserialize(value,
                specifiedType: const FullType(Foo)));
            break;
        }
    
        return builder.build();
      }
    }
    
    enum _$SimpleUnionType {
      empty,
      integer,
      tuple,
      string,
      fooInt,
      fooString,
    }
    
    class _$SimpleUnion extends SimpleUnion {
      final _$SimpleUnionType _type;
      final List<Object> _values;
    
      factory _$SimpleUnion([void Function(SimpleUnionBuilder) updates]) =>
          (new SimpleUnionBuilder()..update(updates)).build();
    
      _$SimpleUnion._(this._type, this._values) : super._() {
        if (_type == null) {
          // TODO: Better exception to throw here?
          throw Exception('SimpleUnion: null type');
        }
        if (_values == null) {
          // TODO: Better exception to throw here?
          throw Exception('SimpleUnion: null value');
        }
      }
    
      @override
      T match<T>({
        @required T Function() empty,
        @required T Function(int) integer,
        @required T Function(int, String) tuple,
        @required T Function(String) string,
        @required T Function(Foo<int>) fooInt,
        @required T Function(Foo<String>) fooString,
      }) {
        switch (_type) {
          case _$SimpleUnionType.empty:
            return empty();
          case _$SimpleUnionType.integer:
            return integer(_values[0]);
          case _$SimpleUnionType.tuple:
            return tuple(_values[0], _values[1]);
          case _$SimpleUnionType.string:
            return string(_values[0]);
          case _$SimpleUnionType.fooInt:
            return fooInt(_values[0]);
          case _$SimpleUnionType.fooString:
            return fooString(_values[0]);
          default:
            // TODO: Better exception to throw here?
            throw Exception('unknown type');
        }
      }
    
      @override
      SimpleUnion rebuild(void Function(SimpleUnionBuilder) updates) =>
          (toBuilder()..update(updates)).build();
    
      @override
      SimpleUnionBuilder toBuilder() => new SimpleUnionBuilder()..replace(this);
    
      @override
      bool operator ==(Object other) {
        if (identical(other, this)) return true;
        return other is SimpleUnion &&
            other.match(
              empty: () => _type == _$SimpleUnionType.empty,
              integer: (otherValue0) =>
                  _type == _$SimpleUnionType.integer && _values[0] == otherValue0,
              tuple: (otherValue0, otherValue1) =>
                  _type == _$SimpleUnionType.tuple &&
                  _values[0] == otherValue0 &&
                  _values[1] == otherValue1,
              string: (otherValue0) =>
                  _type == _$SimpleUnionType.string && _values[0] == otherValue0,
              fooInt: (otherValue0) =>
                  _type == _$SimpleUnionType.fooInt && _values[0] == otherValue0,
              fooString: (otherValue0) =>
                  _type == _$SimpleUnionType.fooString && _values[0] == otherValue0,
            );
      }
    
      @override
      int get hashCode {
        var curVal = $jc(0, _type.hashCode);
        for (final value in _values) {
          curVal = $jc(curVal, value.hashCode);
        }
    
        return $jf(curVal);
      }
    
      @override
      String toString() => (newBuiltValueToStringHelper('SimpleUnion')
            ..add('value', _values)
            ..add('type', _type))
          .toString();
    }
    
    class SimpleUnionBuilder implements Builder<SimpleUnion, SimpleUnionBuilder> {
      _$SimpleUnion _$v;
    
      void empty() {
        _$v = _$SimpleUnion._(_$SimpleUnionType.empty, []);
      }
    
      void integer(int value0) {
        _$v = _$SimpleUnion._(_$SimpleUnionType.integer, [value0]);
      }
    
      void tuple(int value0, String value1) {
        _$v = _$SimpleUnion._(_$SimpleUnionType.tuple, [value0, value1]);
      }
    
      void string(String value0) {
        _$v = _$SimpleUnion._(_$SimpleUnionType.string, [value0]);
      }
    
      void fooInt(Foo<int> value0) {
        _$v = _$SimpleUnion._(_$SimpleUnionType.fooInt, [value0]);
      }
    
      void fooString(Foo<String> value0) {
        _$v = _$SimpleUnion._(_$SimpleUnionType.fooString, [value0]);
      }
    
      SimpleUnionBuilder();
    
      @override
      void replace(SimpleUnion other) {
        if (other == null) {
          throw new ArgumentError.notNull('other');
        }
        _$v = other as _$SimpleUnion;
      }
    
      @override
      void update(void Function(SimpleUnionBuilder) updates) {
        if (updates != null) updates(this);
      }
    
      @override
      _$SimpleUnion build() {
        if (_$v == null) {
          throw new ArgumentError.notNull('SimpleUnion');
        }
        final _$result = _$v;
        replace(_$result);
        return _$result;
      }
    }
  17. amnykon commented on Jan 22, 2020

    @amnykon

    I like the final List<Object> _values; it will work in either case, though we would need to think about the get functions I proposed. Maybe use Get{TypeName}{Member} if the type is a tuple?

    The gets are not as important for a tuple, probably could leave them out for now and add them in latter.

  18. davidmorgan commented on Jan 23, 2020

    @davidmorgan
    Collaborator

    Nested builders would mean that when you use the union builder, you can also use the nested field builder. (Actually, have to).

    // A `BuiltList` is represented in the builder as a `ListBuilder`.
    var unionValue = ComplexUnion((b) => b.builtList..replace([1, 2, 3])..sort());
    // A `SimpleUnion` is represented in the builder as a `SimpleUnionBuilder`.
    var unionValue = ComplexUnion((b) => b..simpleUnion.integer(3));
    
  19. davidmorgan commented on Jan 23, 2020

    @davidmorgan
    Collaborator

    Re: operator==, you could just check/cast for _$SimpleUnion instead of SimpleUnion. It probably makes sense to forbid other implementations of the type.

  20. realcr commented on Jan 23, 2020

    @realcr

    Hey, so I gave the class SimpleUnion extends _$SimpleUnion design a try,
    I think that the result is cleaner than what we had earlier.
    The ergonomics is{Type} methods were easy to add with this design.

    The operator== implementation was updated to avoid using match. That
    said, I believe that what I did there is still not very clean (You can check
    it out below).

    I wasn't sure if having BuiltList<Object> _values could therefore be a better
    design choice than List<Object> _values, but eventually decided against it
    because using BuiltList<Object> requires an extra dependency.

    Not fully sure what are the implications of having the class SimpleUnion extends _$SimpleUnion design over nested builders.

    I hope to start exploring code generation implementation soon.

    User code
    library union;
    
    // import 'package:built_collection/built_collection.dart';
    import 'package:built_value/built_value.dart';
    // import 'package:built_value/json_object.dart';
    import 'package:built_value/serializer.dart';
    import 'package:meta/meta.dart';
    
    part 'union_example_g.dart';
    
    
    class Foo<T> {
      T data;
      Foo(data): data = data;
    
      @override
      bool operator ==(Object other) {
        if (identical(other, this)) {
          return true;
        }
        return other is Foo<T> &&
            data == other.data;
      }
    }
    
    final fooSerializer = FooSerializer<int>();
    
    class FooSerializer<T> implements StructuredSerializer<Foo<T>> {
      @override
      final Iterable<Type> types = const [Foo];
      @override
      final String wireName = 'Foo';
    
      @override
      Iterable<Object> serialize(Serializers serializers, Foo<T> object,
          {FullType specifiedType = FullType.unspecified}) {
        return <Object>[serializers.serialize(object.data, specifiedType: FullType(T))];
      }
    
      @override
      Foo<T> deserialize(Serializers serializers, Iterable<Object> serialized,
          {FullType specifiedType = FullType.unspecified}) {
    
        final iterator = serialized.iterator;
        iterator.moveNext();
        return Foo(serializers.deserialize(iterator.current, specifiedType: FullType(T)));
      }
    }
    
    class SimpleUnion extends _$SimpleUnion {
      static Serializer<SimpleUnion> get serializer => _$simpleUnionSerializer;
    
      SimpleUnion.empty(): super.empty();
      SimpleUnion.integer(int integer): super.integer(integer);
      SimpleUnion.tuple(int tupleInt, String tupleString): super.tuple(tupleInt, tupleString);
      SimpleUnion.string(String string): super.string(string);
      SimpleUnion.fooInt(Foo<int> fooInt): super.fooInt(fooInt);
      SimpleUnion.fooString(Foo<String> fooString): super.fooString(fooString);
    }
    Example test code
    // Unions
    var foo = Foo<int>(3);
    final unionValue1 = SimpleUnion.fooInt(foo);
    
    var descString = unionValue1.match(
      empty: () => 'empty',
      integer: (_) => 'integer',
      tuple: (_1, _2) => 'tuple',
      string: (_) => 'string',
      fooInt: (_) => 'fooInt',
      fooString: (_) => 'fooString');
    
    assert(descString == 'fooInt');
    
    // Check equality operator:
    final unionValue2 = SimpleUnion.integer(3);
    final unionValue3 = SimpleUnion.integer(3);
    
    assert(unionValue2 == unionValue3);
    
    // Check tuple:
    final unionValue4 = SimpleUnion.tuple(4, 'four');
    
    var res = unionValue4.match(
      empty: () => [],
      integer: (_) => [],
      tuple: (res_num, res_string) => [res_num, res_string],
      string: (_) => [],
      fooInt: (_) => [],
      fooString: (_) => []);
    
    assert(res[0] == 4);
    assert(res[1] == 'four');
    
    final unionValue5 = SimpleUnion.empty();
    Autogenerated code
    // GENERATED CODE - DO NOT MODIFY BY HAND
    
    part of union;
    
    // **************************************************************************
    // BuiltValueGenerator
    // **************************************************************************
    
    Serializer<SimpleUnion> _$simpleUnionSerializer = new _$SimpleUnionSerializer();
    
    class _$SimpleUnionSerializer implements StructuredSerializer<SimpleUnion> {
      @override
      final Iterable<Type> types = const [SimpleUnion, _$SimpleUnion];
      @override
      final String wireName = 'SimpleUnion';
    
      @override
      Iterable<Object> serialize(Serializers serializers, SimpleUnion object,
          {FullType specifiedType = FullType.unspecified}) {
        final result = object.match(
            empty: () => <Object>['empty'],
            integer: (value0) => <Object>[
                  'integer',
                  serializers.serialize(value0, specifiedType: const FullType(int))
                ],
            tuple: (value0, value1) => <Object>[
                  'tuple',
                  <Object>[serializers.serialize(value0, specifiedType: const FullType(int)),
                  serializers.serialize(value1,
                      specifiedType: const FullType(String))]
                ],
            string: (value0) => <Object>[
                  'string',
                  serializers.serialize(value0,
                      specifiedType: const FullType(String))
                ],
            fooInt: (value0) {
              print('value0 = $value0');
              print(value0.data);
              return <Object>[
                'fooInt',
                serializers.serialize(value0, specifiedType: const FullType(Foo))
              ];
            },
            fooString: (value0) => <Object>[
                  'fooString',
                  serializers.serialize(value0, specifiedType: const FullType(Foo))
                ]);
    
        return result;
      }
    
      @override
      SimpleUnion deserialize(Serializers serializers, Iterable<Object> serialized,
          {FullType specifiedType = FullType.unspecified}) {
    
        final iterator = serialized.iterator;
        iterator.moveNext();
        final key = iterator.current as String;
        iterator.moveNext();
        final Object value = iterator.current;
        var result;
        switch (key) {
          case 'empty':
            result = SimpleUnion.empty();
            break;
          case 'integer':
            iterator.moveNext();
            result = SimpleUnion.integer(serializers.deserialize(value,
                specifiedType: const FullType(int)));
            break;
          case 'tuple':
            final iterator = (value as Iterable<Object>).iterator;
            iterator.moveNext();
            final dynamic value0 = iterator.current;
            iterator.moveNext();
            final dynamic value1 = iterator.current;
    
            result = SimpleUnion.tuple(
                serializers.deserialize(value0, specifiedType: const FullType(int)),
                serializers.deserialize(value1,
                    specifiedType: const FullType(String)));
            break;
          case 'string':
            result = SimpleUnion.integer(serializers.deserialize(value,
                specifiedType: const FullType(String)));
            break;
          case 'fooInt':
            result = SimpleUnion.fooInt(serializers.deserialize(value,
                specifiedType: const FullType(Foo)));
            break;
          case 'fooString':
            result = SimpleUnion.fooInt(serializers.deserialize(value,
                specifiedType: const FullType(Foo)));
            break;
        }
    
        return result;
      }
    }
    
    enum _$SimpleUnionType {
      empty,
      integer,
      tuple,
      string,
      fooInt,
      fooString,
    }
    
    abstract class _$SimpleUnion {
      final _$SimpleUnionType _type;
      final List<Object> _values;
    
      _$SimpleUnion.empty(): _type = _$SimpleUnionType.empty, _values = [];
      _$SimpleUnion.integer(int integer): _type = _$SimpleUnionType.integer, _values = [integer];
      _$SimpleUnion.tuple(int tupleInteger, String tupleString): _type = _$SimpleUnionType.tuple, _values = [tupleInteger, tupleString];
      _$SimpleUnion.string(String string): _type = _$SimpleUnionType.string, _values = [string];
      _$SimpleUnion.fooInt(Foo<int> fooInt): _type = _$SimpleUnionType.fooInt, _values = [fooInt];
      _$SimpleUnion.fooString(Foo<String> fooString): _type = _$SimpleUnionType.fooString, _values = [fooString];
    
      T match<T>({
        @required T Function() empty,
        @required T Function(int) integer,
        @required T Function(int, String) tuple,
        @required T Function(String) string,
        @required T Function(Foo<int>) fooInt,
        @required T Function(Foo<String>) fooString,
      }) {
        switch (_type) {
          case _$SimpleUnionType.empty:
            return empty();
          case _$SimpleUnionType.integer:
            return integer(_values[0]);
          case _$SimpleUnionType.tuple:
            return tuple(_values[0], _values[1]);
          case _$SimpleUnionType.string:
            return string(_values[0]);
          case _$SimpleUnionType.fooInt:
            return fooInt(_values[0]);
          case _$SimpleUnionType.fooString:
            return fooString(_values[0]);
          default:
            // TODO: Better exception to throw here?
            throw Exception('unknown type');
        }
      }
    
      bool get isEmpty => _type == _$SimpleUnionType.empty;
      bool get isInteger => _type == _$SimpleUnionType.integer;
      bool get isTuple => _type == _$SimpleUnionType.tuple;
      bool get isString => _type == _$SimpleUnionType.string;
      bool get isfooInt => _type == _$SimpleUnionType.fooInt;
      bool get isFooString => _type == _$SimpleUnionType.fooString;
    
      @override
      bool operator ==(Object other) {
        if (identical(other, this)) {
          return true;
        }
        if (other is _$SimpleUnion) {
          if (_type != other._type) {
            return false;
          }
          if (_values.length != other._values.length) {
            return false;
          }
          for (var i=0; i < _values.length; ++i) {
            if (_values[i] != other._values[i]) {
              return false;
            }
          }
          return true;
        } else {
          return false;
        }
      }
    
      @override
      int get hashCode {
        var curVal = $jc(0, _type.hashCode);
        for (final value in _values) {
          curVal = $jc(curVal, value.hashCode);
        }
    
        return $jf(curVal);
      }
    
      @override
      String toString() => (newBuiltValueToStringHelper('SimpleUnion')
            ..add('value', _values)
            ..add('type', _type))
          .toString();
    }
  21. davidmorgan commented on Jan 23, 2020

    @davidmorgan
    Collaborator

    The problem with using class SimpleUnion extends _$SimpleUnion is that it means your class exposes API that is purely in the generated code. Generated code can be harder to deal with--it might not be generated yet, it might change when the generator changes--and so this makes the public API of the class less accessible.

    It's true that built_value currently does this by default for builders. This is a compromise; the builder API is much less important than the value type API, so it's less of a problem that it's hidden.

    I don't see a great way to do this for union types, since they need more API surface in the union class that we can't predict. One option is to have the analyzer plugin add the boilerplate for you. In the long run I think that's a good way to do it, because it makes all the API visible. But it has to work really well everywhere, and we're not currently at that point.

    There has been quite a lot of discussion about improving how codegen works so we can better separate the public API and the generated code; in particular about using the external keyword to let you declare explicitly that you're writing out an API that is going to be generated later.

  22. realcr commented on Jan 23, 2020

    @realcr

    I understand now for the first time the rationale behind the extends Built<...> design decision: it makes sure autogenerated code is never exposed as part of the API.

    To summarize, I currently see two strategies of adding a Union:

    • class SimpleUnion extends _$SimpleUnion
      • pros: Extra ergonomics (for example: is{Type})
      • cons: automatically generated code exposed as part of the API.
    • abstract class SimpleUnion implements Built<SimpleUnion, SimpleUnionBuilder>
      • pros: Automatically generated code is hidden and never reaches the API.
      • cons: Less ergonomics (We only get to have the match function), somewhat of a stranger way to declare the variants (Using match declaration, instead of using constructors)

    Regarding the extra ergonomics: As a Rust programmer all you have to deal with a Rust enum (equivalent to Union in this case) is a match statement. There is nothing else. It is sometimes a bit not ergonomic, but I lived with it a few years already and stayed alive, so I think it is a reasonable option too.

    @davidmorgan : I think that your last comment was too polite for me to be able to deduce what to do next (: Which strategy would you like to have implemented?

    One more thought: Possibly if you do not intend to merge such feature to built_value at all at this point, maybe there is a possibility to add the union feature as a separate dart package, and in some way let the two packages co-exist with the code-gen? I am not sure how to do something like that, but you might have an idea of what is possible.

    EDIT: Extra thought: In the autogenerated implementation of SimpleUnionBuilder with the abstract class SimpleUnion implements Built<SimpleUnion, SimpleUnionBuilder> strategy, isn't the user exposed to generated code at the API level?

  23. davidmorgan commented on Jan 23, 2020

    @davidmorgan
    Collaborator

    I didn't make a clear recommendation because I'm not hugely satisfied with either option :) but on the whole I think option two is the way to go, it's consistent with Built and EnumClass, and it's more future proof. This only works if the result is good enough that people want to use it, though.

    Yes, it's possible to have it be a separate package. Originally the serializers codegen was in a separate package to the value type codegen. It basically just works automatically :) ... the output from multiple generators is collected in the same .g.dart file. So if you copied/renamed the built_value packages they'd work.

    Re: your edit, yes, SimpleUnionBuilder is generated API. That's what I meant in my comment by 'It's true that built_value currently does this by default for builders. This is a compromise...'; you do have the option to explicitly write out the builder API, but people didn't want to do that. For comparison, note that AutoValue for Java requires to explicitly write out the full builder API.

  24. realcr commented on Jan 23, 2020

    @realcr

    Sounds reasonable.

    I am currently also exploring the option of making an external dart package that will provide a Union. I think it might allow us to initially check what it is like, without putting on the maintainers of built_value the burden of maintaining the newly exposed API forever.

    My first attempt was to try to use the sum_types dart package, which implements a concept similar to the class SimpleUnion extends _$SimpleUnion design we discussed here.

    I hit an issue with the serializers, I thought that you might have an idea of how to fix it.
    I added this line in the original dart file:

    @SerializersFor([Nat, MyStruct])
    Serializers serializers = _$serializers;

    Above, MyStruct is a built_value class (extending Built<...>), while Nat is a structure extended by the sum_types dart package. However, in the code generated by built_value the Nat serializer I implemented by hand is missing:

    Serializers _$serializers = (new Serializers().toBuilder()
          ..add(MyStruct.serializer)
          ..addBuilderFactory(
              const FullType(BuiltList, const [const FullType(int)]),
              () => new ListBuilder<int>()))
        .build();

    And therefore I get this exception during runtime when trying to serialize MyStruct:

    Unhandled exception:
    Bad state: No serializer for 'Nat'.

    When I added the missing serializer manually by adding: ..add(Nat.serializer), serialization worked as expected.

    Full code:

    lib/ser.dart
    library ser;
    
    import 'package:meta/meta.dart';
    
    import 'package:built_value/built_value.dart';
    import 'package:built_value/serializer.dart';
    
    import 'package:built_collection/built_collection.dart';
    import 'package:sum_types/sum_types.dart';
    
    part 'ser.g.dart';
    
    final natSerializer = NatSerializer();
    
    class NatSerializer implements StructuredSerializer<Nat> {
      @override
      final Iterable<Type> types = const [Nat];
      @override
      final String wireName = 'Nat';
    
      @override
      Iterable<Object> serialize(Serializers serializers, Nat object,
          {FullType specifiedType = FullType.unspecified}) {
        return object.iswitch(
            zero: () => <Object>['zero'],
            variant: (string) => <Object>['variant', string]);
      }
    
      @override
      Nat deserialize(Serializers serializers, Iterable<Object> serialized,
          {FullType specifiedType = FullType.unspecified}) {
        final iterator = serialized.iterator;
        iterator.moveNext();
        String typeName = iterator.current;
    
        switch (typeName) {
          case 'zero':
            return Nat.zero();
          case 'variant':
            iterator.moveNext();
            final value0 = iterator.current;
            return Nat.variant(value0);
          default:
            // TODO: Better exception to throw here?
            throw Exception('unknown type');
        }
      }
    }
    
    @SumType()
    class Nat extends _$Nat {
      static Serializer<Nat> get serializer => natSerializer;
    
      const Nat.zero() : super(zero: const Unit());
      const Nat.variant(String value) : super(variant: value);
    }
    
    abstract class MyStruct implements Built<MyStruct, MyStructBuilder> {
      static Serializer<MyStruct> get serializer => _$myStructSerializer;
    
      BuiltList<int> get myList;
      String get myString;
      Nat get myNat;
    
      @nullable
      @BuiltValueField(serialize: true)
      Null get myNull;
    
      factory MyStruct([void Function(MyStructBuilder) updates]) = _$MyStruct;
      MyStruct._();
    }
    
    @SerializersFor([Nat, MyStruct])
    Serializers serializers = _$serializers;
    bin/ser.dart
    import 'dart:convert';
    import 'package:ser/ser.dart';
    import 'package:built_value/serializer.dart';
    import 'package:built_value/standard_json_plugin.dart';
    
    void main() {
      final myStruct = MyStruct((b) => b
          ..myList.add(1)
          ..myList.add(2)
          ..myNat = Nat.variant('variant_str')
          ..myNull = null
          ..myString = 'my_string');
    
      var serializersWithPlugin =
          (serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();
    
      var json = serializersWithPlugin.serialize(myStruct, specifiedType: FullType(MyStruct));
    
      JsonEncoder encoder = new JsonEncoder.withIndent('  ');
      String prettyprint = encoder.convert(json);
      print(prettyprint);
    }
  25. davidmorgan commented on Jan 28, 2020

    @davidmorgan
    Collaborator

    built_value will only auto-add serializers that belong to built_value classes; so it's expected that it won't add the Nat serializer automatically.

    There's an issue open that it should be a compile time error if you add Nat in SerializersFor since it didn't know what to do with it.

  26. realcr commented on Jan 29, 2020

    @realcr

    Hey, I tried to play a bit with the idea of having an external library that will implement a built_value compatible union. I think that the current list of constraints leaves us in a catch-22 situation:

    1. To be able to serialize the union class we have to implement Built<SimpleUnion, SimpleUnionBuilder>, which implies we have to use the abstract class SimpleUnion implements Built<SimpleUnion, SimpleUnionBuilder> design.
    2. If we have the line abstract class SimpleUnion implements Built<SimpleUnion, SimpleUnionBuilder> inside the code, the automatic code generator of built_value will attempt to generate code for the union class as if it was a usual built_value class. Therefore the code will not compile.

    I am not sure how to continue from here. Some ideas I had:

    • Is there possibly a way to opt out of built_value automatic code generation for a certain class? Maybe decorate it somehow or put it somewhere else?
    • Is there a way to forcefully add my own serializer into the serializers list? (Even though the class it serializes does not implement Built<...>?)

    The way I see it the hygienic way for code generation is to have a decorator above every class, this way the code generation module doesn't have to guess which classes should be dealt with. This also allows different code generation libraries to work well together. However, built_value code generator already tries to inspect every class, and changing this behaviour will break all existing code using built_value, so this is probably not an option.

    If none of these options are possible, I think there is no hope left but to fully fork the built_value code generator if we want to have a built_value style union.

  27. davidmorgan commented on Jan 29, 2020

    @davidmorgan
    Collaborator

    For #1: what is it you're getting from the Built interface? Would it work to have your own (renamed) copy of it?

    Thanks.

  28. realcr commented on Jan 29, 2020

    @realcr

    For #1: what is it you're getting from the Built interface?

    Hey, I hope that I understood the question correctly. The only reason I am using the Built interface is to be able to have automatic serialization. If I have my own copy of it (Like MyBuilt<...>) my class's serializer will not get into the serializers list, and therefore I will not be able serialize it (As an example, see the issue I was having when trying to use the sum_types dart package, here: #395 (comment))

  29. davidmorgan commented on Jan 30, 2020

    @davidmorgan
    Collaborator

    Ah, I see.

    I think it's probably neatest to ask people to explicitly modify the @SerializersFor declaration to add in the union serializers. That way we don't run into undefined behaviour about how Built behaves when reused by other packages :)

    What do you think? Thanks.

  30. realcr commented on Jan 31, 2020

    @realcr

    Hey, I managed to make a working dart package for unions that integrates well enough with built_value.dart, this is the repository: https://github.com/freedomlayer/built_union.dart
    I haven't uploaded a pub package yet. This is my first dart contribution, I am not yet really sure how to do it, but I will find out.

    Check out the tests to see what the usage syntax looks like.

    I made sure the license is compatible with this repository, so that if in the future you might want to merge some of the code from built_union.dart it wouldn't be a problem.

    Hacks

    There are two hacks worth mentioning in making built_union.dart work in harmony with built_value.dart, mostly on the serialization side:

    1. There was a special case for serialization: A tuple with an empty variant, that did not serialize well with the existing StandardJsonPlugin, so I created a CustomJsonPlugin that extends it and made a little fix. You can see it here. You might consider adding this change upstream to StandardJsonPlugin code.

    2. As noted before @SerializersFor(...) doesn't work properly with anything that does not inherit from Built<...>, so I had to create serializers manually. It is good enough for my use case, but maybe in the future we can find a solution for this.

    Things not implemented

    I did not yet implement generics. I think it is possible, but it wasn't important enough for me at this point. If someone wants to implement it, this is something I will be happy to merge.

    In addition, the only ergonomics methods that were added are the is{Type} style methods. Other ideas are welcome.

    EDIT: now on pub.dev:

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions