-
Notifications
You must be signed in to change notification settings - Fork 71
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
Ordinal-based coproducts #1091
Conversation
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] |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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]] = { |
There was a problem hiding this comment.
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] = |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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._ |
There was a problem hiding this comment.
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
There was a problem hiding this 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) |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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, ...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gah dayum
@kubukoz if you can give it a review, I'd appreciate it :) |
I will |
@@ -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] |
There was a problem hiding this comment.
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]( |
There was a problem hiding this comment.
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, _] |
There was a problem hiding this comment.
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
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]]) | ||
} |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: final class?
def apply[A](alt: Alt[E, A]): F[E] = { | ||
resultCache(alt).asInstanceOf[F[E]] |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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] = |
There was a problem hiding this comment.
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] = |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gah dayum
Service
andUnionSchema
to be based on coproduct'sordinals
, in order to allow for indexed-based dispatch (which should be more performant than map-based dispatch)endpoint
method in the service interface, so that theinput
can be accessed on its own.Alt
to carry a projector partial function.PartialFunction[U, A]
is preferred toU => Option[A]
because it doesn't necessarily induce an instantiation. It's also trivial to go fromPartialFunction[U, A]
toU => Option[A]
via the.lift
methodSchema.either
andSchema.tuple
smart-constructorsRender unions as sealed abstract classes instead of sealed traitsnot doing it because of how it adds a field to the bytecode representation of ADT member classes which uses more memorySchema.tuple
viaBoilerplate
to handle all tuples from arity 2 to 22