Scala allows to define types using the type
keyword, which usually have slightly different meaning and purpose depending on when they are declared.
If you use type
inside an object or a package object, you'd define a type alias, i.e. a shorter/clearer name for another type:
package object whatever {
type IntPredicate = Int => Boolean
def checkZero(p: IntPredicate): Boolean = p(0)
}
Types declared in classes/traits are usually intended to be overridden in subclasses/subtraits, and are also eventually resolved to a concrete type:
trait FixtureSpec {
type FixtureType
def initFixture(f: FixtureType) = ...
}
trait SomeSpec extends FixtureSpec {
override type FixtureType = String
def test(): Unit = {
initFixture("hello")
...
}
}
There are other uses for abstract type declarations, but anyway they eventually are resolved to some concrete types.
However, there is also an option to declare an abstract type (i.e. without actual definition) inside an object:
object Example {
type X
}
And this compiles, as opposed to e.g. abstract methods:
object Example {
def method: String // compilation error
}
Because objects cannot be extended, they can never be resolved to concrete types.
I assumed that such type definitions could be conveniently used as phantom types. For example (using Shapeless' tagged types):
import shapeless.tag.@@
import shapeless.tag
type ++>[-F, +T]
trait Converter
val intStringConverter: Converter @@ (String ++> Int) = tag[String ++> Int](...)
However, it seems that the way the type system treats these types is different from regular types, which causes the above usage of "abstract" types to fail in certain scenarios.
In particular, when looking for implicit parameters, Scala eventually looks into implicit scope associated with "associated" types, i.e. types which are present in the type signature of the implicit parameters. However, it seems that there is some limitation on nesting of these associated types when "abstract" types are used. Consider this example setup:
import shapeless.tag.@@
trait Converter
type ++>[-F, +T]
case class DomainType()
object DomainType {
implicit val converter0: Converter @@ DomainType = null
implicit val converter1: Converter @@ Seq[DomainType] = null
implicit val converter2: Converter @@ (Seq[String] ++> Seq[DomainType]) = null
}
// compiles
implicitly[Converter @@ DomainType]
// compiles
implicitly[Converter @@ Seq[DomainType]]
// fails!
implicitly[Converter @@ (Seq[String] ++> Seq[DomainType])]
Here, the first two implicit resolutions compile just fine, while the last one fails with an error about a missing implicit. If I define the implicit in the same scope as the implicitly
call, it then compiles:
implicit val converter2: Converter @@ (Seq[String] ++> Seq[DomainType]) = null
// compiles
implicitly[Converter @@ (Seq[String] ++> Seq[DomainType])]
However, if I change the ++>
definition to be a trait
rather than type
:
trait ++>[-F, +T]
then all implicitly
calls above compile just fine.
Therefore, my question is, what exactly is the purpose of such type declarations? What problems they are intended to solve, and why are they not prohibited, like other kinds of abstract members in objects?