Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pattern Match on Case Objects with Type Members

Scala has a nice feature to infer type parameter inside the pattern match. It also checks pattern match exhaustiveness. For example:

sealed trait PField[T]

case object PField1 extends PField[String]

case object PField2 extends PField[Int]

def getValue[X](f: PField[X]): X = f match {
  case PField1 => "aaa"
  case PField2 => 123
}

Is it possible to achieve the same but using type members instead of type parameters?

sealed trait Field {
  type T
}

case object Field1 extends Field {
  type T = String
}

case object Field2 extends Field {
  type T = Int
}

Following solutions do not work (Tested in Scala 2.12.6):

//No exhaustiveness check
def getValue[X](f: Field {type T = X}): X = f match {
  case Field1 => "aaa"
  //    case Field2 => 123
}

//casting needed
def getValue2[X](f: Field {type T = X}): X = (f: Field) match {
  case Field1 => "aaa".asInstanceOf[X]
  case Field2 => 123.asInstanceOf[X]
}

type Generified[X] = Field {type T = X}

//No exhaustiveness check
def getValue3[X](f: Generified[X]): X = f match {
  case Field1 => "aaa"
  //    case Field2 => 123
}

Type parameter is really problematic in my case because I have many sub-hierarchies of Fields and each hierarchy have some type classes. I can't put all the needed dependencies inside case object's because they are exported in a thin JAR to clients.

like image 710
sikor Avatar asked Aug 09 '18 12:08

sikor


People also ask

Which method of case class allows using objects in pattern matching?

The match method takes a number of cases as an argument. Each alternative takes a pattern and one or more expressions that will be performed if the pattern matches.

What is pattern matching explain with example?

Pattern matching in computer science is the checking and locating of specific sequences of data of some pattern among raw data or a sequence of tokens. Unlike pattern recognition, the match has to be exact in the case of pattern matching.

What is Scala pattern matching?

Pattern matching is a way of checking the given sequence of tokens for the presence of the specific pattern. It is the most widely used feature in Scala. It is a technique for checking a value against a pattern. It is similar to the switch statement of Java and C.

How does pattern matching work?

Pattern Matching works by "reading" through text strings to match patterns that are defined using Pattern Matching Expressions, also known as Regular Expressions. Pattern Matching can be used in Identification as well as in Pre-Classification Processing, Page Processing, or Storage Processing.


2 Answers

This solution is a simplified version of solution posted by @Andrey Tyukin. He stated that

it does not hold that a.T = b.T for any two values a, b of type Field3.

It means that, to have exhaustive pattern match, type members must be ignored. So in order to have both exhaustiveness and type inference we need sealed hierarchy with type parameters.

He suggested to create separate hierarchy of case classes and pattern match on them instead of on the main hierarchy. However in my case it can be simplified: I created new sealed trait with type parameter but the same case objects are used for pattern match ("unique guarantee" is held in the object itself). This is final solution:

sealed trait Field {
  type T
  def ug: TField[T]
}

sealed trait TField[G] extends Field {
  type T = G
  def ug: TField[T] = this
}

case object Field1 extends TField[String]
case object Field2 extends TField[Int]

def getValue[X](f: Field {type T = X}): X = (f.ug: TField[X]) match {
  case Field1 => "abc"
  case Field2 => 123
}

Thanks to that I can use Field trait to define type classes without going into higher kinded types and switch into TField[G] for pattern match.

like image 101
sikor Avatar answered Oct 19 '22 01:10

sikor


All your cases that extend Field are singleton objects, so that for each subtype F of Field it holds:

 if
    a: F, b: F, b.T = X
 then
    a.T = X

This does not hold in general, for example for

class Bar { type Q }
case class Field3(b: Bar) extends Field { type T = b.Q }

it does not hold that a.T = b.T for any two values a, b of type Field3.

So, you have to somehow guarantee that all subclasses of Field are well-behaved like Field1 and Field2, and that they are not like hypothetical Field3. You can do this by adding an implicit argument to getValue that acts as a proof that the field is well behaved. For example, proving that the field is indeed a singleton object would be sufficient.

Here is a rough sketch:

sealed trait Field { type T }
case object Field1 extends Field { type T = String }
case object Field2 extends Field { type T = Int }

sealed trait UniqGuarantee[UniqueTypeAsPathDependent]
case class S1UG[X](f: String =:= X) extends UniqGuarantee[X]
case class S2UG[X](f: Int =:= X) extends UniqGuarantee[X]

sealed trait IsSingleton[F <: Field] {
  def apply(singleton: F): UniqGuarantee[singleton.T]
}

implicit object F1_is_Singleton extends IsSingleton[Field1.type] {
  def apply(singleton: Field1.type): UniqGuarantee[singleton.T] = 
    S1UG(implicitly)
}

implicit object F2_is_Singleton extends IsSingleton[Field2.type] {
  def apply(singleton: Field2.type): UniqGuarantee[singleton.T] =
    S2UG(implicitly)
}

def getValue[F <: Field]
  (f: F)
  (implicit sing: IsSingleton[F])
: f.T = sing(f) match {
  case S1UG(uniqGuarantee) => uniqGuarantee("abc")
  case S2UG(uniqGuarantee) => uniqGuarantee(123)
}

This implementation does typecheck, and it also shows warnings if the pattern matching is non-exhaustive.

Admittedly, the solution is pretty heavyweight, because it requires that you implement an entire separate hierarchy of case classes and implicit objects that act as "proofs" that your Fields are indeed singletons.

I think the solution could be shortened quite a bit, I just don't see how right now.

like image 20
Andrey Tyukin Avatar answered Oct 18 '22 23:10

Andrey Tyukin