Search code examples
scalaobjectpattern-matchingextension-methodsimplicit

How to add extension method to a singleton object in scala 2?


I've stumbled upon this thread and there @prolativ provided some fancy scala 3 syntax for creating extension methods for objects

extension (int: Int.type)
  def unapply(s: String): Option[Int] = s.toIntOption

But this got me thinking if there is a way to do something similar in scala 2?

I've tried

implicit class IntOps(s: Int.type) {
  def unapply(s: String): Option[Int] = s.toIntOption
}

Which seemed just scala 2 translation, but it wont compile with error object Int is not a case class, nor does it have a valid unapply/unapplySeq member.

UPD In case of pattern matching there is a way to do this - just add new object and match it:

object IntOps {
  def unapply(s: String): Option[Int] = s.toIntOption
}

but the question remains - how to add extension methods to object?


Solution

  • An implicit class is a correct way to add extension method to an object

    implicit class IntOps(obj: Int.type) {
      def parse(s: String): Option[Int] = s.toIntOption
    }
    
    Int.parse("123") // Some(123)
    

    And unapply can be added in the same way

    implicit class IntOps(obj: Int.type) {
      def unapply(s: String): Option[Int] = s.toIntOption
    }
    
    Int.unapply("123") // Some(123)
    

    Just this will have no impact on pattern matching

    "123" match {
      case Int(i) => ??? // doesn't compile: object Int is not a case class, nor does it have a valid unapply/unapplySeq member
    }
    

    The place where Scala 2 compiler throws object Int is not a case class, nor does it have a valid unapply/unapplySeq member is here: https://github.com/scala/scala/blob/v2.13.10/src/compiler/scala/tools/nsc/typechecker/PatternTypers.scala#L121-L122

    else if (!reallyExists(member))
      CaseClassConstructorError(fun, s"${fun.symbol} is not a case class, nor does it have a valid unapply/unapplySeq member")
    

    This is based on symbols (kind of typeOf[Int.type].decl(TermName("unapply")) in scala-reflect terms). Implicit conversions (extension methods) are not checked here.

    This is modified in Scala 3: https://github.com/lampepfl/dotty/blob/3.2.2/compiler/src/dotty/tools/dotc/typer/Applications.scala#L1291-L1341 The compiler tries to typecheck x.unapply and this includes implicit conversions.

    By the way, when Scala 2 compiler decides whether to generate unapply in the companion object of a case class (Int wasn't a case class) the compiler doesn't check extension methods either

    How can I view the code that Scala uses to automatically generate the apply function for case classes?

    case class MyClass(i: Int)
    
    MyClass(42) match {
      case MyClass(i) => println(i) // 42
    }
    
    case class MyClass(i: Int)
    object MyClass {
      def unapply(mc: MyClass): Option[Int] = Some(100)
    }
    
    MyClass(42) match {
      case MyClass(i) => println(i) // 100
    }
    
    case class MyClass(i: Int)
    
    implicit class MyClassCompanionOps(mc: MyClass.type) {
      def unapply(mc: MyClass): Option[Int] = Some(100)
    }
    
    MyClass(42) match {
      case MyClass(i) => println(i) // 42
    }
    

    And this behavior is the same in Scala 3.


    How to extend String to add new unapply function for using it in extracting?

    enrich PartialFunction with unapply functionality