obeattie
obeattie

Reputation: 3344

Mixing dependent types and 'concrete' types in Scala 3

I'm fairly new to Scala in general, and Scala 3 in particular, and I'm trying to write some code that deals with transparently encoding + decoding values before they are passed to another library.

Basically, I need to map a set of types like Ints to a counterpart in the underlying library. The code I've written is too verbose to replicate here in full, but here's a minimal example demonstrating the kind of thing, using a higher-kinded Encoder type that encapsulates encoding values into types which depend on the values' original types:

trait Encoder[T] {
    type U
    def encode(v: T): U
}

object Encoder {
    given Encoder[Int] with {
        override type U = String

        override def encode(v: Int): String = v.toString
    }
}

case class Value[T : Encoder](v: T) {
    val encoder: Encoder[T] = summon[Encoder[T]]
}

I also need to be able to write functions that deal with specific types of Value and which have 'concrete' return types. Like this:

def doStuff(v1: Value[Int]): String = {
    v1.encoder.encode(v1.v)
}

However, even though in this case v1.codec.encode does indeed return a String, I get an error:

-- [E007] Type Mismatch Error: -------------------------------------------------
2 |    v1.encoder.encode(v1.v)
  |    ^^^^^^^^^^^^^^^^^^^^^^^
  |    Found:    v1.encoder.U
  |    Required: String

What can I do differently to solve this error? Really appreciate any pointers to help a newbie out šŸ™

Upvotes: 2

Views: 229

Answers (2)

yangzai
yangzai

Reputation: 992

I would also suggest giving Match Types a try if we are only talking about Scala 3 here.

import scala.util.Try

type Encoder[T] = T match
  case Int => String
  case String => Either[Throwable, Int]

case class Value[T](v: T):
  def encode: Encoder[T] = v match
    case u: Int => u.toString
    case u: String => Try(u.toInt).toEither


object Main extends App:
  val (v1, v2) = (Value(1), Value(2))
  def doStuff(v: Value[Int]): String =
    v.encode

  println(doStuff(v1) + doStuff(v2)) //12
  println(Value(v1.encode).encode) //Right(1)

Upvotes: 0

Silvio Mayolo
Silvio Mayolo

Reputation: 70267

Answering the question in the comments

Is there any sensible way I tell the compiler that Iā€™m only interested in Values with Encoders that encode to String?

You can force Value to remember its encoder's result type with an extra type argument.

case class Value[T, R](val v: T)(
  using val encoder: Encoder[T],
        val eqv: encoder.U =:= R,
)

The encoder is the same as your encoder, just moved to the using list so we can use it in implicit resolution.

eqv is a proof that R (our type parameter) is equivalent to the encoder's U type.

Then doStuff can take a Value[Int, String]

def doStuff(v1: Value[Int, String]): String = {
    v1.eqv(v1.encoder.encode(v1.v))
}

Let's be clear about what's happening here. v1.encoder.encode(v1.v) returns an encoder.U. Scala isn't smart enough to know what that is. However, we also have a proof that encoder.U is equal to String, and that proof can be used to convert an encoder.U to a String. And that's exactly what =:=.apply does.

We have to do this back in the case class because you've already lost the type information by the time we hit doStuff. Only the case class (which instantiates the implicit encoder) knows what the result type is, so we need to expose it there.

If you have other places in your codebase where you don't care about the result type, you can fill in a type parameter R for it, or use a wildcard Value[Int, ?].

Upvotes: 3

Related Questions