racetrack
racetrack

Reputation: 3756

Phantom existential types in Scala

I'm trying to write a value class that wraps Scala collection's Map and provides an alternative get. I'm trying to use a phantom type in the value class and tag the Key with the same type using the method member. If the result of member is Some(k) then the user should be able to call get(k) and get a V instead of an Option[V].

import scala.collection.{Map => M}

class Key[PH, K] private (val k: K) extends AnyVal

object Key {
  def apply[PH, K](k: K): Key[PH, K] = new Key(k)
}

class Map[PH, K, V] private (val m: M[K, V]) extends AnyVal {
  def member(k: K): Option[Key[PH, K]] = m.get(k).map(_ => Key(k))

  def get(key: Key[PH, K]): V = m.get(key.k).get
}

object Map {
  def apply[PH, K, V](m: M[K, V]): Map[PH, K, V] = new Map(m)
}

def withMap[K, V, T](m: M[K, V])(cont: (Map[PH, K, V] forSome { type PH }) => T): T = cont(Map(m))

withMap(M("a" -> "a")){ m =>
  m.member("a") match {
    case Some(v) => println(m.get(v))
    case None => println(":(")
  }
}

But currently it fails compiling with the following error:

found   : Key[PH(in value $anonfun),String] where type +PH(in value $anonfun)
required: Key[PH(in value cont),String]
    case Some(v) => println(m.get(v))

How can I convince scalac that the PHs are the same?

Upvotes: 2

Views: 169

Answers (2)

HTNW
HTNW

Reputation: 29148

Phantom types are meaningless. You should say what you mean: every Key belongs to a certain Map:

import scala.collection.immutable.Map // it is not safe to use Maps in general!

class KeyedMap[K, V](val m: Map[K, V]) extends AnyVal {
  import KeyedMap._
  def member(k: K): Option[Key[K, V, m.type]] = m.get(k).map { _ => new Key[K, V, m.type](k) }
  def fromKey(k: Key[K, V, m.type]): V = m(k.k)
}
object KeyedMap {
  //                              vvvvvvvvvvvvvv requires this parameter to be <something>.type
  class Key[K, +V, M <: Map[K, V] with Singleton] private[KeyedMap](val k: K) extends AnyVal
}

object Test {
  def main(args: String*): Unit = {
    val m = new KeyedMap(Map("a" -> "b"))
    m.member("a") match {
      case Some(v) => println(m.fromKey(v))
      case None => println(":(")
    }
  }
}

Upvotes: 1

HTNW
HTNW

Reputation: 29148

Destructure the existential:

withMap(M("a" -> "a")) { case m =>
  m.member("a") match {
    case Some(v) => println(m.get(v))
    case None => println(":(")
  }
}

This abbreviates

withMap(M("a" -> "a")) { case m: Map[ph, String, String] => // name the phantom type ph, name the map m =>
  m.member("a") match {
    case Some(v) => println(m.get(v))
    case None => println(":(")
  }
}

The destructuring allows m to be given a non-existential type in terms of a newly introduced type variable. This means that every occurrence of m can now have the same type.

Upvotes: 2

Related Questions