¿Por qué a Haskell(a veces) se lo conoce como "el mejor lenguaje imperativo"?
imperative-programming (3)
Además de lo que otros ya han mencionado, tener acciones colaterales ser de primera clase a veces es útil. Aquí hay un ejemplo tonto para mostrar la idea:
f = sequence_ (reverse [print 1, print 2, print 3])
Este ejemplo muestra cómo puede compilar cálculos con efectos secundarios (en este ejemplo, print
) y luego colocar las estructuras de datos o manipularlas de otras maneras, antes de ejecutarlas.
(Espero que esta pregunta sea sobre el tema: intenté buscar una respuesta pero no encontré una respuesta definitiva. Si esto sucede fuera del tema o si ya se respondió, modifíquelo / quítelo).
Recuerdo haber escuchado / leído el comentario medio en broma sobre que Haskell es el mejor lenguaje imperativo varias veces, lo que por supuesto suena raro ya que Haskell es mejor conocido por sus características funcionales .
Así que mi pregunta es, ¿qué cualidades / características (si hay alguna) de Haskell dan razones para justificar que Haskell sea considerado el mejor lenguaje imperativo , o en realidad es más una broma?
Lo considero una verdad a medias. Haskell tiene una asombrosa capacidad de abstracción, y eso incluye abstracción sobre ideas imperativas. Por ejemplo, Haskell no tiene un bucle while imperativo incorporado, pero podemos simplemente escribirlo y ahora lo hace:
while :: (Monad m) => m Bool -> m () -> m ()
while cond action = do
c <- cond
if c
then action >> while cond action
else return ()
Este nivel de abstracción es difícil para muchos lenguajes imperativos. Esto se puede hacer en idiomas imperativos que tienen cierres; p.ej. Python y C #.
Pero Haskell también tiene la capacidad (muy singular) de caracterizar los efectos secundarios permitidos , utilizando las clases de Monad. Por ejemplo, si tenemos una función:
foo :: (MonadWriter [String] m) => m Int
Esta puede ser una función "imperativa", pero sabemos que solo puede hacer dos cosas:
- "Salida" una secuencia de cadenas
- devolver un int
No puede imprimir en la consola o establecer conexiones de red, etc. Combinado con la capacidad de abstracción, puede escribir funciones que actúen sobre "cualquier cálculo que produzca una secuencia", etc.
En realidad, se trata de las habilidades abstractas de Haskell lo que lo convierte en un lenguaje imperativo muy fino.
Sin embargo, la mitad falsa es la sintaxis. Encuentro a Haskell bastante prolijo e incómodo de usar en un estilo imperativo. Aquí hay un ejemplo de cálculo de estilo imperativo usando el ciclo while
anterior, que encuentra el último elemento de una lista enlazada:
lastElt :: [a] -> IO a
lastElt [] = fail "Empty list!!"
lastElt xs = do
lst <- newIORef xs
ret <- newIORef (head xs)
while (not . null <$> readIORef lst) $ do
(x:xs) <- readIORef lst
writeIORef lst xs
writeIORef ret x
readIORef ret
Toda esa basura IORef, la doble lectura, tener que vincular el resultado de una lectura, fmapping ( <$>
) para operar en el resultado de un cálculo en línea ... todo es muy complicado. Tiene mucho sentido desde una perspectiva funcional , pero los lenguajes imperativos tienden a barrer la mayoría de estos detalles debajo de la alfombra para que sean más fáciles de usar.
Es cierto que quizás si utilizáramos un combinador de estilo while
diferente sería más limpio. Pero si llevas esa filosofía lo suficientemente lejos (usando un rico conjunto de combinadores para expresarte con claridad), entonces vuelves a la programación funcional. Imperativo estilo Haskell simplemente no "fluye" como un lenguaje imperativo bien diseñado, por ejemplo, python.
En conclusión, con un estiramiento facial sintáctico, Haskell podría ser el mejor lenguaje imperativo. Pero, por la naturaleza de los estiramientos faciales, sería reemplazar algo internamente hermoso y real con algo externamente bello y falso.
EDITAR : Contraste lastElt
con esta transliteración python:
def last_elt(xs):
assert xs, "Empty list!!"
lst = xs
ret = xs.head
while lst:
ret = lst.head
lst = lst.tail
return ret
El mismo número de líneas, pero cada línea tiene bastante menos ruido.
EDIT 2
Por lo que vale, así es como se ve un reemplazo puro en Haskell:
lastElt = return . last
Eso es. O bien, si me prohíbe usar Prelude.last
:
lastElt [] = fail "Unsafe lastElt called on empty list"
lastElt [x] = return x
lastElt (_:xs) = lastElt xs
O bien, si desea que funcione en cualquier estructura de datos Foldable
y reconozca que en realidad no necesita IO
para manejar los errores:
import Data.Foldable (Foldable, foldMap)
import Data.Monoid (Monoid(..), Last(..))
lastElt :: (Foldable t) => t a -> Maybe a
lastElt = getLast . foldMap (Last . Just)
con Map
, por ejemplo:
λ➔ let example = fromList [(10, "spam"), (50, "eggs"), (20, "ham")] :: Map Int String
λ➔ lastElt example
Just "eggs"
El operador (.)
Es la composición de la función .
No es una broma, y lo creo. Trataré de mantener esto accesible para aquellos que no conocen ningún Haskell. Haskell usa la notación do (entre otras cosas) para permitirte escribir un código imperativo (sí, usa mónadas, pero no te preocupes por eso). Estas son algunas de las ventajas que Haskell le ofrece:
Fácil creación de subrutinas. Digamos que quiero que una función imprima un valor para stdout y stderr. Puedo escribir lo siguiente, definir la subrutina con una línea corta:
do let printBoth s = putStrLn s >> hPutStrLn stderr s printBoth "Hello" -- Some other code printBoth "Goodbye"
Código fácil de pasar alrededor. Dado que he escrito lo anterior, si ahora quiero usar la función
printBoth
para imprimir toda una lista de cadenas, eso se hace fácilmente pasando mi subrutina a la funciónmapM_
:mapM_ printBoth ["Hello", "World!"]
Otro ejemplo, aunque no es imprescindible, es la clasificación. Supongamos que desea ordenar cadenas solo por longitud. Puedes escribir:
sortBy (/a b -> compare (length a) (length b)) ["aaaa", "b", "cc"]
Lo cual te dará ["b", "cc", "aaaa"]. (Puedes escribirlo más corto que eso, también, pero no importa por ahora).
Código fácil de reutilizar. Esa función
mapM_
se usa mucho y reemplaza para cada bucle en otros idiomas. También existe porforever
que actúa como un tiempo (verdadero) y varias otras funciones que se pueden pasar código y ejecutarlo de diferentes maneras. Por lo tanto, los bucles en otros idiomas se reemplazan por estas funciones de control en Haskell (que no son especiales; puede definirlas usted mismo muy fácilmente). En general, esto dificulta que la condición de bucle sea incorrecta, al igual que para cada bucle es más difícil de obtener que los equivalentes de iterador de larga duración (por ejemplo, en Java) o bucles de indexación de matriz (por ejemplo, en C).- Encuadernación no asignación. Básicamente, solo puede asignar una variable una vez (como una única asignación estática). Esto elimina mucha confusión sobre los posibles valores de una variable en cualquier punto dado (su valor solo se establece en una línea).
Efectos secundarios contenidos Digamos que quiero leer una línea de stdin y escribirla en stdout luego de aplicarle alguna función (lo llamaremos foo). Puedes escribir:
do line <- getLine putStrLn (foo line)
Sé de inmediato que
foo
no tiene ningún efecto secundario inesperado (como actualizar una variable global, o desasignar memoria, o lo que sea), porque su tipo debe ser Cadena -> Cadena, lo que significa que es una función pura; cualquier valor que pase, debe devolver el mismo resultado cada vez, sin efectos secundarios. Haskell separa muy bien el código de efectos laterales del código puro. En algo como C, o incluso Java, esto no es obvio (¿ese método getFoo () cambia el estado? Esperaría que no, pero podría hacer ...).- Recolección de basura. Hoy en día, muchos idiomas son basura, pero vale la pena mencionar: no hay problemas para asignar y desasignar la memoria.
Probablemente haya algunas otras ventajas además, pero esas son las que vienen a la mente.