Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ordinal-based coproducts #1091

Merged
merged 21 commits into from
Jul 24, 2023
Merged

Ordinal-based coproducts #1091

merged 21 commits into from
Jul 24, 2023

Conversation

Baccata
Copy link
Contributor

@Baccata Baccata commented Jul 17, 2023

  • Changes the internal models of Service and UnionSchema to be based on coproduct's ordinals, in order to allow for indexed-based dispatch (which should be more performant than map-based dispatch)
  • In particular, split the endpoint method in the service interface, so that the input can be accessed on its own.
  • Changes the model of Alt to carry a projector partial function. PartialFunction[U, A] is preferred to U => Option[A] because it doesn't necessarily induce an instantiation. It's also trivial to go from PartialFunction[U, A] to U => Option[A] via the .lift method
  • Amends the rendering logic to render ordinal values where they're needed (all operations, all union members)
  • Adds Schema.either and Schema.tuple smart-constructors
  • Amend documentation where needed
  • Render unions as sealed abstract classes instead of sealed traits not doing it because of how it adds a field to the bytecode representation of ADT member classes which uses more memory
  • Generalise Schema.tuple via Boilerplate to handle all tuples from arity 2 to 22

Change Service/Union schemas to be ordinal based. This has the following
benefits:

1. Allows for indexed-based random access instead of label-centric mapping.
2. Aligns our model with what Scala 3 does around Sum Mirrors, which are
centered around ordinals.
3. Forces a projection function to be held by alternatives, which makes
sense (arguably speaking)
@@ -26,7 +27,8 @@ import kinds._
final case class Alt[U, A](
label: String,
schema: Schema[A],
inject: A => U
inject: A => U,
project: PartialFunction[U, A]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New projection function, which removes the need to have projector in Dispatcher

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inject + project form something similar to a prism, wondering if it'd be useful to define it that way...

edit: at the time I was originally writing this, we didn't have #1103 - that other PR adds the primitives, so we could actually use them in alts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it and I don't really want to because it'd impact things too much. Moreover we're making the generation of optics an opt-in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And we can't make the Alt a bonafide Prism and expose it to users because of recursion concerns. Alts need to be hidden in the computation of the schema.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm I don't know about the recursion concerns, can you elaborate?

other than that I get it

Copy link
Contributor Author

@Baccata Baccata Jul 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm I don't know about the recursion concerns, can you elaborate?

Actually I was partially wrong. What I meant was that :

val schema: Schema[A] = Schema.recursive {
    val aField = schema.optional[A]("a", _.a) 
    Schema.struct(aField)(A(_))
} 

Rendering it as follows to make aField visible to the external world doesn't work because aField refers to the initialised schema.

val aField = schema.optional[A]("a", _.a) 
val schema: Schema[A] = Schema.recursive {
    Schema.struct(aField)(A(_))
} 

However we could potentially render it as follows, which works :

val schema: Schema[A] = Schema.recursive {
    Schema.struct(aField)(A(_))
} 

val aField = schema.optional[A]("a", _.a) 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll sync with Jeff to explore an alternative to his PR after this one is merged.


def union[U](alts: Vector[Alt[U, _]])(dispatch: U => Alt.WithValue[U, _]): Schema.UnionSchema[U] =
Schema.UnionSchema(placeholder, Hints.empty, alts, dispatch)
def either[A, B](left: Schema[A], right: Schema[B]) : Schema[Either[A, B]] = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new smart constructors for Schema[Either[A, B]] and Schema[Tuple[A, B]]

hints: Hints
) extends Service.Reflective[DynamicOp]
with DynamicSchemaIndex.ServiceWrapper {

type Alg[P[_, _, _, _, _]] = PolyFunction5.From[DynamicOp]#Algebra[P]
override val service: Service[Alg] = this

private lazy val endpointMap: Map[ShapeId, Endpoint[_, _, _, _, _]] =
endpoints.map(ep => ep.id -> ep).toMap
private lazy val ordinalMap: Map[ShapeId, Int] =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be changed by finding a way for the DynamicEndpoint to carry the ordinal

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any issue with making it do so?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although tbh I don't mind it either way, the map seems just as fine

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any issue with making it do so?

It's not that simple, because the DynamicOps are not currently tied to a service.

@@ -222,21 +222,6 @@ object Boilerplate {
- final def andThen[H[${`_.._`}]](other: PolyFunction$suffix[G, H]): PolyFunction$suffix[F, H] = new PolyFunction$suffix[F, H]{
- def apply[${`A..N`}](fa: F[${`A..N`}]): H[${`A..N`}] = other(self(fa))
- }
-
- import Kind$arity._
Copy link
Contributor Author

@Baccata Baccata Jul 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing this because the unsafe caching needs to be backed by maps or arrays depending on the situation

Copy link
Contributor

@daddykotex daddykotex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that def either change makes it so much cleaner

good job once more

case Right(string) => right(string)
}
}
implicit val schema: Schema[Foo] = Schema.either(int, string)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that looks good, wow

package smithy4s
package schema

class PartiallyAppliedTuple protected[schema](placeholder: ShapeId) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allows for Schema.tuple(schemaA, schemaB, ...)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gah dayum

@Baccata Baccata marked this pull request as ready for review July 19, 2023 08:05
@Baccata
Copy link
Contributor Author

Baccata commented Jul 19, 2023

@kubukoz if you can give it a review, I'd appreciate it :)

@kubukoz
Copy link
Member

kubukoz commented Jul 19, 2023

