Providing Typeclasses

Imagine you have a typeclass for serialization, for example:

trait SerializeToString[T] {
  def serialize(t: T): String
}
object SerializeToString {
  given SerializeToString[String] = str => str
  given SerializeToString[Long] = _.toString
}

trait DeserializeFromString[T] {
  def deserialize(s: String): Either[String, T]
}
object DeserializeFromString {
  given DeserializeFromString[String] = Right(_)
  given DeserializeFromString[Long] = str => str.toLongOption.toRight(s"Not a long: $str")
}

And now you have your newtypes:

import yantl.*

object Age extends Newtype.ValidatedOf(IArray(ValidatorRule.minValue(0L)))
type Age = Age.Type

object Name extends Newtype.WithoutValidationOf[String]
type Name = Name.Type

Wouldn't it be nice if these typeclass instances could be provided for your newtypes automatically?

given newTypeSerializeToString[TUnderlying, TWrapper](using
  newType: yantl.Newtype.WithType[TUnderlying, TWrapper],
  serializer: SerializeToString[TUnderlying],
): SerializeToString[TWrapper] =
  t => serializer.serialize(t.unwrap)

given newTypeDeserializeFromString[TUnderlying, TWrapper, TError](using
  newType: yantl.Newtype.WithTypeAndError[TUnderlying, TWrapper, TError],
  asString: yantl.AsString[TError],
  deserializer: DeserializeFromString[TUnderlying],
): DeserializeFromString[TWrapper] =
  str => deserializer.deserialize(str).flatMap(newType.make.asString)

// You need to define how to convert the error messages to strings first.
given [A]: AsString[A] = AsString.fromToString

// The instances are automatically provided
summon[SerializeToString[Age]]
// res0: SerializeToString[Type] = repl.MdocSession$MdocApp$$Lambda$13639/0x00000001035bc040@5a914caa
summon[DeserializeFromString[Age]]
// res1: DeserializeFromString[Type] = repl.MdocSession$MdocApp$$Lambda$13641/0x00000001035bd040@4fb6e08c

summon[SerializeToString[Name]]
// res2: SerializeToString[Type] = repl.MdocSession$MdocApp$$Lambda$13639/0x00000001035bc040@4b00116e
summon[DeserializeFromString[Name]]
// res3: DeserializeFromString[Type] = repl.MdocSession$MdocApp$$Lambda$13641/0x00000001035bd040@7307b030

You can also provide instances only for unvalidated newtypes:

given newTypeDeserializeUnvalidatedFromString[TUnderlying, TWrapper](using
  newType: yantl.Newtype.WithUnvalidatedType[TUnderlying, TWrapper],
  deserializer: DeserializeFromString[TUnderlying],
): DeserializeFromString[TWrapper] =
  str => deserializer.deserialize(str).map(newType.apply)

// The instance is automatically provided
summon[DeserializeFromString[Name]]
// res4: DeserializeFromString[Type] = repl.MdocSession$MdocApp$$Lambda$13644/0x00000001035be440@74a5c347