-
Notifications
You must be signed in to change notification settings - Fork 70
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
Changes from 3 commits
c53a6c9
cb20981
08a26d4
3f4716c
f7549c7
4c9d6e9
0d91ff2
c11322c
03f5675
e2d4f44
39e260b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -501,6 +501,24 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self => | |
} | ||
} | ||
|
||
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}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we have a helper for this : |
||
line"implicit val $lowerCasedName${hint.id.getName.capitalize}: $target[$tpe] = $interpreter.fromSchema(schema)" | ||
} | ||
|
||
private def renderTypeclasses( | ||
hints: List[Hint], | ||
tpe: NameRef | ||
): Option[Lines] = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can return |
||
(hints.collectFirst { case h: Hint.Typeclasses => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 . There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 👍🏼 |
||
newline ++ Lines( | ||
h.values.map(renderTypeclass(_, tpe)).toList | ||
) | ||
}) | ||
} | ||
|
||
private def renderProductNonMixin( | ||
product: Product, | ||
adtParent: Option[NameRef], | ||
|
@@ -591,6 +609,7 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self => | |
} else { | ||
line"implicit val schema: $Schema_[${product.nameRef}] = $constant_(${product.nameRef}()).withId(id).addHints(hints)" | ||
}, | ||
renderTypeclasses(product.hints, product.nameRef), | ||
additionalLines | ||
) | ||
) | ||
|
@@ -842,7 +861,8 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self => | |
if (error) "" else ".withId(id).addHints(hints)" | ||
) | ||
.appendToLast(if (recursive) ")" else "") | ||
} | ||
}, | ||
renderTypeclasses(hints, name) | ||
) | ||
) | ||
} | ||
|
@@ -914,7 +934,8 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self => | |
line"val values: $list[$name] = $list".args( | ||
values.map(_.name) | ||
), | ||
line"implicit val schema: $Schema_[$name] = $enumeration_(values).withId(id).addHints(hints)" | ||
line"implicit val schema: $Schema_[$name] = $enumeration_(values).withId(id).addHints(hints)", | ||
renderTypeclasses(hints, name) | ||
) | ||
) | ||
|
||
|
@@ -940,7 +961,8 @@ private[internals] class Renderer(compilationUnit: CompilationUnit) { self => | |
line"val underlyingSchema: $Schema_[$tpe] = ${tpe.schemaRef}$trailingCalls", | ||
lines( | ||
line"implicit val schema: $Schema_[$name] = $definition$bijection_(underlyingSchema, asBijection)$closing" | ||
) | ||
), | ||
renderTypeclasses(hints, name) | ||
) | ||
) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
--- | ||
sidebar_label: Typeclass Instances | ||
title: Non-Orphan Typeclass Instances | ||
--- | ||
|
||
As of smithy4s version `0.18.x` you are able to create custom typeclass instances inside the companion objects of classes in the code generated by smithy4s. This allows you to have instances that are found via implicit resolution without any need to special imports. Common examples where this will come in handy are for the `cats.Show` and `cats.Eq` typeclasses, but you can use this feature for any typeclass. | ||
|
||
Here we will show an example using the `cats.Eq` typeclass. | ||
|
||
## Setup Typeclass in Smithy | ||
|
||
Here we will use the `smithy4s.meta#typeclass` trait to define an `eq` trait that represents the `cats.Eq` typeclass. | ||
|
||
```smithy | ||
use smithy4s.meta#typeclass | ||
|
||
@trait | ||
@typeclass(targetType: "cats.Eq", interpreter: "smithy4s.example.typeclass.EqInterpreter") | ||
structure eq {} | ||
``` | ||
|
||
We are specifying `cats.Eq` as the `targetType` since that is the typeclass which this trait represents. We are then specifying `smithy4s.example.typeclass.EqInterpreter` as the classpath which points to a `CachedSchemaCompiler` for the `cats.Eq` typeclass. | ||
|
||
## Implement CachedSchemaCompiler | ||
|
||
Smithy4s has a concept called `CachedSchemaCompiler` which is an abstraction which we use here to interpret a `smithy4s.Schema` to produce an instance of a typeclass. Here is what this will look like: | ||
|
||
```scala | ||
object EqInterpreter extends CachedSchemaCompiler.Impl[Eq] { | ||
|
||
protected type Aux[A] = Eq[A] | ||
|
||
def fromSchema[A]( | ||
schema: Schema[A], | ||
cache: Cache | ||
): Eq[A] = { | ||
schema.compile(new EqSchemaVisitor(cache)) | ||
} | ||
|
||
} | ||
``` | ||
|
||
Here we are delegating to the `EqSchemaVisitor` which is doing the heavy lifting of interpreting the `smithy4s.Schema`. The `CachedSchemaCompiler.Impl` provides a `Cache` which we utilize to make sure we are not recompiling the same schema more than once. For more details on implementing a `SchemaVisitor`, you can check out the full `cats.Eq` schema compiler and visitor [here](<!-- TODO ADD LINK ONCE AVAILABLE -->). | ||
|
||
## Use the eq typeclass trait | ||
|
||
Now we are ready to use the `eq` trait we defined above. | ||
|
||
```smithy | ||
@eq | ||
structure MovieTheater { | ||
name: String | ||
} | ||
``` | ||
|
||
This tells smithy4s to generate an instance of the `Eq` typeclass in the companion object of the `MovieTheater` type and to use the `CachedSchemaCompiler` defined above for the implementation. The generated code will look like: | ||
|
||
```scala | ||
case class MovieTheater(name: Option[String] = None) | ||
object MovieTheater extends ShapeTag.Companion[MovieTheater] { | ||
val id: ShapeId = ShapeId("smithy4s.example", "MovieTheater") | ||
|
||
// ... | ||
|
||
implicit val schema: Schema[MovieTheater] = // ... | ||
|
||
implicit val movieTheaterEq: cats.Eq[MovieTheater] = EqInterpreter.fromSchema(schema) | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package smithy4s.example | ||
|
||
import smithy4s.Hints | ||
import smithy4s.Schema | ||
import smithy4s.ShapeId | ||
import smithy4s.ShapeTag | ||
import smithy4s.schema.Schema.constant | ||
|
||
case class Eq() | ||
object Eq extends ShapeTag.Companion[Eq] { | ||
val id: ShapeId = ShapeId("smithy4s.example", "eq") | ||
|
||
val hints: Hints = Hints( | ||
smithy.api.Trait(selector = None, structurallyExclusive = None, conflicts = None, breakingChanges = None), | ||
) | ||
|
||
implicit val schema: Schema[Eq] = constant(Eq()).withId(id).addHints(hints) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package smithy4s.example | ||
|
||
import smithy4s.Hints | ||
import smithy4s.Schema | ||
import smithy4s.ShapeId | ||
import smithy4s.ShapeTag | ||
import smithy4s.example.typeclass.EqInterpreter | ||
import smithy4s.schema.Schema.string | ||
import smithy4s.schema.Schema.struct | ||
|
||
case class MovieTheater(name: Option[String] = None) | ||
object MovieTheater extends ShapeTag.Companion[MovieTheater] { | ||
val id: ShapeId = ShapeId("smithy4s.example", "MovieTheater") | ||
|
||
val hints: Hints = Hints( | ||
smithy4s.example.Eq(), | ||
) | ||
|
||
implicit val schema: Schema[MovieTheater] = struct( | ||
string.optional[MovieTheater]("name", _.name), | ||
){ | ||
MovieTheater.apply | ||
}.withId(id).addHints(hints) | ||
|
||
implicit val movieTheaterEq: cats.Eq[MovieTheater] = EqInterpreter.fromSchema(schema) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks great to me. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package smithy4s.example | ||
|
||
import smithy4s.Enumeration | ||
import smithy4s.Hints | ||
import smithy4s.Schema | ||
import smithy4s.ShapeId | ||
import smithy4s.ShapeTag | ||
import smithy4s.example.typeclass.EqInterpreter | ||
import smithy4s.schema.Schema.enumeration | ||
|
||
sealed abstract class NetworkConnectionType(_value: String, _name: String, _intValue: Int, _hints: Hints) extends Enumeration.Value { | ||
override type EnumType = NetworkConnectionType | ||
override val value: String = _value | ||
override val name: String = _name | ||
override val intValue: Int = _intValue | ||
override val hints: Hints = _hints | ||
override def enumeration: Enumeration[EnumType] = NetworkConnectionType | ||
@inline final def widen: NetworkConnectionType = this | ||
} | ||
object NetworkConnectionType extends Enumeration[NetworkConnectionType] with ShapeTag.Companion[NetworkConnectionType] { | ||
val id: ShapeId = ShapeId("smithy4s.example", "NetworkConnectionType") | ||
|
||
val hints: Hints = Hints( | ||
smithy4s.example.Eq(), | ||
) | ||
|
||
case object ETHERNET extends NetworkConnectionType("ETHERNET", "ETHERNET", 0, Hints()) | ||
case object WIFI extends NetworkConnectionType("WIFI", "WIFI", 1, Hints()) | ||
|
||
val values: List[NetworkConnectionType] = List( | ||
ETHERNET, | ||
WIFI, | ||
) | ||
implicit val schema: Schema[NetworkConnectionType] = enumeration(values).withId(id).addHints(hints) | ||
|
||
implicit val networkConnectionTypeEq: cats.Eq[NetworkConnectionType] = EqInterpreter.fromSchema(schema) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package smithy4s.example | ||
|
||
import smithy4s.Hints | ||
import smithy4s.Schema | ||
import smithy4s.ShapeId | ||
import smithy4s.ShapeTag | ||
import smithy4s.example.typeclass.EqInterpreter | ||
import smithy4s.schema.Schema.bijection | ||
import smithy4s.schema.Schema.union | ||
|
||
sealed trait PersonContactInfo extends scala.Product with scala.Serializable { | ||
@inline final def widen: PersonContactInfo = this | ||
} | ||
object PersonContactInfo extends ShapeTag.Companion[PersonContactInfo] { | ||
val id: ShapeId = ShapeId("smithy4s.example", "PersonContactInfo") | ||
|
||
val hints: Hints = Hints( | ||
smithy4s.example.Eq(), | ||
) | ||
|
||
case class EmailCase(email: PersonEmail) extends PersonContactInfo | ||
case class PhoneCase(phone: PersonPhoneNumber) extends PersonContactInfo | ||
|
||
object EmailCase { | ||
val hints: Hints = Hints.empty | ||
val schema: Schema[EmailCase] = bijection(PersonEmail.schema.addHints(hints), EmailCase(_), _.email) | ||
val alt = schema.oneOf[PersonContactInfo]("email") | ||
} | ||
object PhoneCase { | ||
val hints: Hints = Hints.empty | ||
val schema: Schema[PhoneCase] = bijection(PersonPhoneNumber.schema.addHints(hints), PhoneCase(_), _.phone) | ||
val alt = schema.oneOf[PersonContactInfo]("phone") | ||
} | ||
|
||
implicit val schema: Schema[PersonContactInfo] = union( | ||
EmailCase.alt, | ||
PhoneCase.alt, | ||
){ | ||
case c: EmailCase => EmailCase.alt(c) | ||
case c: PhoneCase => PhoneCase.alt(c) | ||
}.withId(id).addHints(hints) | ||
|
||
implicit val personContactInfoEq: cats.Eq[PersonContactInfo] = EqInterpreter.fromSchema(schema) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package smithy4s.example | ||
|
||
import smithy4s.Hints | ||
import smithy4s.Newtype | ||
import smithy4s.Schema | ||
import smithy4s.ShapeId | ||
import smithy4s.example.typeclass.EqInterpreter | ||
import smithy4s.schema.Schema.bijection | ||
import smithy4s.schema.Schema.string | ||
|
||
object PersonEmail extends Newtype[String] { | ||
val id: ShapeId = ShapeId("smithy4s.example", "PersonEmail") | ||
val hints: Hints = Hints( | ||
smithy4s.example.Eq(), | ||
) | ||
val underlyingSchema: Schema[String] = string.withId(id).addHints(hints) | ||
implicit val schema: Schema[PersonEmail] = bijection(underlyingSchema, asBijection) | ||
|
||
implicit val personEmailEq: cats.Eq[PersonEmail] = EqInterpreter.fromSchema(schema) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package smithy4s.example | ||
|
||
import smithy4s.Hints | ||
import smithy4s.Newtype | ||
import smithy4s.Schema | ||
import smithy4s.ShapeId | ||
import smithy4s.example.typeclass.EqInterpreter | ||
import smithy4s.schema.Schema.bijection | ||
import smithy4s.schema.Schema.string | ||
|
||
object PersonPhoneNumber extends Newtype[String] { | ||
val id: ShapeId = ShapeId("smithy4s.example", "PersonPhoneNumber") | ||
val hints: Hints = Hints( | ||
smithy4s.example.Eq(), | ||
) | ||
val underlyingSchema: Schema[String] = string.withId(id).addHints(hints) | ||
implicit val schema: Schema[PersonPhoneNumber] = bijection(underlyingSchema, asBijection) | ||
|
||
implicit val personPhoneNumberEq: cats.Eq[PersonPhoneNumber] = EqInterpreter.fromSchema(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.
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