I will

@kubukoz kubukoz self-requested a review July 19, 2023 16:13
@@ -26,7 +27,8 @@ import kinds._
final case class Alt[U, A](
label: String,
schema: Schema[A],
inject: A => U
inject: A => U,
project: PartialFunction[U, A]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inject + project form something similar to a prism, wondering if it'd be useful to define it that way...

edit: at the time I was originally writing this, we didn't have #1103 - that other PR adds the primitives, so we could actually use them in alts

@@ -59,10 +61,23 @@ final case class Alt[U, A](
object Alt {

final case class WithValue[U, A](
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: can we remove this now? I don't see anything failing to compile when I do this

diff --git a/modules/core/src/smithy4s/schema/Alt.scala b/modules/core/src/smithy4s/schema/Alt.scala
index 45eb6db6..20e41ad0 100644
--- a/modules/core/src/smithy4s/schema/Alt.scala
+++ b/modules/core/src/smithy4s/schema/Alt.scala
@@ -34,8 +34,8 @@ final case class Alt[U, A](
   @deprecated("use .schema instead", since = "0.18.0")
   def instance: Schema[A] = schema
 
-  def apply(value: A): Alt.WithValue[U, A] =
-    Alt.WithValue(this, value)
+  // def apply(value: A): Alt.WithValue[U, A] =
+  //   Alt.WithValue(this, value)
 
   def hints: Hints = schema.hints
   def memberHints: Hints = schema.hints.memberHints
@@ -60,10 +60,10 @@ final case class Alt[U, A](
 }
 object Alt {
 
-  final case class WithValue[U, A](
-      private[schema] val alt: Alt[U, A],
-      value: A
-  )
+  // final case class WithValue[U, A](
+  //     private[schema] val alt: Alt[U, A],
+  //     value: A
+  // )
 
   type ClassTagged[U] = WithClassTag[U, _]
   final case class WithClassTag[U, A <: U](

value: A
)

type ClassTagged[U] = WithClassTag[U, _]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: I don't see this type being used anywhere

Comment on lines 69 to 79
final case class WithClassTag[U, A <: U](
alt: Alt[U, A],
classTag: scala.reflect.ClassTag[A]
)

object WithClassTag {
implicit def fromAlt[U, A <: U: ClassTag](
alt: Alt[U, A]
): WithClassTag[U, A] =
WithClassTag(alt, implicitly[ClassTag[A]])
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: are these used anywhere?


import smithy4s.Hints

class PartiallyAppliedUnion[U](val alts: Vector[Alt[U, _]]) extends AnyVal {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: final class?

Comment on lines +96 to +97
def apply[A](alt: Alt[E, A]): F[E] = {
resultCache(alt).asInstanceOf[F[E]]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: is this something we could encapsulate in a public pattern? I suppose it'd be a sort of dual to Dispatcher

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might be able to but I've not thought about it enough to know if it's possible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the bare minimum for us to be able to generalise would be to have a flatmap constraint on the F

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be fine with such a constraint. flatten + map (from Covariant) would be fine, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave it a go and it's a lot more complicated to generalise than the encoding side, partially because the action of decoding may "consume" data.

So it ends up looking like this :

    def compileDecoder[Decoder[_], F[_], Message, Discriminator](
        precompile: Precompiler[Decoder],
        discriminator: Alt[_, _] => Discriminator,
        discriminate: Message => F[(Discriminator, Message)]
    )(implicit
        decoder: DecoderK[Decoder, F, Message]
    ): Decoder[U]

It may be worth exploring further but certainly not in this PR

@@ -86,51 +101,48 @@ object Alt {
encoderK: EncoderK[G, Result]
): G[U]

def projector[A](alt: Alt[U, A]): U => Option[A]
def ordinal(u: U): Int
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: could we make it more constrained than just Int, in dispatchers? maybe a type member?

then, we'd also need methods like def get[A](i: Ordinal)(items: Seq[A]): A.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could have get[A](u: U)(items: Seq[A]): A ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that... works. but now I'm afraid of people passing a seq of the wrong length. It's equally bad :(

the whole idea is reminiscent of Representable from cats

hints: Hints
) extends Service.Reflective[DynamicOp]
with DynamicSchemaIndex.ServiceWrapper {

type Alg[P[_, _, _, _, _]] = PolyFunction5.From[DynamicOp]#Algebra[P]
override val service: Service[Alg] = this

private lazy val endpointMap: Map[ShapeId, Endpoint[_, _, _, _, _]] =
endpoints.map(ep => ep.id -> ep).toMap
private lazy val ordinalMap: Map[ShapeId, Int] =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any issue with making it do so?

hints: Hints
) extends Service.Reflective[DynamicOp]
with DynamicSchemaIndex.ServiceWrapper {

type Alg[P[_, _, _, _, _]] = PolyFunction5.From[DynamicOp]#Algebra[P]
override val service: Service[Alg] = this

private lazy val endpointMap: Map[ShapeId, Endpoint[_, _, _, _, _]] =
endpoints.map(ep => ep.id -> ep).toMap
private lazy val ordinalMap: Map[ShapeId, Int] =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although tbh I don't mind it either way, the map seems just as fine

package smithy4s
package schema

class PartiallyAppliedTuple protected[schema](placeholder: ShapeId) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gah dayum

@Baccata Baccata merged commit 8b8cba7 into series/0.18 Jul 24, 2023
10 checks passed
@Baccata Baccata deleted the ordinal-based-coproducts branch July 24, 2023 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants