zio-schema icon indicating copy to clipboard operation
zio-schema copied to clipboard

Dynamic optics with generic records and enums

Open jdegoes opened this issue 2 years ago • 0 comments

Although the AccessorBuilder machinery lets library authors build optics using their own data types, a generic mechanism would be extremely useful so that library authors have to do less work.

There are three parts to this problem:

  1. ADT for generic, refiied optic. Creating a sealed trait DynamicOptic[-From, +To] hierarchy, which simply stores the information that is available via AccessorBuilder (i.e. the field / case and the types). This is truly a "reified optic", as it merely stores path information. This would live in zio.schema.optics.
  2. Macro-powered type refinements. Using macros to create a type refinement containing the names and types of the fields of a record, and the cases of an enum. For example:
    type GenericRefinement= AnyRef {
      def name: String
      def age: Int
    }
    
    This generic refinement could be stored on the Schema of the appropriate type (e.g. Record#GenericRefinement).
  3. Library-focused accessor support. We can introduce types like GenericRecord and GenericEnum, which extend Dynamic (Scala 2) or Selectable (Scala 3), and whose constructors merely require the ability to lookup a DynamicOptic from a value with the specified Schema, e.g.:
    trait AccessorLookup[Underlying] {
      def lookupLens[S, A](value: Underlying, schema: Schema[A], lens: DynamicOptic.Lens[S, A]): Either[String, A]
      ...
    }
    
    This is only an approximate design because cases of enums and elements in collections will require a different signature. In order to be maximally useful, the lookup functions cannot return a raw A value, because that would preclude applications in libraries like ZIO Flow, but they could return a F[A] for some accessor-lookup specified wrapper type (or maybe, F[String, A]). Given a library supplied AccessorLookup, we could have constructors for GenericRecord and GenericEnum:
    class GenericRecord(accessorLookup: AccessorLookup) extends Selectable {
      ...
    }
    object GenericRecord {
      def make[Underlying](underlying: Underlying, lookup: AccessorLookup[Underlying], schema: Schema.Record[_]): schema.GenericRefinement = ???
     }
    
    With this, a user can now create generic records and generic enums, and lookup and maybe even modify terms of records and enums, as per the interfaces defined above, all in a type-safe way, and without having to write any boilerplate themselves.

jdegoes avatar Feb 16 '22 18:02 jdegoes