scala haskell language-features where-clause

¿Scala equivalente a las cláusulas de Haskell?



language-features where-clause (4)

¿Es posible usar algo similar a las cláusulas where en Scala? Tal vez hay algún truco que no pensé?

Editar:

Gracias por todas sus respuestas, son muy apreciadas. En resumen: vars locales, vals y defs se pueden utilizar para lograr casi la misma cosa. Para la evaluación perezosa, se puede usar la val valiosa (con almacenamiento en caché implícito) o definiciones de funciones. Asegurar la pureza funcional se deja al programador.

Ahora solo queda una pregunta: ¿hay una manera de poner las definiciones de valores o funciones después de las expresiones en las que se usan? A veces eso parece mucho más claro. Esto es posible con los campos / métodos de una clase u objeto, pero no parece funcionar dentro de los métodos.

Otra cosa que no fue mencionada en las respuestas hasta ahora. Las cláusulas where también limitan el alcance de las expresiones definidas en ellas. Tampoco he encontrado una manera de lograrlo en Scala.


En Hakell, donde las cláusulas contienen definiciones locales de una función. Scala no tiene cláusulas donde explícitas, pero se puede lograr la misma funcionalidad al tener var , val y def locales.

`Var` local y` val`

En Scala:

def foo(x: Int, y: Int): Int = { val a = x + y var b = x * y a - b }

En Haskell:

foo :: Integer -> Integer -> Integer foo x y = a - b where a = x + y b = x * y

Local `def`

En scala

def foo(x: Int, y: Int): Int = { def bar(x: Int) = x * x y + bar(x) }

En haskell

foo :: Integer -> Integer -> Integer foo x y = y + bar x where bar x = x * x

Corríjame si he cometido algún error de sintaxis en el ejemplo de Haskell, ya que actualmente no tengo ningún compilador de Haskell instalado en esta computadora :).

Se pueden lograr ejemplos más complicados de maneras similares (por ejemplo, mediante la comparación de patrones, que ambos idiomas admiten). Las funciones locales tienen exactamente la misma sintaxis que cualquier otra función, solo que su alcance es el bloque en el que se encuentran.

EDITAR : También vea la respuesta de Daniel para un ejemplo de este tipo y alguna explicación sobre el tema.

EDITAR 2 : Se agregó una discusión acerca de las lazy y val lazy .

Lazy `var` y` val`

La respuesta de Edward Kmett señaló acertadamente que la cláusula de Haskell en donde tiene pereza y pureza. Puedes hacer algo muy similar en Scala usando variables lazy . Estos solo son instanciados cuando es necesario. Considere el siguiente ejemplo:

