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?
Difference Between Definition and DeclarationDefinition allocates memory to an entity. A declaration does not allocate memory to the entities. Once you define an entity, you cannot keep repeating the definition process again and again.
Purpose. A type declaration statement specifies the type, length, and attributes of objects and functions. You can assign initial values to objects. A declaration type specification (declaration_type_spec) is used in a nonexecutable statement.
Declaration means that variable is only declared and memory is allocated, but no value is set. However, definition means the variables has been initialized. The same works for variables, arrays, collections, etc.
A declaration establishes the names and characteristics of data objects used in a program. A definition allocates storage for data objects, and associates an identifier with that object. When you declare or define a type, no storage is allocated.
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?
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With