¿Por qué Haskell?

Al ser un lenguaje puramente funcional, Haskell te limita de muchos de los métodos convencionales de programación en un lenguaje orientado a objetos. Pero la limitación de las opciones de programación realmente nos ofrece beneficios sobre otros idiomas.?

En este tutorial, echaremos un vistazo a Haskell e intentaremos aclarar qué es y por qué vale la pena utilizarlo en sus proyectos futuros..


Haskell de un vistazo

Haskell es un lenguaje muy diferente..

Haskell es un lenguaje muy diferente al que puede estar acostumbrado, en la forma en que organiza su código en funciones "Puras". Una función pura es aquella que no realiza tareas externas más que devolver un valor calculado. Estas tareas externas generalmente se conocen como "Efectos secundarios.

Esto incluye obtener datos externos del usuario, imprimir en la consola, leer un archivo, etc. En Haskell, no pone ninguna de estas acciones en sus funciones puras..

Ahora puede que se pregunte, "¿de qué sirve un programa si no puede interactuar con el mundo exterior?" Bueno, Haskell resuelve esto con un tipo especial de función, llamada función IO. Esencialmente, se separan todas las partes de procesamiento de datos de su código en funciones puras, y luego se colocan las partes que cargan datos dentro y fuera de las funciones de IO. La función "principal" a la que se llama cuando su programa se ejecuta por primera vez es una función IO.

Revisemos una comparación rápida entre un programa Java estándar y su equivalente de Haskell.

Versión de Java:

 import java.io. *; class Test public static void main (String [] args) System.out.println ("Cuál es tu nombre:"); BufferedReader br = new BufferedReader (new InputStreamReader (System.in)); Nombre de cadena = nulo; prueba name = br.readLine ();  catch (IOException e) System.out.println ("Hubo un error");  System.out.println ("Hola" + nombre); 

Versión de Haskell:

 welcomeMessage name = "Hello" ++ name main = do putStrLn "Cuál es tu nombre:" nombre <- getLine putStrLn $ welcomeMessage name

Lo primero que puede notar cuando mira un programa de Haskell es que no hay corchetes. En Haskell, solo aplicas corchetes cuando intentas agrupar cosas. La primera línea en la parte superior del programa, que comienza con mensaje de bienvenida - es en realidad una función; acepta una cadena y devuelve el mensaje de bienvenida. La única otra cosa que puede parecer un tanto extraña aquí es el signo de dólar en la última línea.

putStrLn $ welcomeMessage nombre

Este signo de dólar simplemente le dice a Haskell que primero realice lo que está en el lado derecho del signo de dólar y luego se mueva hacia la izquierda. Esto es necesario porque, en Haskell, podría pasar una función como parámetro a otra función; así que Haskell no sabe si estás intentando pasar el mensaje de bienvenida función para putStrLn, o procesalo primero.

Además del hecho de que el programa Haskell es considerablemente más corto que la implementación de Java, la diferencia principal es que hemos separado el procesamiento de datos en una puro función, mientras que, en la versión de Java, solo la imprimimos. Este es su trabajo en Haskell en pocas palabras: separar su código en sus componentes. ¿Porque preguntas? Bien. hay un par de razones; revisemos algunos de ellos.

1. Código más seguro

No hay manera de que este código se rompa.

Si alguna vez te fallaron programas en el pasado, sabrás que el problema siempre está relacionado con una de estas operaciones inseguras, como un error al leer un archivo, un usuario ingresó el tipo de datos incorrecto, etc. Al limitar sus funciones a solo procesar datos, se le garantiza que no se bloquearán. La comparación más natural con la que la mayoría de la gente está familiarizada es una función matemática.

En matemáticas, una función calcula un resultado; eso es todo. Por ejemplo, si tuviera que escribir una función matemática, como f (x) = 2x + 4, entonces, si paso x = 2, Obtendré 8. Si en cambio paso en x = 3, Obtendré 10 como resultado. No hay forma de que este código se rompa. Además, dado que todo se divide en pequeñas funciones, las pruebas unitarias se vuelven triviales; puede probar cada parte individual de su programa y continuar sabiendo que es 100% seguro.

2. Mayor modularidad del código

Otro beneficio de separar su código en múltiples funciones es la reutilización del código. Imagina si todas las funciones estándar, como min y max, También se imprimió el valor a la pantalla. Entonces, estas funciones solo serían relevantes en circunstancias muy particulares y, en la mayoría de los casos, tendría que escribir sus propias funciones que solo devuelven un valor sin imprimirlo. Lo mismo se aplica a su código personalizado. Si tiene un programa que convierte una medida de cm a pulgadas, puede poner el proceso de conversión real en una función pura y luego reutilizarlo en todas partes. Sin embargo, si lo incluyes en tu programa, deberás volver a escribirlo cada vez. Ahora bien, esto parece bastante obvio en teoría, pero, si recuerdas la comparación de Java de arriba, hay algunas cosas a las que estamos acostumbrados a solo codificar en duro.

