Pourquoi Haskell?

En tant que langage purement fonctionnel, Haskell vous limite à la plupart des méthodes classiques de programmation dans un langage orienté objet. Mais limiter les options de programmation nous offre-t-il vraiment des avantages par rapport aux autres langues?

Dans ce tutoriel, nous allons jeter un regard sur Haskell et tenter de clarifier ce que c'est, et pourquoi cela pourrait valoir la peine d'être utilisé dans vos futurs projets..


Haskell en bref

Haskell est un type de langage très différent.

Haskell est un type de langage très différent de celui auquel vous êtes habitué, en ce sens que vous organisez votre code en fonctions "pures". Une fonction pure est une fonction qui n'effectue aucune tâche extérieure autre que le renvoi d'une valeur calculée. Ces tâches extérieures sont généralement appelées «effets secondaires»..

Cela inclut l'extraction de données extérieures de l'utilisateur, l'impression sur la console, la lecture d'un fichier, etc. En Haskell, vous ne mettez aucune de ces actions dans vos fonctions pures..

Maintenant, vous vous demandez peut-être: "à quoi sert un programme s'il ne peut pas interagir avec le monde extérieur?" Haskell résout ce problème avec un type de fonction spécial, appelé fonction IO. Essentiellement, vous séparez toutes les parties de votre code qui traitent les données en fonctions pures, puis vous insérez les parties qui chargent les données dans des fonctions IO. La fonction "principale" appelée lors de la première exécution de votre programme est une fonction IO.

Passons en revue une comparaison rapide entre un programme Java standard et son équivalent Haskell.

Version Java:

 importer java.io. *; class Test public static void main (String [] args) System.out.println ("Quel est votre nom:"); BufferedReader br = new BufferedReader (new InputStreamReader (System.in)); Nom de la chaîne = null; try name = br.readLine ();  catch (IOException e) System.out.println ("Une erreur s'est produite");  System.out.println ("Hello" + nom); 

Version Haskell:

 welcomeMessage name = "Hello" ++ name main = do putStrLn "Quel est votre nom:" name <- getLine putStrLn $ welcomeMessage name

La première chose que vous remarquerez peut-être en regardant un programme Haskell, c'est qu'il n'y a pas de crochets. En Haskell, vous appliquez uniquement des crochets lorsque vous essayez de regrouper des éléments. La première ligne en haut du programme - qui commence par message de bienvenue - est en fait une fonction; il accepte une chaîne et renvoie le message de bienvenue. La seule autre chose qui peut paraître quelque peu étrange ici est le signe dollar sur la dernière ligne.

putStrLn $ welcomeMessage name

Ce signe dollar indique simplement à Haskell de commencer par exécuter ce qui se trouve du côté droit du signe dollar, puis de passer à gauche. Cela est nécessaire car, dans Haskell, vous pouvez transmettre une fonction en tant que paramètre à une autre fonction. donc Haskell ne sait pas si vous essayez de passer le message de bienvenue fonction à putStrLn, ou le traiter d'abord.

Outre le fait que le programme Haskell est considérablement plus court que l'implémentation Java, la principale différence est que nous avons séparé le traitement des données en un pur fonction, alors que dans la version Java, nous l’avions seulement imprimée. En un mot, votre travail à Haskell consiste à séparer votre code en ses composants. Pourquoi demandes-tu? Bien. il y a plusieurs raisons; passons en revue certains d'entre eux.

1. Code plus sûr

Il n'y a aucun moyen pour ce code de casser.

Si des programmes vous ont déjà bloqué dans le passé, vous savez alors que le problème est toujours lié à l'une de ces opérations dangereuses, telles qu'une erreur lors de la lecture d'un fichier, un utilisateur ayant entré un type de données incorrect, etc. En limitant vos fonctions au traitement de données, vous êtes assuré qu'elles ne tomberont pas en panne. La comparaison la plus naturelle que la plupart des gens connaissent est une fonction mathématique.

En maths, une fonction calcule un résultat; c'est tout. Par exemple, si je devais écrire une fonction mathématique, comme f (x) = 2x + 4, alors, si je passe dans x = 2, j'aurai 8. Si je passe à la place x = 3, j'aurai dix Par conséquent. Il n'y a aucun moyen pour ce code de casser. De plus, puisque tout est divisé en petites fonctions, les tests unitaires deviennent triviaux. vous pouvez tester chaque partie de votre programme et continuer en sachant qu'il est sûr à 100%.

2. Modularité accrue du code

