ideaboxer
ideaboxer

Reputation: 4101

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

Upvotes: 0

Views: 374

Answers (3)

som-snytt
som-snytt

Reputation: 39577

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

Upvotes: 2

Alexey Romanov
Alexey Romanov

Reputation: 170745

You can easily create a parameterized class for extractors instead of an object:

class Untranslation(val key: String) {
  def unapply(s: String)(implicit t: Map[String, String]): Option[CommandType] = {
    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
    }
  }
}

To match, an extractor needs to have a stable identifier, which can be done by assigning it to a val (so you unfortunately need an extra line for each key, but of course they can be used in multiple matches):

val UntranslationW = new Untranslation("w")
val UntranslationT = new Untranslation("t")
...

"width multiplied by 2" match {
  case UntranslationW(v) => ...
  case UntranslationT(v) => ...
  case _ => println("nothing found")
}

Upvotes: 3

Andrey Tyukin
Andrey Tyukin

Reputation: 44918

Whether you want to implement a proper parser or not, you should at least create the data structures that can represent your commands faithfully.

Here is one proposal:

sealed trait Dimension {
  def translate(implicit t: Map[Symbol, String]) = 
    t(Symbol(toString.toLowerCase))
}
case object W extends Dimension
case object H extends Dimension
case object L extends Dimension
case object R extends Dimension
case object T extends Dimension
case object B extends Dimension
object Dimension {
  def all = List(W, H, L, R, T, B)
}

sealed trait CommandModifier {
  def translate(implicit t: Map[Symbol, String]): String
}
case object Unmodified extends CommandModifier {
  def translate(implicit t: Map[Symbol, String]) = ""
}
case class Multiplied(factor: Int) extends CommandModifier {
  def translate(implicit t: Map[Symbol, String]) = t('m) + " " + factor
}


case class Command(dim: Dimension, mod: CommandModifier) {
  def translate(implicit t: Map[Symbol, String]) = 
    dim.translate + " " + mod.translate
}

A Command is a proper case class that has the dimension and the modifier as member. The CommandModifiers are modeled as a separate sealed trait. The Dimensions (width, height etc.) are essentially just an enumeration. The short magic-value Strings "w", "h" have been replaced by symbols 'w, 'h etc.

Now you can implement an Untranslation extractor that extracts the entire command in one go, and therefore does not need any additional parameters:

object Untranslation {
  def unapply(s: String)(implicit t: Map[Symbol, String]): Option[Command] = {
    val sParts = s.split(" ").toList
    for (dim <- Dimension.all) {
      val a: List[String] = dim.translate.split(" ").toList
      val b: List[String] = t('m).split(" ").toList
      val ab: List[String] = a ++ b
      sParts match {
        case `a` => return Some(Command(dim, Unmodified))
        case `ab` :+ value => return Some(Command(dim, Multiplied(value.toInt)))
        // + some more cases
        case _ => None
      }
    }
    None
  }
}

A small example. Here is how you can parse and write out commands in English and German. First, the two dictionaries that map the formal symbols to actual words in a natural language:

val En = Map(
  'w -> "width",
  'h -> "height",
  'l -> "left",
  'r -> "right",
  't -> "top",
  'b -> "bottom",
  'm -> "multiplied by"
)

val De = Map(
  'w -> "Breite",
  'h -> "Höhe",
  'l -> "links",
  'r -> "rechts",
  't -> "oben",
  'b -> "unten",
  'm -> "mal"
)

Using the En-dictionary, you can now match commands in English:

for (example <- List(
  "width multiplied by 2",
  "top",
  "height multiplied by 42"
)) {
  println("-" * 60)
  implicit val lang = En
  example match {
    case Untranslation(v) => {
      println(v)
      println(v.translate(En))
      println(v.translate(De))
    }
    case _ => println("invalid command")
  }
}

Here is what is matched, and how it is translated in both English and German:

------------------------------------------------------------
Command(W,Multiplied(2))
width multiplied by 2
Breite mal 2
------------------------------------------------------------
Command(T,Unmodified)
top 
oben 
------------------------------------------------------------
Command(H,Multiplied(42))
height multiplied by 42
Höhe mal 42

The same works the other way round, from German to English:

for (example <- List(
  "Breite mal 2",
  "oben",
  "Höhe mal 42"
)) {
  println("-" * 60)
  implicit val lang = De
  example match {
    case Untranslation(v) => {
      println(v)
      println(v.translate(En))
      println(v.translate(De))
    }
    case _ => println("invalid command")
  }
}

Output:

------------------------------------------------------------
Command(W,Multiplied(2))
width multiplied by 2
Breite mal 2
------------------------------------------------------------
Command(T,Unmodified)
top 
oben 
------------------------------------------------------------
Command(H,Multiplied(42))
height multiplied by 42
Höhe mal 42

Note that the entire approach with string splitting and pattern matching is extremely brittle, and does not scale at all. If you want to do it properly, you have to write a proper parser (either using a parser generator, or using a parser combinator library).

Upvotes: 1

Related Questions