Además, Haskell ofrece dos formas de combinar funciones: el operador de puntos y las funciones de orden superior.

El operador de punto le permite encadenar funciones para que la salida de una función entre en la entrada de la siguiente.

Aquí hay un ejemplo rápido para demostrar esta idea:

 cmToInches cm = cm * 0.3937 formatInchesStr i = show i ++ "inches" main = do putStrLn "Introduzca la longitud en cm:" inp <- getLine let c = (read inp :: Float) (putStrLn . formatInchesStr . cmToInches) c

Esto es similar al último ejemplo de Haskell, pero aquí, he combinado la salida de centímetros a pulgadas a la entrada de formatInchesStr, y han atado esa salida a putStrLn. Las funciones de orden superior son funciones que aceptan otras funciones como una entrada, o funciones que dan salida a una función como su salida. Un ejemplo útil de esto es el incorporado de Haskell. mapa función. mapa toma una función que estaba destinada a un solo valor y realiza esta función en una matriz de objetos. Las funciones de orden superior le permiten abstraer secciones de código que tienen varias funciones en común, y luego simplemente proporcionar una función como parámetro para cambiar el efecto general.

3. Mejor optimización

En Haskell, no hay soporte para cambiar el estado o datos mutables.

En Haskell, no hay soporte para cambiar el estado o los datos mutables, por lo que si intenta cambiar una variable después de que se haya establecido, recibirá un error en el momento de la compilación. Es posible que esto no suene atractivo al principio, pero hace que su programa sea "referencialmente transparente". Lo que esto significa es que sus funciones siempre devolverán los mismos valores, siempre que las mismas entradas. Esto le permite a Haskell simplificar su función o reemplazarla por completo con un valor almacenado en caché, y su programa continuará ejecutándose normalmente, como se esperaba. De nuevo, una buena analogía a esto son las funciones matemáticas, ya que todas las funciones matemáticas son referencialmente transparentes. Si tuvieras una función, como pecado (90), Podrías reemplazar eso con el número. 1, porque tienen el mismo valor, lo que le ahorra tiempo al calcular esto cada vez. Otro beneficio que obtiene con este tipo de código es que, si tiene funciones que no dependen unas de otras, puede ejecutarlas en paralelo, lo que aumenta el rendimiento general de su aplicación..

4. Mayor productividad en el flujo de trabajo

Personalmente, he descubierto que esto conduce a un flujo de trabajo considerablemente más eficiente..

Al hacer que sus funciones sean componentes individuales que no dependen de nada más, usted puede planificar y ejecutar su proyecto de una manera mucho más enfocada. Convencionalmente, haría una lista de tareas muy genérica que abarque muchas cosas, como "Build Object Parser" o algo así, que realmente no le permita saber qué implica o cuánto tiempo llevará. Tienes una idea básica, pero, muchas veces, las cosas tienden a "surgir".

En Haskell, la mayoría de las funciones son bastante cortas (un par de líneas, máximo) y están bastante enfocadas. La mayoría de ellos solo ejecutan una única tarea específica. Pero entonces, tiene otras funciones, que son una combinación de estas funciones de nivel inferior. Por lo tanto, su lista de tareas pendientes se compone de funciones muy específicas, en las que sabe exactamente lo que hace cada una de antemano. Personalmente, he descubierto que esto conduce a un flujo de trabajo considerablemente más eficiente..

Ahora este flujo de trabajo no es exclusivo de Haskell; Usted puede hacer esto fácilmente en cualquier idioma. La única diferencia es que esta es la forma preferida en Haskell, como se aplica a otros idiomas, donde es más probable que combine varias tareas juntas.

Por eso le recomendé que aprendiera Haskell, incluso si no planea usarlo todos los días. Te obliga a entrar en este hábito..

Ahora que le he dado una visión general rápida de algunos de los beneficios de usar Haskell, veamos un ejemplo del mundo real. Debido a que este es un sitio relacionado con la red, pensé que una demostración relevante sería hacer un programa Haskell que pueda respaldar sus bases de datos MySQL.

Empecemos con un poco de planificación..


Construyendo un programa de Haskell

Planificación

