library - El "let" equivalente de Clojure en Scala
jinq java 8 (6)
A menudo me enfrento a la siguiente situación: supongo que tengo estas tres funciones
def firstFn: Int = ...
def secondFn(b: Int): Long = ...
def thirdFn(x: Int, y: Long, z: Long): Long = ...
Y también tengo función de calculate
. Mi primer acercamiento puede verse así:
def calculate(a: Long) = thirdFn(firstFn, secondFn(firstFn), secondFn(firstFn) + a)
Se ve hermosa y sin ningún corchete, solo una expresión. Pero no es óptimo, así que termino con este código:
def calculate(a: Long) = {
val first = firstFn
val second = secondFn(first)
thirdFn(first, second, second + a)
}
Ahora es varias expresiones rodeadas de llaves. En esos momentos envidio un poco a Clojure. Con la función let puedo definir esta función en una expresión.
Así que mi objetivo aquí es definir la función de calculate
con una expresión . Se me ocurren 2 soluciones.
1 - Con scalaz puedo definirlo así (hay mejores maneras de hacer esto con scalaz):
def calculate(a: Long) =
firstFn |> {first => secondFn(first) |> {second => thirdFn(first, second, second + a)}}
Lo que no me gusta de esta solución es que está anidado. val
más val
tengo, más profunda es esta anidación.
2 - Con la comprensión puedo lograr algo similar:
def calculate(a: Long) =
for (first <- Option(firstFn); second <- Option(secondFn(first))) yield thirdFn(first, second, second + a)
Por un lado, esta solución tiene una estructura plana, al igual que let
Clojure, pero por otro lado, necesito ajustar los resultados de las funciones en Option
y recibir la Option
como resultado del calculate
(es bueno si estoy tratando con nulos, pero no No ... y no quiero).
¿Hay mejores maneras de lograr mi objetivo? ¿Cuál es la forma idiomática de lidiar con tales situaciones (puede ser que debería quedarme con val
s ... pero let
forma de hacerlo parezca tan elegante)?
Por otro lado está conectado a la transparencia referencial . Las tres funciones son referencialmente transparentes (en mi ejemplo, firstFn
calcula alguna constante como Pi), por lo que teóricamente se pueden reemplazar con los resultados de los cálculos. Lo sé, pero el compilador no, así que no puede optimizar mi primer intento. Y aquí está mi segunda pregunta:
¿Puedo de alguna manera (puede ser con anotación) dar una pista al compilador, de que mi función es referencialmente transparente, para que pueda optimizar esta función para mí (ponga algún tipo de almacenamiento en caché allí, por ejemplo)?
Editar
Gracias a todos por las grandes respuestas! Es simplemente imposible seleccionar la mejor respuesta (puede ser porque son todas muy buenas), así que aceptaré la respuesta con el mayor número de votos, creo que es lo suficientemente justo.
¿Por qué no usar la coincidencia de patrones aquí?
def calcula (a: largo) = primera coincidencia de Fn {caso f => segunda coincidencia de F (f) {caso s => terceraFn (f, s, s + a)}}
¿Qué hay de usar el curry para registrar los valores de retorno de la función (los parámetros de los grupos de parámetros anteriores están disponibles en los grupos de sucesión)?
Un poco extraño pero bastante conciso y sin repetidas invocaciones:
def calculate(a: Long)(f: Int = firstFn)(s: Long = secondFn(f)) = thirdFn(f, s, s + a)
println(calculate(1L)()())
Aquí hay una opción que puede haber pasado por alto.
def calculate(a: Long)(i: Int = firstFn)(j: Long = secondFn(i)) = thirdFn(i,j,j+a)
Si realmente quieres crear un método, esta es la forma en que lo haría.
Alternativamente, podría crear un método (uno podría nombrarlo como) que evite el anidamiento:
class Usable[A](a: A) {
def use[B](f: A=>B) = f(a)
def reuse[B,C](f: A=>B)(g: (A,B)=>C) = g(a,f(a))
// Could add more
}
implicit def use_anything[A](a: A) = new Usable(a)
def calculate(a: Long) =
firstFn.reuse(secondFn)((first, second) => thirdFn(first,second,second+a))
Pero ahora es posible que tenga que nombrar las mismas cosas varias veces.
Quédate con la forma original:
def calculate(a: Long) = {
val first = firstFn
val second = secondFn(first)
thirdFn(first, second, second + a)
}
Es conciso y claro, incluso para los desarrolladores de Java. Es más o menos equivalente a dejar, solo sin limitar el alcance de los nombres.
Si cree que la primera forma es más limpia / más elegante / más legible, ¿por qué no seguirla?
Primero, lea este reciente mensaje de confirmación para el compilador de Scala de Martin Odersky y tómelo en serio ...
Quizás el problema real aquí es saltar al instante al afirmar que es subóptimo. La JVM es bastante buena para optimizar este tipo de cosas. A veces, es simplemente increíble!
Suponiendo que tiene un problema de rendimiento genuino en una aplicación que necesita una aceleración genuina, debe comenzar con un informe del generador de perfiles que demuestre que se trata de un cuello de botella importante, en una JVM adecuadamente configurada y calentada.
Entonces, y solo entonces, deberías buscar formas de hacerlo más rápido para terminar sacrificando la claridad del código.
en el caso no recursivo, sea una reestructuración de lambda.
def firstFn : Int = 42
def secondFn(b : Int) : Long = 42
def thirdFn(x : Int, y : Long, z : Long) : Long = x + y + z
def let[A, B](x : A)(f : A => B) : B = f(x)
def calculate(a: Long) = let(firstFn){first => let(secondFn(first)){second => thirdFn(first, second, second + a)}}
Por supuesto, eso todavía está anidado. No puedo evitar eso. Pero dijiste que te gusta la forma monádica. Así que aquí está la mónada de la identidad.
case class Identity[A](x : A) {
def map[B](f : A => B) = Identity(f(x))
def flatMap[B](f : A => Identity[B]) = f(x)
}
Y aquí está tu cálculo monádico. Desenvuelve el resultado llamando a .x
def calculateMonad(a : Long) = for {
first <- Identity(firstFn)
second <- Identity(secondFn(first))
} yield thirdFn(first, second, second + a)
Pero en este punto seguramente se parece a la versión original de val.
La mónada Identidad existe en Scalaz con más sofisticación.
http://scalaz.googlecode.com/svn/continuous/latest/browse.sxr/scalaz/Identity.scala.html