Search code examples
scalapattern-matchingextractorunapply

Scala: Workaround for unparameterizable extractor


Since extractors cannot take custom parameters (as answered in Stack Overflow: Can extractors be customized...), I try to find an alternative way of solving the following problem.

I have a lot of translations which can be combined. In my code snippet, a dimension can be combined with a factor. For instance "width multiplied by 2". But it can also be "width" (unmultiplied). And there will be further cases like that. I try to classify those string inputs using pattern matching. "width" and "width multiplied by x" should be classified as "width" (key "w"), "height" and "height multiplied by x" should be classified as "height" (key "h"), and so on.

That should be done by the last match in the following example code snippet, which will contain many cases (6 in the example code snippet) each of which should take a key: String parameter ("w", "h", "l", "r", "t", "b").

What I try to achieve is passing the key (that is "w", "h", "l", "r", "t", "b" and so on) to the case Untranslation(v). But obviously I cannot do that (the unapply function can take implicit parameters, but no additional explicit ones).

Now I try to find an alternative but still concise way of classifying my string inputs.

implicit val translations = Map(
  "w" -> "width",
  "h" -> "height",
  "l" -> "left",
  "r" -> "right",
  "t" -> "top",
  "b" -> "bottom",
  // + some more translations
  "m" -> "multiplied by"
)

sealed trait CommandType
object CommandType {
  case object Unmodified extends CommandType
  case object Multiplied extends CommandType
  // ...
}

object Untranslation {
  def unapply(s: String)(implicit t: Map[String, String]): Option[CommandType] = {
    val key: String = "w" // should be variable by case
    val a: List[String] = t(key).split(" ").toList
    val b: List[String] = t("m").split(" ").toList
    val ab: List[String] = a ++ b
    s.split(" ").toList match {
      case `a` => Some(CommandType.Unmodified)
      case `ab` :+ value => Some(CommandType.Multiplied)
      // + some more cases
      case _ => None
    }
  }
}

"width multiplied by 2" match {
  case Untranslation(v) => println(v) // here I would like to pass the key ("w"/"h"/"l"/...)
  case _ => println("nothing found")
}
// outputs: Multiplied

Solution

  • Possibly your question duplicates this one.

    package ex
    
    import language._
    
    object units extends Dynamic {
      class Helper(kind: String) {
        val kindof = kind match {
          case "s" => Symbols.s
          case "m" => Symbols.m
        }
        def value = raw"(\d+)${kindof.name}".r
        object pair {
          def unapply(s: String): Option[(Int, Symbol)] =
            value.unapplySeq(s).map(vs => (vs.head.toInt, kindof))
        }
      }
      def selectDynamic(kind: String) = new Helper(kind)
      object Symbols { val s = 'sec ; val m = 'min }
    }
    
    object Test {
      def main(args: Array[String]): Unit = println {
        args(0) match {
          case units.s.pair(x, s) => s"$x ${s.name}"
          case units.s.value(x) => s"$x seconds"
          case units.m.value(x) => s"$x minutes"
        }
      }
    }
    

    The customization is built into the selection in the case expression. That string is used to construct the desired extractor.

    $ scalac ex.scala && scala ex.Test 24sec
    24 sec
    
    $ scalac ex.scala && scala ex.Test 60min
    60 minutes