Anteriormente mencioné que, en Haskell, realmente no planifica su programa en un estilo de estilo de vista general. En su lugar, organiza las funciones individuales, mientras que, al mismo tiempo, recuerda separar el código en puro y funciones IO. Lo primero que tiene que hacer este programa es conectarse a una base de datos y obtener la lista de tablas. Ambas son funciones de IO, porque obtienen datos de una base de datos externa.

A continuación, tenemos que escribir una función que recorre la lista de tablas y devuelva todas las entradas; esta también es una función IO. Una vez que está terminado, tenemos algunos puro funciones para preparar los datos para su escritura y, por último, tenemos que escribir todas las entradas en los archivos de copia de seguridad junto con la fecha y una consulta para eliminar las entradas antiguas. Aquí hay un modelo de nuestro programa:

Este es el flujo principal del programa, pero, como dije, también habrá algunas funciones de ayuda para hacer cosas como obtener la fecha y demás. Ahora que tenemos todo planeado, podemos comenzar a construir el programa.

edificio

Usaré la biblioteca MySQL HDBC en este programa, que puede instalar ejecutando Cabal instalar HDBC y Cabal instalar HDBC-mysql Si tienes la plataforma Haskell instalada. Comencemos con las dos primeras funciones de la lista, ya que ambas están integradas en la biblioteca HDBC:

 importar Control.Monad importar Database.HDBC importar Database.HDBC.MySQL importar System.IO importar System.Directory importar Data.Time importar Data.Time.Calendar main = do conn <- connectMySQL defaultMySQLConnectInfo  mysqlHost = "127.0.0.1", mysqlUser = "root", mysqlPassword = "pass", mysqlDatabase = "test"  tables <- getTables conn

Esta parte es bastante sencilla; creamos la conexión y luego colocamos la lista de tablas en una variable, llamada mesas. La siguiente función recorrerá la lista de tablas y obtendrá todas las filas en cada una, una forma rápida de hacer esto es hacer una función que maneje solo un valor y luego usar la mapa Función para aplicarlo a la matriz. Ya que estamos mapeando una función IO, tenemos que usar mapaM. Con esto implementado, su código ahora debería verse como lo siguiente:

 getQueryString name = "select * from" ++ name processTable :: IConnection conn => conn -> String -> IO [[SqlValue]] processTable conn name = do let qu = getQueryString filas de nombre <- quickQuery' conn qu [] return rows main = do conn <- connectMySQL defaultMySQLConnectInfo  mysqlHost = "127.0.0.1", mysqlUser = "root", mysqlPassword = "pass", mysqlDatabase = "test"  tables <- getTables conn rows <- mapM (processTable conn) tables

getQueryString Es una función pura que devuelve una seleccionar consulta, y luego tenemos el real tabla de procesos función, que utiliza esta cadena de consulta para recuperar todas las filas de la tabla especificada. Haskell es un lenguaje muy tipificado, lo que básicamente significa que no puede, por ejemplo, poner un En t donde un cuerda se supone que tiene que ir Pero Haskell también es "inferencias de tipo", lo que significa que normalmente no necesita escribir los tipos y Haskell lo resolverá. Aquí tenemos una costumbre. conn tipo, que tenía que declarar explícitamente; así que eso es lo que la línea por encima de la tabla de procesos la función está haciendo.

Lo siguiente en la lista es convertir los valores SQL que fueron devueltos por la función anterior en cadenas. Otra forma de manejar listas, además de mapa Es crear una función recursiva. En nuestro programa, tenemos tres capas de listas: una lista de valores de SQL, que están en una lista de filas, que están en una lista de tablas. usaré mapa para las dos primeras listas, y luego una función recursiva para manejar la última. Esto permitirá que la función en sí sea bastante corta. Aquí está la función resultante:

 unSql x = (fromSql x) :: String sqlToArray [n] = (unSql n): [] sqlToArray (n: n2) = (unSql n): sqlToArray n2

Luego, agregue la siguiente línea a la función principal:

 let stringRows = mapea filas (map sqlToArrays)

Es posible que haya notado que, algunas veces, las variables se declaran como var, y en otras ocasiones, como deja var = funcion. La regla es esencialmente que cuando intenta ejecutar una función de E / S y colocar los resultados en una variable, utiliza la método; para almacenar los resultados de una función pura dentro de una variable, en su lugar, debería usar dejar.

