software - scala vs java
Patrón de coincidencia vs if-else (8)
Soy un novato en Scala. Hace poco escribí una aplicación de hobby y me puse a prueba tratando de usar patrones de coincidencia en lugar de if-else en muchos casos.
user.password == enteredPassword match {
case true => println("User is authenticated")
case false => println("Entered password is invalid")
}
en lugar de
if(user.password == enteredPassword)
println("User is authenticated")
else
println("Entered password is invalid")
¿Son estos enfoques iguales? ¿Es uno de ellos más preferible que otro por alguna razón?
Ambas declaraciones son equivalentes en términos de semántica de código. Pero es posible que el compilador cree un código más complicado (y, por lo tanto, ineficiente) en un caso (la match
).
La coincidencia de patrones se usa generalmente para separar construcciones más complicadas, como expresiones polimórficas o deconstruir unapply
(sin unapply
) en sus componentes. No aconsejaría utilizarlo como sustituto de una declaración simple de if-else : no hay nada de malo en if-else .
Tenga en cuenta que puede usarlo como una expresión en Scala. Por lo tanto, puedes escribir
val foo = if(bar.isEmpty) foobar else bar.foo
Me disculpo por el estúpido ejemplo.
En mi entorno (scala 2.12 y java 8) obtengo resultados diferentes. Match realiza consistentemente mejor en el código anterior:
timeIf: Long = 249 timeMatch: Long = 68
Estoy aquí para ofrecer una opinión diferente: para el ejemplo específico que ofrece, el segundo (si ... más ...) es mejor porque es mucho más fácil de leer.
De hecho, si coloca su primer ejemplo en IntelliJ, le sugerirá que cambie al segundo estilo (si ... más ...). Aquí está la sugerencia de estilo IntelliJ:
Trivial match can be simplified less... (⌘F1)
Suggests to replace trivial pattern match on a boolean expression with a conditional statement.
Before:
bool match {
case true => ???
case false => ???
}
After:
if (bool) {
???
} else {
???
}
Me encontré con la misma pregunta, y había escrito pruebas:
def factorial(x: Int): Int = {
def loop(acc: Int, c: Int): Int = {
c match {
case 0 => acc
case _ => loop(acc * c, c - 1)
}
}
loop(1, x)
}
def factorialIf(x: Int): Int = {
def loop(acc: Int, c: Int): Int =
if (c == 0) acc else loop(acc * c, c - 1)
loop(1, x)
}
def measure(e: (Int) => Int, arg:Int, numIters: Int): Long = {
def loop(max: Int): Unit = {
if (max == 0)
return
else {
val x = e(arg)
loop(max-1)
}
}
val startMatch = System.currentTimeMillis()
loop(numIters)
System.currentTimeMillis() - startMatch
}
val timeIf = measure(factorialIf, 1000,1000000)
val timeMatch = measure(factorial, 1000,1000000)
timeIf: Long = 22 timeMatch: Long = 1092
No coincida el patrón en un solo booleano; usa un if-else.
Por cierto, el código está mejor escrito sin duplicar println
.
println(
if(user.password == enteredPassword)
"User is authenticated"
else
"Entered password is invalid"
)
Para la gran mayoría del código que no es sensible al rendimiento, hay muchas razones por las que desea utilizar la coincidencia de patrones en if / else:
- hace cumplir un valor y tipo de devolución común para cada una de sus ramas
- en idiomas con controles de exhaustividad (como Scala), te obliga a considerar explícitamente todos los casos (y noop los que no necesitas)
- previene las devoluciones anticipadas, que se vuelven más difíciles de razonar si se multiplican en cascada, o las ramas crecen más que la altura de la pantalla (en ese punto se vuelven invisibles). Tener un nivel extra de sangría te avisará que estás dentro de un alcance.
- puede ayudarte a identificar la lógica para sacar. En este caso, el código podría haber sido reescrito y hecho más DRY, depurable y comprobable como este:
val errorMessage = user.password == enteredPassword match {
case true => "User is authenticated"
case false => "Entered password is invalid"
}
println(errorMesssage)
Aquí hay una implementación de bloque if / else equivalente:
var errorMessage = ""
if(user.password == enteredPassword)
errorMessage = "User is authenticated"
else
errorMessage = "Entered password is invalid"
println(errorMessage)
Sí, puedes argumentar que para algo tan simple como una verificación booleana puedes usar una expresión if. Pero eso no es relevante aquí y no se adapta bien a las condiciones con más de 2 ramas.
Si su mayor preocupación es la facilidad de mantenimiento o legibilidad, la coincidencia de patrones es increíble y debe usarlo incluso para cosas menores.
Una forma posiblemente mejor sería emparejar patrones en la cadena directamente, no en el resultado de la comparación, ya que evita la "ceguera booleana". http://existentialtype.wordpress.com/2011/03/15/boolean-blindness/
Una desventaja es la necesidad de usar comillas inversas para proteger la variable de contraseña introducida de la sombra.
Básicamente, debe evitar comerciar con booleanos tanto como sea posible, ya que no transmiten ninguna información al nivel de tipo.
user.password match {
case `enteredPassword` => Right(user)
case _ => Left("passwords don''t match")
}
class MatchVsIf {
def i(b: Boolean) = if (b) 5 else 4
def m(b: Boolean) = b match { case true => 5; case false => 4 }
}
No estoy seguro de por qué querría utilizar la segunda versión más larga y clunkier.
scala> :javap -cp MatchVsIf
Compiled from "<console>"
public class MatchVsIf extends java.lang.Object implements scala.ScalaObject{
public int i(boolean);
Code:
0: iload_1
1: ifeq 8
4: iconst_5
5: goto 9
8: iconst_4
9: ireturn
public int m(boolean);
Code:
0: iload_1
1: istore_2
2: iload_2
3: iconst_1
4: if_icmpne 11
7: iconst_5
8: goto 17
11: iload_2
12: iconst_0
13: if_icmpne 18
16: iconst_4
17: ireturn
18: new #14; //class scala/MatchError
21: dup
22: iload_2
23: invokestatic #20; //Method scala/runtime/BoxesRunTime.boxToBoolean:(Z)Ljava/lang/Boolean;
26: invokespecial #24; //Method scala/MatchError."<init>":(Ljava/lang/Object;)V
29: athrow
Y eso es mucho más bytecode para el partido también. Es bastante eficiente incluso (no hay boxeo a menos que el partido arroje un error, que no puede suceder aquí), pero para compacidad y rendimiento uno debería favorecer a if
/ else
. Sin embargo, si mejora la claridad de su código mediante el uso de coincidencias, continúe (excepto en aquellos casos excepcionales en los que sabe que el rendimiento es crítico, y luego es posible que desee comparar la diferencia).