Включение строк
Мне было любопытно посмотреть, как реализовать 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))
}
}
Обратите внимание, что это решение не обрабатывает хеш-коллизии. Поскольку конкретные строки, которые меня волнуют в моей реальной проблеме, не сталкиваются, я еще не пересекал этот конкретный мост.