scalatypespath-dependent-typephantom-typesrefinement-type

What is this Scala syntax?


Take a look at this:

/** takes a Spellbook and returns a Spellbook guaranteeing 
  * that all spells have been loaded from the database. */
 def checkIfSpellsLoaded[S <: Spellbook](spellbook :S) :Option[S { type SpellsLoaded }] =
    if (spellbook.spellsLoaded) Some(spellbook.asInstanceOf[S { type SpellsLoaded }])
    else None
 def checkIfOwnerLoaded[S <: Spellbook](spellbook :S) :Option[S { type OwnerLoaded }] =
     if (spellbook.ownerLoaded) Some(spellbook.asInstanceOf[S { type OwnerLoaded }])
     else None

What is that { type X } doing as part of a type parameter?? What is going on here?


Solution

  • In Scala class members can be def, val and (relevant for us) type

    https://docs.scala-lang.org/tour/abstract-type-members.html

    https://typelevel.org/blog/2015/07/13/type-members-parameters.html

    Scala: Abstract types vs generics

    How to work with abstract type members in Scala

    Type members are used to create path-dependent types

    What is meant by Scala's path-dependent types?

    https://docs.scala-lang.org/scala3/book/types-dependent-function.html

    If Spellbook has type members SpellsLoaded, OwnerLoaded

    trait Spellbook {
      type SpellsLoaded
      type OwnerLoaded
    
      def spellsLoaded: Boolean
      def ownerLoaded: Boolean
    }
    

    then for S <: Spellbook the types S, S { type SpellsLoaded } and S { type OwnerLoaded } are the same

    type S <: Spellbook
    
    implicitly[(S { type SpellsLoaded }) =:= S] // compiles
    implicitly[S =:= (S { type SpellsLoaded })] // compiles
    implicitly[(S { type OwnerLoaded }) =:= S]  // compiles
    implicitly[S =:= (S { type OwnerLoaded })]  // compiles
    

    But if Spellbook doesn't have type members SpellsLoaded, OwnerLoaded

    trait Spellbook {
      // no SpellsLoaded, OwnerLoaded
    
      def spellsLoaded: Boolean
      def ownerLoaded: Boolean
    }
    

    then the refined types S { type SpellsLoaded } and S { type OwnerLoaded } are just subtypes of S (having those type members)

    implicitly[(S { type SpellsLoaded }) <:< S] // compiles
    // implicitly[S <:< (S { type SpellsLoaded })] // doesn't compile
    implicitly[(S { type OwnerLoaded }) <:< S] // compiles
    // implicitly[S <:< (S { type OwnerLoaded })] // doesn't compile
    

    and the refined types S { type SpellsLoaded = ... } and S { type OwnerLoaded = ... } in their turn are subtypes of the former refined types

    implicitly[(S {type SpellsLoaded = String}) <:< (S {type SpellsLoaded})] // compiles
    // implicitly[(S {type SpellsLoaded}) <:< (S {type SpellsLoaded = String})] // doesn't compile
    implicitly[(S {type OwnerLoaded = Int}) <:< (S {type OwnerLoaded})] // compiles
    // implicitly[(S {type OwnerLoaded}) <:< (S {type OwnerLoaded = Int})] // doesn't compile
    

    S { type SpellsLoaded } and S { type OwnerLoaded } are shorthands for S { type SpellsLoaded >: Nothing <: Any } and S { type OwnerLoaded >: Nothing <: Any } while S { type SpellsLoaded = SL } and S { type OwnerLoaded = OL } are shorthands for S { type SpellsLoaded >: SL <: SL } and S { type OwnerLoaded >: OL <: OL }.

    Casting .asInstanceOf[S { type SpellsLoaded }], .asInstanceOf[S { type OwnerLoaded }] looks like SpellsLoaded, OwnerLoaded are used as phantom types

    https://books.underscore.io/shapeless-guide/shapeless-guide.html#sec:labelled-generic:type-tagging (5.2 Type tagging and phantom types)

    So you seem to encode in types that the methods checkIfSpellsLoaded, checkIfOwnerLoaded were applied to S.

    See also

    Confusion about type refinement syntax

    What is a difference between refinement type and anonymous subclass in Scala 3?