La réutilisation du code est un autre avantage de la séparation de votre code en plusieurs fonctions. Imaginez si toutes les fonctions standard, comme min et max, également imprimé la valeur à l'écran. Ensuite, ces fonctions ne seraient pertinentes que dans des circonstances très particulières et, dans la plupart des cas, vous devrez écrire vos propres fonctions qui renverront uniquement une valeur sans l’imprimer. La même chose s'applique à votre code personnalisé. Si vous avez un programme qui convertit une mesure de cm en pouces, vous pouvez transformer le processus de conversion actuel en une fonction pure, puis le réutiliser partout. Cependant, si vous le codez en dur dans votre programme, vous devrez le ressaisir à chaque fois. Cela semble assez évident en théorie, mais si vous vous souvenez de la comparaison Java, il y a des choses que nous avons l'habitude de coder en dur.

De plus, Haskell offre deux façons de combiner des fonctions: l'opérateur de point et les fonctions d'ordre supérieur..

L’opérateur de point vous permet d’enchaîner les fonctions ensemble de sorte que la sortie d’une fonction passe dans l’entrée de la suivante..

Voici un exemple rapide pour illustrer cette idée:

 cmToInches cm = cm * 0.3937 formatInchesStr i = affichez i ++ "pouces" principal = faites putStrLn "Entrez la longueur en cm:" inp <- getLine let c = (read inp :: Float) (putStrLn . formatInchesStr . cmToInches) c

Ceci est similaire au dernier exemple Haskell, mais ici, j'ai combiné la sortie de cmToInches à l'entrée de formatInchesStr, et ont lié cette sortie à putStrLn. Les fonctions d'ordre supérieur sont des fonctions qui acceptent d'autres fonctions en tant qu'entrée ou des fonctions qui émettent une fonction en tant que sortie. Un exemple utile de ceci est la fonction intégrée de Haskell. carte une fonction. carte prend une fonction qui était destinée à une valeur unique et exécute cette fonction sur un tableau d'objets. Les fonctions d'ordre supérieur vous permettent d'extraire des sections de code que plusieurs fonctions ont en commun, puis de simplement fournir une fonction en tant que paramètre permettant de modifier l'effet global..

3. Meilleure optimisation

En Haskell, il n'y a pas de support pour changer d'état ou de données mutables.

En Haskell, la modification de l'état ou des données mutables n'est pas prise en charge. Par conséquent, si vous essayez de modifier une variable après son paramétrage, vous recevrez une erreur lors de la compilation. Cela peut ne pas paraître attrayant au début, mais cela rend votre programme "référentiellement transparent". Cela signifie que vos fonctions renverront toujours les mêmes valeurs, à condition que les mêmes entrées soient fournies. Cela permet à Haskell de simplifier votre fonction ou de la remplacer entièrement par une valeur mise en cache, et votre programme continuera de fonctionner normalement, comme prévu. Là encore, les fonctions mathématiques sont une bonne analogie: toutes les fonctions mathématiques sont référentiellement transparentes. Si vous aviez une fonction, comme péché (90), vous pouvez remplacer cela par le nombre 1, parce qu'ils ont la même valeur, vous gagnez du temps en calculant chaque fois. Un autre avantage de ce type de code est que, si vous avez des fonctions qui ne s'appuient pas les unes sur les autres, vous pouvez les exécuter en parallèle, ce qui améliore encore les performances globales de votre application..

4. Productivité accrue dans le flux de travail

Personnellement, j'ai constaté que cela conduisait à un flux de travail considérablement plus efficace.

En faisant de vos fonctions des composants individuels qui ne reposent sur rien d’autre, vous êtes en mesure de planifier et d’exécuter votre projet de manière beaucoup plus ciblée. Classiquement, vous feriez une liste de tâches très générique qui engloberait beaucoup de choses, telles que "Construire un analyseur d'objets", ou quelque chose du genre, qui ne vous permettraient pas de savoir ce que cela impliquait ou combien de temps cela prendrait. Vous avez une idée de base, mais, souvent, les choses ont tendance à "monter".

En Haskell, la plupart des fonctions sont assez courtes - quelques lignes, max - et sont très concentrées. La plupart d'entre eux n'exécutent qu'une seule tâche spécifique. Mais alors, vous avez d'autres fonctions, qui sont une combinaison de ces fonctions de niveau inférieur. Votre liste de tâches finit donc par comporter des fonctions très spécifiques, dans lesquelles vous savez exactement ce que chacun fait à l’avance. Personnellement, j'ai constaté que cela conduisait à un flux de travail considérablement plus efficace.

