Включение строк

Мне было любопытно посмотреть, как реализовать Java и Scala при включении строк:

class Java
{
    public static int java(String s)
    {
        switch (s)
        {
        case "foo": return 1;
        case "bar": return 2;
        case "baz": return 3;
        default: return 42;
        }
    }
}
object Scala {
  def scala(s: String): Int = {
    s match {
      case "foo" => 1
      case "bar" => 2
      case "baz" => 3
      case _ => 42
    }
  }
}

Кажется, что Java переключается на хэш-код, а затем сравнивает одну строку:

 0: aload_0       
 1: dup           
 2: astore_1      
 3: invokevirtual #16    // Method java/lang/String.hashCode:()I
 6: lookupswitch  { // 3
           97299: 40
           97307: 52
          101574: 64
         default: 82
    }
40: aload_1       
41: ldc           #22    // String bar
43: invokevirtual #24    // Method java/lang/String.equals:(Ljava/lang/Object;)Z
46: ifne          78
49: goto          82
52: aload_1       
53: ldc           #28    // String baz
55: invokevirtual #24    // Method java/lang/String.equals:(Ljava/lang/Object;)Z
58: ifne          80
61: goto          82
64: aload_1       
65: ldc           #30    // String foo
67: invokevirtual #24    // Method java/lang/String.equals:(Ljava/lang/Object;)Z
70: ifne          76
73: goto          82
76: iconst_1      
77: ireturn       
78: iconst_2      
79: ireturn       
80: iconst_3      
81: ireturn       
82: bipush        42
84: ireturn       

Напротив, Scala, по-видимому, сравнивается со всеми случаями:

 0: aload_1       
 1: astore_2      
 2: ldc           #16    // String foo
 4: aload_2       
 5: invokevirtual #20    // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
 8: ifeq          16
11: iconst_1      
12: istore_3      
13: goto          47
16: ldc           #22    // String bar
18: aload_2       
19: invokevirtual #20    // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
22: ifeq          30
25: iconst_2      
26: istore_3      
27: goto          47
30: ldc           #24    // String baz
32: aload_2       
33: invokevirtual #20    // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
36: ifeq          44
39: iconst_3      
40: istore_3      
41: goto          47
44: bipush        42
46: istore_3      
47: iload_3       
48: ireturn       

Можно ли убедить Scala использовать хэш-код? Я предпочел бы решение O (1) решения O (n). В моем реальном коде мне нужно сравнить с 33 возможными ключевыми словами.

Ответы

Ответ 1

Определенно, кажется, что этот случай является недостатком оптимизации из компилятора Scala. Несомненно, конструкция match намного (намного) мощнее, чем коммутатор/случай в Java, и ее намного сложнее оптимизировать, но она может обнаружить эти особые случаи, в которых будет применяться простое хеш-сравнение.

Кроме того, я не думаю, что этот случай будет отображаться много раз в идиоматическом Scala, потому что вы всегда соответствуете классам case, которые имеют некоторое значение, отличное от другого значения.

Ответ 2

Я думаю, проблема в том, что вы думаете о Scala с точки зрения Java (я думаю, что вы также преждевременно оптимизируете, но эй).

Я бы подумал, что решение, которое вы хотите, должно вместо этого сохранить ваше отображение. У вас есть функция, которая отображает из String → Int, правильно? Так сделайте это:

class Memoize1[-T, +R](f: T => R) extends (T => R) {
  import scala.collection.mutable
  private[this] val vals = mutable.Map.empty[T, R]

  def apply(x: T): R = {
    if (vals.contains(x)) {
      vals(x)
    }
    else {
      val y = f(x)
      vals += ((x, y))
      y
    }
  }
}

object Memoize1 {
  def apply[T, R](f: T => R) = new Memoize1(f)
}

(этот memoizing код взят из здесь.

Затем вы можете memoize свой код следующим образом:

object Scala {
  def scala(s: String): Int = {
    s match {
      case "foo" => 1
      case "bar" => 2
      case "baz" => 3
      case _ => 42
    }
  }

  val memoed = Memoize1(Scala.scala)

  val n = memoed("foo")
}

Тада! Теперь вы делаете сравнения хеш-значений. Хотя я добавлю, что большинство примеров memoization (включая этот) являются игрушками и не выживут в большинстве случаев. В режиме реального времени memoization должен включать верхний предел в размере, которое вы готовы кэшировать, а в случае вашего кода, где у вас крошечный число возможных действительных случаев и огромное количество недействительных случаев, я бы подумал о создании общего класса, который предварительно создает карту и имеет специализированный поиск, который гласит: "в моем кеше вы выигрываете, а не в моем кеше, по умолчанию". что можно сделать очень легко, настроив memoizer, чтобы взять List ввода для проверки и изменить код "не в кеше", чтобы вернуть значение по умолчанию.

Ответ 3

Эта проблема побудила меня узнать о макросах Scala, и я мог бы также поделиться своим решением.

Вот как я использую макрос:

switch(s, 42, "foo", "bar", "baz")

Связанные значения подсчитываются автоматически. Если это не то, что вы хотите, вы можете изменить реализацию, чтобы принять ArrowAssoc вместо этого, но это было слишком сложно для меня.

И вот как реализуется макрос:

import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context
import scala.collection.mutable.ListBuffer

object StringSwitch {

  def switch(value: String, default: Long, cases: String*): Long =
    macro switchImpl

  def switchImpl(c: Context)(value: c.Expr[String], default: c.Expr[Long],
                             cases: c.Expr[String]*): c.Expr[Long] = {
    import c.universe._

    val buf = new ListBuffer[CaseDef]
    var i = 0
    for (x <- cases) {
      x match {
        case Expr(Literal(Constant(y))) =>
          i += 1
          buf += cq"${y.hashCode} => if ($x.equals($value)) $i else $default"

        case _ => throw new AssertionError("string literal expected")
      }
    }
    buf += cq"_ => $default"

    c.Expr(Match(q"$value.hashCode", buf.toList))
  }
}

Обратите внимание, что это решение не обрабатывает хеш-коллизии. Поскольку конкретные строки, которые меня волнуют в моей реальной проблеме, не сталкиваются, я еще не пересекал этот конкретный мост.