def foo(x: Int, y: Int) = { print("--- Line 1: "); lazy val lazy1: Int = { print("-- lazy1 evaluated "); x^2} println(); print("--- Line 2: "); lazy val lazy2: Int = { print("-- lazy2 evaluated "); y^2} println(); print("--- Line 3: "); lazy val lazy3: Int = { print("-- lazy3 evaluated ") while(true) {} // infinite loop! x^2 + y^2 } println(); print("--- Line 4 (if clause): "); if (x < y) lazy1 + lazy2 else lazy2 + lazy1 }

Aquí lazy1 , lazy2 y lazy3 son todas variables perezosas. lazy3 nunca se lazy3 una instancia (por lo tanto, este código nunca entra en un bucle infinito) y el orden de lazy1 de instancias de lazy1 y lazy2 depende de los argumentos de la función. Por ejemplo, cuando llama a foo(1,2) obtendrá lazy1 instancia de lazy2 antes que lazy2 y cuando llame a foo(2,1) obtendrá el reverso. Pruebe el código en el intérprete de scala y vea la copia impresa. (No lo pondré aquí porque esta respuesta ya es bastante larga).

Podría obtener resultados similares si en lugar de variables perezosas usara funciones sin argumentos. En el ejemplo anterior, podría reemplazar cada valor lazy val con una def y lograr resultados similares. La diferencia es que las variables perezosas se almacenan en caché (alias solo se evalúan una vez), pero se evalúa una def cada vez que se invoca.

EDITAR 3: Se agregó una discusión sobre el alcance, vea la pregunta.

Alcance de las definiciones locales.

Las definiciones locales tienen el alcance del bloque en el que están declaradas, como se esperaba (bueno, la mayoría de las veces, en situaciones raras pueden escapar del bloque, como cuando se usa la vinculación de la variable de la mitad del flujo para los bucles). Por lo tanto, se pueden usar var , val y def locales para limitar el alcance de una expresión. Tomemos el siguiente ejemplo:

object Obj { def bar = "outer scope" def innerFun() { def bar = "inner scope" println(bar) // prints inner scope } def outerFun() { println(bar) // prints outer scope } def smthDifferent() { println(bar) // prints inner scope ! :) def bar = "inner scope" println(bar) // prints inner scope } def doesNotCompile() { { def fun = "fun" // local to this block 42 // blocks must not end with a definition... } println(fun) } }

Tanto innerFun() como outerFun() comportan como se espera. La definición de bar en innerFun() oculta la bar definida en el ámbito de envolvente. Además, la función fun es local a su bloque adjunto, por lo que no puede usarse de otra manera. El método doesNotCompile() ... no compila. Es interesante observar que ambas llamadas smthDifferent() método smthDifferent() imprimen inner scope . Por lo tanto, sí, puedes poner definiciones después de que se usen dentro de los métodos. Sin embargo, no lo recomendaría, ya que creo que es una mala práctica (al menos en mi opinión). En los archivos de clase, puede organizar las definiciones de métodos como desee, pero mantendría todas las def dentro de una función antes de que se usen. Y val s y var s ... bueno ... me resulta incómodo colocarlos después de que se usan.

También tenga en cuenta que cada bloque debe terminar con una expresión no con una definición, por lo tanto, no puede tener todas las definiciones al final de un bloque. Probablemente pondría todas las definiciones al comienzo de un bloque, y luego escribiría toda mi lógica produciendo un resultado al final de ese bloque. Parece más natural de esa manera, en lugar de:

{ // some logic // some defs // some other logic, returning the result }

Como dije anteriormente, no puedes terminar un bloque con solo // some defs . Aquí es donde Scala se diferencia ligeramente de Haskell :).

EDIT 4 : elaborado en la definición de cosas después de usarlos, sugerido por el comentario de Kim .

Definiendo ''cosas'' después de usarlas

Esto es algo difícil de implementar en un lenguaje que tiene efectos secundarios. En un mundo puro sin efectos secundarios, el orden no sería importante (los métodos no dependerían de ningún efecto secundario). Pero, como Scala permite efectos secundarios, el lugar donde se define una función es importante. Además, cuando define un val o var , el lado derecho debe evaluarse en su lugar para instanciar ese val . Considere el siguiente ejemplo:

// does not compile :) def foo(x: Int) = { // println *has* to execute now, but // cannot call f(10) as the closure // that you call has not been created yet! // it''s similar to calling a variable that is null println(f(10)) var aVar = 1 // the closure has to be created here, // as it cannot capture aVar otherwise def f(i: Int) = i + aVar aVar = aVar + 1 f(10) }

Sin embargo, el ejemplo que das funciona si los val son lazy o si son val def .

def foo(): Int = { println(1) lazy val a = { println("a"); b } println(2) lazy val b = { println("b"); 1 } println(3) a + a }

Este ejemplo también muestra muy bien el almacenamiento en caché en el trabajo (intente cambiar el valor lazy val a def y vea qué sucede :)

Todavía pienso en un mundo con efectos secundarios que es mejor seguir teniendo definiciones antes de usarlas. Es más fácil leer el código fuente de esa manera.

-- Flaviu Cipcigan


Haskell une valores a nombres con expresiones let y where . Estoy bastante seguro de que cualquier expresión where se puedan estandarizar en expresiones de margen (independientemente del orden de evaluación) antes de la evaluación o la generación de código.

Scala codifica enlaces con sentencias val dentro de un alcance. El compilador asegura que el valor asignado a ese nombre no cambia. Estos parecen ser let-like porque se ejecutan en primer lugar para durar. Esto es contrario a lo que queremos que lea nuestro código: la idea principal se muestra primero y los detalles de apoyo expresados ​​después. Esta es la causa de nuestra carga estética.

En el espíritu de estandarizar where -> let , una forma podríamos codificar un donde en Scala podría ser con macros (no lo probé, solo EXPN1 where { EXPN2 } hipótesis) EXPN1 where { EXPN2 } tal que EXPN1 es una expresión válida, y EXPN2 podría ser válido dentro de una declaración de objeto expandiéndose a:

object $genObjectname { EXPN2 } { import $genObjectName._; EXPN1 }

Ejemplo de uso:

sausageStuffer compose meatGrinder where { val sausageStuffer = ... // you really don''t want to know val meatGrinder = ... // not that pretty }

Siento tu dolor. Te responderé si alguna vez hago una macro que funcione.


Puede usar var y val para proporcionar variables locales, pero eso es diferente de la cláusula where de Haskell en dos aspectos bastante importantes: la pereza y la pureza.

La cláusula where de Haskell es útil porque la pereza y la pureza permiten al compilador instanciar únicamente las variables en la cláusula where que realmente se usan.

Esto significa que puede escribir una definición local larga y grande, y colocar una cláusula where debajo de ella y no es necesario tener en cuenta el orden de los efectos (debido a la pureza) y tampoco es necesario hacerlo si cada rama de código individual necesita todas las definiciones en la cláusula where, porque la pereza permite que los términos no utilizados en la cláusula where existan igual que thunks, cuya pureza permite al compilador elegir eludir el código resultante cuando no se usan.

Desafortunadamente, Scala no tiene ninguna de estas propiedades, por lo que no puede proporcionar un equivalente completo a la cláusula where de Haskell.

Debe factorizar manualmente las var s y val s que usa y colocarlas antes de las declaraciones que las usan, de manera muy similar let declaraciones de ML let .


Similar, si. No voy a entrar en detalles, como Flaviu ya lo ha hecho, pero daré un ejemplo de la Wikipedia.

Haskell:

calc :: String -> [Float] calc = foldl f [] . words where f (x:y:zs) "+" = (y + x):zs f (x:y:zs) "-" = (y - x):zs f (x:y:zs) "*" = (y * x):zs f (x:y:zs) "/" = (y / x):zs f xs y = read y : xs

Esas definiciones son solo definiciones locales a calc . Entonces, en Scala, haríamos esto:

def calc(s: String): List[Float] = { def f(s: List[Float], op: String) = (s, op) match { case (x :: y :: zs, "+") => (y + x) :: zs case (x :: y :: zs, "-") => (y - x) :: zs case (x :: y :: zs, "*") => (y * x) :: zs case (x :: y :: zs, "/") => (y / x) :: zs case (xs, y) => read(y) :: xs } s.words.foldLeft(List[Float]())(f) }

Dado que Scala no tiene equivalente de read , puede definirlo como se muestra a continuación, con el fin de ejecutar este ejemplo en particular:

def read(s: String) = s.toFloat

Scala tampoco tiene words , para mi disgusto, aunque es fácil de definir:

implicit toWords(s: String) = new AnyRef { def words = s.split("//s") }

Ahora, la definición de Haskell es más compacta por varias razones:

  • Tiene una inferencia de tipos más poderosa, por lo que no se necesita declarar nada más allá del tipo de calc . Scala no puede hacer eso debido a una decisión de diseño consciente de estar orientado a objetos con el modelo de clase.

  • Tiene una definición de coincidencia de patrón implícita, mientras que, en Scala, debe declarar la función y luego declarar la coincidencia del patrón.

  • Su manejo del curry es simplemente superior al de Scala, en lo que respecta a la concisión. Esto es el resultado de varias decisiones, sobre el modelo de clase y la notación del operador, en las que el manejo del curry no se consideró tan importante.

  • Haskell tiene un tratamiento especial para las listas, lo que hace posible tener una sintaxis más concisa para ellas. En Scala, las listas se tratan como cualquier otra clase, el esfuerzo se realiza, en cambio, para garantizar que cualquier clase pueda ser tan compacta como la Lista en Scala.

Por lo tanto, hay varias razones por las que Scala hace lo que hace, aunque me encantaría una definición de coincidencia de patrones implícita. :-)