Maintenant, ce flux de travail n'est pas exclusif à Haskell; vous pouvez facilement le faire dans n’importe quelle langue. La seule différence est que c'est la méthode préférée en Haskell, comparée à d'autres langues, dans laquelle vous êtes plus susceptible de combiner plusieurs tâches ensemble..

C'est pourquoi j'ai recommandé d'apprendre Haskell, même si vous ne prévoyez pas de l'utiliser tous les jours. Cela vous oblige à prendre cette habitude.

Maintenant que je vous ai donné un aperçu des avantages d’utiliser Haskell, examinons un exemple concret. Comme il s’agit d’un site Web, j’ai pensé qu’une démonstration pertinente consisterait à créer un programme Haskell capable de sauvegarder vos bases de données MySQL..

Commençons par un peu de planification.


Construire un programme Haskell

Planification

J'ai mentionné précédemment que, dans Haskell, vous ne planifiez pas vraiment votre programme dans un style de type aperçu. Au lieu de cela, vous organisez les fonctions individuelles tout en vous rappelant de séparer le code en pur et les fonctions IO. La première chose que ce programme doit faire est de se connecter à une base de données et d’obtenir la liste des tables. Ce sont deux fonctions IO, car elles récupèrent les données d'une base de données externe.

Ensuite, nous devons écrire une fonction qui parcourra la liste des tables et renverra toutes les entrées - c’est aussi une fonction IO. Une fois que c'est fini, nous avons quelques pur fonctions pour que les données soient prêtes pour l'écriture et, dernier point, nous devons écrire toutes les entrées dans des fichiers de sauvegarde avec la date et une requête pour supprimer les anciennes entrées. Voici un modèle de notre programme:

Il s’agit du principal flux du programme, mais, comme je l’ai dit, il y aura également quelques fonctions d’aide permettant de faire des choses comme obtenir la date, etc. Maintenant que nous avons tout tracé, nous pouvons commencer à construire le programme.

Bâtiment

Je vais utiliser la bibliothèque HDBC MySQL dans ce programme, que vous pouvez installer en exécutant cabale installer HDBC et cabale installer HDBC-mysql si vous avez la plate-forme Haskell installée. Commençons par les deux premières fonctions de la liste, celles-ci étant toutes deux intégrées à la bibliothèque HDBC:

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

Cette partie est assez simple; nous créons la connexion et mettons ensuite la liste des tables dans une variable, appelée les tables. La fonction suivante parcourt la liste des tables et obtient toutes les lignes de chacune. Un moyen rapide de le faire consiste à créer une fonction ne gérant qu'une valeur, puis d'utiliser le carte fonction pour l'appliquer au tableau. Puisque nous mappons une fonction IO, nous devons utiliser mapM. Avec ceci implémenté, votre code devrait maintenant ressembler à ceci:

 getQueryString name = "select * from" ++ name processTable :: IConnection conn => conn -> Chaîne -> IO [[SqlValue]] processTable conn name = do laisser qu = getQueryString nom de rangées <- 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 est une fonction pure qui renvoie un sélectionner requête, puis nous avons le réel processTable function, qui utilise cette chaîne de requête pour extraire toutes les lignes de la table spécifiée. Haskell est un langage fortement typé, ce qui signifie fondamentalement que vous ne pouvez pas, par exemple, mettre un int où un chaîne est censé y aller. Mais Haskell est aussi une "déduction de types", ce qui signifie que vous n'avez généralement pas besoin d'écrire les types et que Haskell le trouvera. Ici, nous avons une coutume Connecticut type, que je devais déclarer explicitement; c'est donc ce que la ligne au-dessus de la processTable la fonction fait.

La prochaine étape de la liste consiste à convertir les valeurs SQL renvoyées par la fonction précédente en chaînes. Une autre façon de gérer les listes, à part carte est de créer une fonction récursive. Dans notre programme, nous avons trois couches de listes: une liste de valeurs SQL, qui sont dans une liste de lignes, qui sont dans une liste de tables. j'utiliserai carte pour les deux premières listes, puis une fonction récursive pour gérer la dernière. Cela permettra à la fonction elle-même d'être assez courte. Voici la fonction résultante:

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

Ajoutez ensuite la ligne suivante à la fonction principale:

 laisser stringRows = map (map sqlToArrays) rows

Vous avez peut-être remarqué que, parfois, les variables sont déclarées comme var, et à d'autres moments, comme laisser var = function. La règle est essentiellement la suivante: lorsque vous essayez d’exécuter une fonction IO et de placer les résultats dans une variable, vous utilisez méthode; pour stocker les résultats d'une fonction pure dans une variable, vous utiliseriez plutôt laisser.