La siguiente parte será un poco complicada. Tenemos todas las filas en formato de cadena y, ahora, tenemos que reemplazar cada fila de valores con una cadena de inserción que MySQL entenderá. El problema es que los nombres de las tablas están en una matriz separada; entonces un doble mapa La función realmente no funcionará en este caso. Podríamos haber utilizado mapa una vez, pero luego tendríamos que combinar las listas en una sola - posiblemente usando tuplas porque mapa solo acepta un parámetro de entrada, así que decidí que sería más simple escribir nuevas funciones recursivas. Como tenemos una matriz de tres capas, necesitaremos tres funciones recursivas separadas, de modo que cada nivel pueda pasar su contenido a la siguiente capa. Aquí están las tres funciones junto con una función auxiliar para generar la consulta SQL real:

 flattenArgs [arg] = "\" "++ arg ++" \ "" flattenArgs (arg1: args) = "\" "++ arg1 ++" \ "," ++ (flattenArgs args) iQuery name args = " inserte en los valores de "++ name ++" ("++ (flattenArgs args) ++"); \ n "insertStrRows name [arg] = iQuery name arg insertStrRows name (arg1: args) = (iQuery name arg1) ++ (insertStrRows name args) insertStrTables [table] [rows] = insertStrRows table rows: [] insertStrTables (table1: other) (rows1: etc) = (insertStrRows table1 rows1): (insertStrTables other etc)

De nuevo, agregue lo siguiente a la función principal:

 vamos insertStrs = insertStrTables tablas stringRows

los flattenArgs y iQuery Las funciones trabajan juntas para crear la consulta de inserción de SQL real. Después de eso, solo tenemos las dos funciones recursivas. Observe que, en dos de las tres funciones recursivas, ingresamos una matriz, pero la función devuelve una cadena. Al hacer esto, estamos eliminando dos de las matrices anidadas. Ahora, solo tenemos una matriz con una cadena de salida por tabla. El último paso es escribir los datos en sus archivos correspondientes; esto es significativamente más fácil, ahora que simplemente estamos tratando con una matriz simple. Aquí está la última parte junto con la función para obtener la fecha:

 dateStr = do t <- getCurrentTime return (showGregorian . utctDay $ t) filename name time = "Backups/" ++ name ++ "_" ++ time ++ ".bac" writeToFile name queries = do let output = (deleteStr name) ++ queries time <- dateStr createDirectoryIfMissing False "Backups" f <- openFile (filename name time) WriteMode hPutStr f output hClose f writeFiles [n] [q] = writeToFile n q writeFiles (n:n2) (q:q2) = do writeFiles [n] [q] writeFiles n2 q2

los fechaStr La función convierte la fecha actual en una cadena con el formato, YYYY-MM-DD. Luego, está la función de nombre de archivo, que pone todas las piezas del nombre de archivo juntas. los writeToFile La función se encarga de la salida a los archivos. Por último, la escribir archivos La función itera a través de la lista de tablas, por lo que puede tener un archivo por tabla. Todo lo que queda por hacer es terminar la función principal con la llamada a escribir archivos, y agrega un mensaje informando al usuario cuando haya terminado. Una vez completado, su principal La función debería verse así:

 main = do conn <- connectMySQL defaultMySQLConnectInfo  mysqlHost = "127.0.0.1", mysqlUser = "root", mysqlPassword = "pass", mysqlDatabase = "test"  tables <- getTables conn rows <- mapM (processTable conn) tables let stringRows = map (map sqlToArray) rows let insertStrs = insertStrTables tables stringRows writeFiles tables insertStrs putStrLn "Databases Sucessfully Backed Up"

Ahora, si alguna de sus bases de datos pierde su información, puede pegar las consultas SQL directamente desde su archivo de copia de seguridad en cualquier terminal o programa MySQL que pueda ejecutar consultas; restaurará los datos a ese punto en el tiempo. También puede agregar una tarea cron para ejecutar esta hora o cada día, a fin de mantener sus copias de seguridad actualizadas.


Terminando

Hay un excelente libro de Miran Lipovača, llamado "Learn you a Haskell".

¡Eso es todo lo que tengo para este tutorial! En el futuro, si está interesado en aprender Haskell por completo, hay algunos buenos recursos para revisar. Hay un excelente libro, de Miran Lipovača, llamado "Learn you a Haskell", que incluso tiene una versión en línea gratuita. Ese sería un excelente comienzo.

Si busca funciones específicas, debe consultar Hoogle, que es un motor de búsqueda similar a Google que le permite buscar por nombre, o incluso por tipo. Por lo tanto, si necesita una función que convierte una cadena en una lista de cadenas, debe escribir Cadena -> [Cadena], y le proporcionará todas las funciones aplicables. También hay un sitio, llamado hackage.haskell.org, que contiene la lista de módulos para Haskell; Puedes instalarlos todos a través de Cabal..

Espero que hayas disfrutado este tutorial. Si tiene alguna pregunta, siéntase libre de publicar un comentario a continuación; Haré todo lo posible para responderle lo antes posible.!