Scala 3 Manifesto 0.1.0
Over the weekend, I created Scala 3 Manifesto 0.1.0, a small library to re-implement scala.reflect.Manifest
in Scala 3.
Programming languages operate at two levels. First, the material level where bits and bytes are moved to take actions. Second, there is a higher, spiritual level that describes the material level. The first level is the runtime. The second level is the compile-time. Scala programs are written using val
terms that are typed, but at the JVM bytecode, JS, or Native, the variables turn into something different, often more general. For example, a variable typed to List[Int]
becomes List[AnyRef]
at runtime.
Example of Scala 2.x Manifest
In Scala 2.x, scala.reflect.Manifest
provides a mechanism to materialize the spiritual (type) information into the runtime. This is called reification.
Here’s a demonstration of the limitation caused by type erasure:
// BAD EXAMPLE in Scala 2.12.20
package example
object Hello extends App {
println(foo(List("hi")))
def foo(xs: Any): String =
xs match {
case xs: List[Int] => "xs is List[Int]"
case xs: List[String] => "xs is List[String]"
case xs => "xs is unknown"
}
}
If you run the code on either Scala 2.x or 3.x, you get an incorrect answer:
sbt:foo> run
[info] running example.Hello
xs is List[Int]
This is because the pattern match runs at runtime, and List[Int]
and List[String]
are not distinguished.
We can fix this problem by capturing the type information of List("hi")
, and do our own type checking:
package example
import scala.reflect.Manifest
object Hello extends App {
println(foo(List("hi")))
def foo[A1: Manifest](xs: A1): String = {
val m = implicitly[Manifest[A1]]
val mli = implicitly[Manifest[List[Int]]]
val mls = implicitly[Manifest[List[String]]]
xs match {
case xs: List[Int] if m == mli => "xs is List[Int]"
case xs: List[String] if m == mls => "xs is List[String]"
}
}
}
This will print:
sbt:foo> run
[info] running example.Hello
xs is List[String]
Manifest
also provides typeArguments
method to traverse into the type parameters. Generally speaking, Manifest
provides a lightweight interface for metaprogramming without going into the macros.
Scala 3 problem
In Scala 3 Manifest
still works, but it shows a deprecation warning:
[warn] -- Deprecation Warning: src/main/scala/Hello.scala:6:25 -------
[warn] 6 | println(foo(List("hi")))
[warn] | ^
[warn] |Compiler synthesis of Manifest and OptManifest is deprecated, instead
[warn] |replace with the type `scala.reflect.ClassTag[List[String]]`.
[warn] |Alternatively, consider using the new metaprogramming features of Scala 3,
[warn] |see https://docs.scala-lang.org/scala3/reference/metaprogramming.html
ClassTag
doesn’t seem to capture the type information:
// BAD EXAMPLE in Scala 3.5.1
package example
import scala.reflect.ClassTag
@main def hello(): Unit =
println(foo(List("hi"))) // xs is List[Int]
def foo[A1: ClassTag](xs: A1): String =
val m = summon[ClassTag[A1]]
val mli = summon[ClassTag[List[Int]]]
val mls = summon[ClassTag[List[String]]]
xs match
case xs: List[Int] if m == mli => "xs is List[Int]"
case xs: List[String] if m == mls => "xs is List[String]"
sbt:foo> run
[info] running example.hello
xs is List[Int]
Scala 3 also adds a mechanism called TypeTest, which might make the pattern match look nicer, but the default TypeTest
cannot capture the type arguments:
sbt:foo> run
[info] compiling 1 Scala source to xxx/target/out/jvm/scala-3.5.1/foo/backend ...
[warn] -- [E092] Pattern Match Unchecked Warning: xxx/src/main/scala/Hello.scala:7:25
[warn] 7 | println(foo(List("hi")))
[warn] | ^
[warn] |the type test for List[Int] cannot be checked at runtime because its type arguments can't be determined from List[String]
[warn] |
[warn] | longer explanation available when compiling with `-explain`
[warn] one warning found
[info] running example.hello
xs is List[Int]
Manifesto 0.1.0
scalaVersion := "3.5.1"
Compile / scalacOptions += "-deprecation"
libraryDependencies += "com.eed3si9n.manifesto" %% "manifesto" % "0.1.0"
Manifesto is a small library that implements Manifesto[A1]
, which can be used as a replacement of Manifest[A1]
in certain use cases:
package example
import com.eed3si9n.manifesto.Manifesto
@main def hello(): Unit =
println(foo(List("hi"))) // xs is List[String]
def foo[A1: Manifesto](xs: A1): String =
val m = Manifesto[A1]
val mli = Manifesto[List[Int]]
val mls = Manifesto[List[String]]
xs match
case xs: List[Int] if m == mli => "xs is List[Int]"
case xs: List[String] if m == mls => "xs is List[String]"
This works as expected:
sbt:foo> run
[info] running example.hello
xs is List[String]
Manifesto
provides typeArguments
method, which returns a list of Manifesto
s:
package example
import com.eed3si9n.manifesto.Manifesto
@main def hello(): Unit =
println(bar(List("hi"))) // java.lang.String
def bar[A1: Manifesto](xs: A1): String =
val m = Manifesto[A1]
m.typeArguments(0).show
Details
Manifesto is a tree-shaped data structure:
trait Manifesto[A1]:
def typeCon: String
def typeArguments: List[Manifesto[?]]
def isSingleton: Boolean
override def toString(): String = show
def show: String =
if typeArguments.isEmpty then typeCon
else s"""$typeCon[${typeArguments.mkString(",")}]"""
....
end Manifesto
object Manifesto:
def apply[A1: Manifesto]: Manifesto[A1] = summon[Manifesto[A1]]
inline given [A1]: Manifesto[A1] = Derivation.derived[A1]
end Manifesto
The derivation code is a relatively simple Scala 3 macro that traverses over the TypeRepr
. TypeRepr
provides access to the type information during compile-time. So we can retreieve the class name at each level, and recursively creates Manifesto
structure.
Manifest
provides more features, but for my own use case having this information allows me to cross build with both Scala 2.x and 3.x with a minimum shim.
Prior works
There are several projects that are related to this idea: