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

non-orphan implicit typeclasses #912

Merged
merged 11 commits into from
May 11, 2023
Merged

Conversation

lewisjkl
Copy link
Contributor

Making it so typeclass instances can be added in companion objects in the generated code. This uses a mechanism similar to refinements to specify the location of the typeclass and the interpreter that provides it.

Will add more details as this PR comes out of draft status.

MovieTheater.apply
}.withId(id).addHints(hints)

implicit val movieTheaterEq: cats.Eq[MovieTheater] = EqInterpreter.fromSchema(schema)
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 is the main thing to look at here. I still have a ways to go on this PR, but I wanted to put it up now to get early feedback before I get further along with it.

Copy link
Member

Choose a reason for hiding this comment

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

Looks great to me.

@kubukoz
Copy link
Member

kubukoz commented Apr 20, 2023

ohh la la. I may like this.


val precompiler = new Alt.Precompiler[Schema, AltEq] {
def apply[A](label: String, instance: Schema[A]): AltEq[A] = {
// Here we "cheat" to recover the `Alt` corresponding to `A`, as this information
Copy link
Member

Choose a reason for hiding this comment

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

Shall we add support for this in Precompiler?

}
}

val precompiler = new Alt.Precompiler[Schema, AltEq] {
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: usually I just inline this

Comment on lines 192 to 193
def primitiveEq[P](primitive: Primitive[P]): Eq[P] = {
primitive match {
Copy link
Member

Choose a reason for hiding this comment

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

This smells an awful lot like Primitive.deriving.

Comment on lines 183 to 184
val eqA: Eq[A] = self(schema)
(x: B, y: B) => eqA.eqv(refinement.from(x), refinement.from(y))
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
val eqA: Eq[A] = self(schema)
(x: B, y: B) => eqA.eqv(refinement.from(x), refinement.from(y))
self(schema).contramap(refinement.from)

same in bijections

): Eq[S] = { (x: S, y: S) =>
{
def forField[A2](field: Field[Schema, S, A2]): Boolean = {
val eqField = field.foldK(new Field.FolderK[Schema, S, Eq]() {
Copy link
Member

Choose a reason for hiding this comment

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

issue: since this is an example we probably want to follow best practices, so I'd suggest some more "precompiled" way: make forField return an S => Boolean and move the (x: S, y: S) => below so that we traverse the fields while instantiating the whole thing.

Comment on lines 71 to 72
implicit val valueEq: Eq[V] = self(value)
Eq[Map[K, V]]
Copy link
Member

Choose a reason for hiding this comment

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

kinda interesting how they made Maps comparable without an Eq for the key

Comment on lines 57 to 62
tag match {
case CollectionTag.ListTag => Eq[List[A]]
case CollectionTag.SetTag => Eq[Set[A]]
case CollectionTag.VectorTag => Eq[Vector[A]]
case CollectionTag.IndexedSeqTag => Eq[IndexedSeq[A]]
}
Copy link
Member

Choose a reason for hiding this comment

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

thought: we could have a CollectionTag.deriving in a similar spirit to Primitive.deriving

MovieTheater.apply
}.withId(id).addHints(hints)

implicit val movieTheaterEq: cats.Eq[MovieTheater] = EqInterpreter.fromSchema(schema)
Copy link
Member

Choose a reason for hiding this comment

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

Looks great to me.

@kubukoz
Copy link
Member

kubukoz commented Apr 20, 2023

sorry for the comment spam @lewisjkl, this is looking great

@lewisjkl
Copy link
Contributor Author

sorry for the comment spam @lewisjkl, this is looking great

No worries @kubukoz I actually copied that EqInterpreter from something that @yisraelU worked on a while back. I really just put it as a placeholder until he PRs in a smithy4s-cats module that adds that among other interpreters. Then we can remove it from this PR and just reference it instead.

@@ -280,6 +280,8 @@ private[internals] object Hint {
case object IndexedSeq extends SpecializedList
}
case object UniqueItems extends Hint
case class Typeclass(id: ShapeId, targetType: String, interpreter: String)
Copy link
Contributor

@yisraelU yisraelU Apr 21, 2023

Choose a reason for hiding this comment

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

A nice-to-have would be a type alias wherever a String refers to a Class (for doc purposes)
Perhaps this should extend to the Smithy definitions too

hints: List[Hint],
tpe: NameRef
): Option[Lines] = {
(hints.collectFirst { case h: Hint.Typeclasses =>
Copy link
Contributor

Choose a reason for hiding this comment

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

I find collectFirst to be a bit misleading. There can be only one ,of type Hint.Typeclass and if there could be more , I think we would want them all

Copy link
Contributor

Choose a reason for hiding this comment

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

It's not misleading, I think it's just wrong. It should be collect here.

Copy link
Contributor

Choose a reason for hiding this comment

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

oh of course .I was thinking this was Hints as opposed to a List of hint

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 think it's right. There should only ever be one Typeclasses hint which may contain multiple 'Typeclass' instances. It doesn't need to be modeled that way, but that's how I did it so we'd keep the idea that you can only have 1 of a given trait/hint per shape. Collect first is just so I could do the find and map at the same time

Copy link
Contributor

Choose a reason for hiding this comment

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

but that's how I did it so we'd keep the idea that you can only have 1 of a given trait/hint per shape

I've "violated" that assumption in my commit 😅. For non-meta hints, I think you are correct, but there is a distinction to be had between hints that get rendered and available at runtime and hints that aren't. Trying to abide by the invariant at codegen time seems to be detrimental to the readability of the code .

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, sorry ,should have amended my comment. It wasn't "wrong", I was just confused between the existence of a Typeclass and a Typeclasses type, which got mixed together in my brain.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah that totally makes sense, I think it's clearer without the extra typeclasses hint so works for me 👍🏼

private def renderTypeclass(hint: Hint.Typeclass, tpe: NameRef): Line = {
val target = NameRef(hint.targetType)
val interpreter = NameRef(hint.interpreter)
val lowerCasedName = s"${tpe.name.head.toLower.toString}${tpe.name.tail}"
Copy link
Contributor

@Baccata Baccata Apr 21, 2023

Choose a reason for hiding this comment

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

I think we have a helper for this : uncapitalise

private def renderTypeclasses(
hints: List[Hint],
tpe: NameRef
): Option[Lines] = {
Copy link
Contributor

Choose a reason for hiding this comment

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

You can return Lines

removing EqSchemaVisitor in favour of a universalEquals-based
implementation.
@Baccata
Copy link
Contributor

Baccata commented Apr 21, 2023

Build fails because docs are currently linking a non-existing class. We'll wait for the cats-module to be merged to go back to this PR.

@lewisjkl
Copy link
Contributor Author

lewisjkl commented May 5, 2023

Will add changelog for this and #942 on Monday

@lewisjkl lewisjkl mentioned this pull request May 8, 2023
@lewisjkl lewisjkl marked this pull request as ready for review May 8, 2023 18:56
@Baccata Baccata merged commit 79534b7 into series/0.18 May 11, 2023
@Baccata Baccata deleted the inlined-implicit-typeclasses branch May 11, 2023 07:18
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.

5 participants