What is the meaning of a type declaration without definition in an object?
Asked Answered
G

1

5

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?

Ganda answered 10/1, 2019 at 21:59 Comment(0)
M
4

For a method (or value) there are only 2 options: either it has body (and then it is "concrete") or it doesn't (then it is "abstract"). A type X is always some type interval X >: LowerBound <: UpperBound (and we call it concrete if LowerBound = UpperBound or completely abstract if LowerBound = Nothing, UpperBound = Any but there is variety of cases between those). So if we'd like to forbid abstract types in objects we should always have way to check that types LowerBound and UpperBound are equal. But they can be defined in some complex way and generally such check can be not so easy:

object Example {
  type X >: N#Add[N] <: N#Mult[Two] // Do we expect that compiler proves n+n=n*2?
}
Multistage answered 10/1, 2019 at 22:31 Comment(2)
Still, for the type to be "not completely abstract" does require presence of the lower/upper bound constraints; "bare" abstract types, without any constraints, are what you call "completely abstract", and, as far as I can see, they could have been causing compilation errors, and it wouldn't have prevent writing any useful programs. Same logic actually applies even to bounded types too: is there any useful program which could be written using this feature?Ganda
But I admit I didn't even think about subtype/supertype boundaries and how they could be related to this; it is very interesting, thanks for the explanation!Ganda

© 2022 - 2024 — McMap. All rights reserved.