La prochaine partie va être un peu délicate. Nous avons toutes les lignes au format chaîne, et nous devons maintenant remplacer chaque ligne de valeurs par une chaîne d’insertion que MySQL comprendra. Le problème est que les noms de table sont dans un tableau séparé; donc un double carte la fonction ne fonctionnera pas vraiment dans ce cas. Nous aurions pu utiliser carte une fois, mais alors nous devrions combiner les listes en un - éventuellement en utilisant des tuples parce carte n'accepte qu'un paramètre d'entrée - j'ai donc décidé qu'il serait plus simple d'écrire de nouvelles fonctions récursives. Comme nous avons un tableau à trois couches, nous aurons besoin de trois fonctions récursives distinctes, afin que chaque niveau puisse transmettre son contenu à la couche suivante. Voici les trois fonctions avec une fonction d'assistance pour générer la requête SQL réelle:

 flattenArgs [arg] = "\" "++ arg ++" \ "" flattenArgs (arg1: args) = "\" "++ arg1 ++" \ "," ++ (flattenArgs args) nom iQuery args = " insérer dans "++ nom ++" valeurs ("++ (flattenArgs args) ++"); \ n "insertStrRows nom [arg] = nom iQuery arg insertStrRows nom (arg1: args) = (nom iQuery arg1) ++ (insertStrRows name args) insertStrTables [table] [lignes] = insertStrRows lignes de table: [] insertStrTables (table1: autre) (lignes1: etc) = (insertStrRows table1 lignes1): (insertStrTables autre etc.)

Encore une fois, ajoutez ce qui suit à la fonction principale:

 let insertStrs = tables insertStrTables stringRows

le flattenArgs et iQuery les fonctions fonctionnent ensemble pour créer la requête d'insertion SQL réelle. Après cela, nous n’avons plus que les deux fonctions récursives. Notez que, dans deux des trois fonctions récursives, nous entrons un tableau, mais la fonction renvoie une chaîne. Ce faisant, nous supprimons deux des tableaux imbriqués. Maintenant, nous avons seulement un tableau avec une chaîne de sortie par table. La dernière étape consiste à écrire les données dans les fichiers correspondants. c'est beaucoup plus facile, maintenant que nous ne traitons plus que d'un simple tableau. Voici la dernière partie avec la fonction pour obtenir la date:

 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

le dateStr function convertit la date actuelle en une chaîne au format, AAAA-MM-JJ. Ensuite, il y a la fonction filename, qui regroupe tous les éléments du nom de fichier. le writeToFile function prend en charge la sortie dans les fichiers. Enfin, le writeFiles La fonction itère dans la liste des tables, vous pouvez donc avoir un fichier par table. Il ne reste plus qu'à terminer la fonction principale avec l'appel à writeFiles, et ajouter un message informant l'utilisateur quand c'est fini. Une fois terminé, votre principale la fonction devrait ressembler à ceci:

 principal = faire 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"

Désormais, si l'une de vos bases de données perdait ses informations, vous pouvez coller les requêtes SQL directement à partir de son fichier de sauvegarde dans tout terminal ou programme MySQL pouvant exécuter des requêtes. il restaure les données à ce moment-là. Vous pouvez également ajouter un travail cron à exécuter toutes les heures ou tous les jours, afin de conserver vos sauvegardes à jour..


Finir

Il existe un excellent livre de Miran Lipovača intitulé "Learn you a Haskell".

C'est tout ce que j'ai pour ce tutoriel! À l'avenir, si vous souhaitez apprendre pleinement Haskell, il existe quelques bonnes ressources à consulter. Il existe un excellent livre de Miran Lipovača intitulé "Learn you a Haskell", qui a même une version gratuite en ligne. Ce serait un excellent début.

Si vous recherchez des fonctions spécifiques, consultez Hoogle, un moteur de recherche semblable à celui de Google qui vous permet d'effectuer une recherche par nom ou même par type. Donc, si vous avez besoin d’une fonction qui convertit une chaîne en une liste de chaînes, vous devez taper String -> [String], et il vous fournira toutes les fonctions applicables. Il existe également un site, appelé hackage.haskell.org, qui contient la liste des modules pour Haskell; vous pouvez les installer tous à travers cabale.

J'espère que vous avez apprécié ce tutoriel. Si vous avez des questions, n'hésitez pas à poster un commentaire ci-dessous; Je ferai de mon mieux pour vous répondre au plus vite.!