Haskell LanguageDémarrer avec le langage Haskell


Remarques

Logo Haskell

Haskell est un langage de programmation avancé, purement fonctionnel.

Caractéristiques:

  • Typiquement statiquement: chaque expression dans Haskell a un type qui est déterminé au moment de la compilation. La vérification de type statique est le processus de vérification de la sécurité de type d'un programme en fonction de l'analyse du texte d'un programme (code source). Si un programme passe un vérificateur de type statique, le programme est assuré de satisfaire un ensemble de propriétés de sécurité de type pour toutes les entrées possibles.
  • Purement fonctionnel : toute fonction dans Haskell est une fonction au sens mathématique. Il n'y a pas d'instructions ou d'instructions, seulement des expressions qui ne peuvent muter des variables (locales ou globales) ni des états d'accès tels que des nombres temporels ou aléatoires.
  • Concurrent: son compilateur phare, GHC, est livré avec une bibliothèque parallèle très performante de ramasse-miettes et de concurrences légères contenant un certain nombre de primitives et d'abstractions utiles.
  • Evaluation paresseuse: les fonctions n'évaluent pas leurs arguments. Retarde l'évaluation d'une expression jusqu'à ce que sa valeur soit nécessaire.
  • Usage général: Haskell est conçu pour être utilisé dans tous les contextes et environnements.
  • Packages: La contribution open source à Haskell est très active avec un large éventail de packages disponibles sur les serveurs de packages publics.

Le dernier standard de Haskell est Haskell 2010. En mai 2016, un groupe travaille sur la prochaine version, Haskell 2020.

La documentation officielle de Haskell est également une ressource complète et utile. Super endroit pour trouver des livres, des cours, des tutoriels, des manuels, des guides, etc.

Versions

Version Date de sortie
Haskell 2010 2012-07-10
Haskell 98 2002-12-01

Commencer

REPL en ligne

La manière la plus simple de commencer à écrire Haskell est probablement d'aller sur le site Web Haskell ou Try Haskell et d'utiliser la ligne REPL (read-eval-print-loop) en ligne sur la page d'accueil. Le REPL en ligne prend en charge la plupart des fonctionnalités de base et même certaines entrées-sorties. Il existe également un didacticiel de base qui peut être démarré en tapant l' help la commande. Un outil idéal pour commencer à apprendre les bases de Haskell et essayer certaines choses.

GHC (i)

GHCi est un environnement interactif fourni avec le compilateur Glorious / Glasgow Haskell . Le GHC peut être installé séparément, mais ce n'est qu'un compilateur. Pour pouvoir installer de nouvelles bibliothèques, des outils tels que Cabal et Stack doivent également être installés. Si vous utilisez un système d'exploitation de type Unix, l'installation la plus simple consiste à installer Stack en utilisant:

curl -sSL https://get.haskellstack.org/ | sh
 

Cela installe GHC isolé du reste de votre système, il est donc facile à supprimer. Toutes les commandes doivent cependant être précédées de stack . Une autre approche simple consiste à installer une plate-forme Haskell . La plateforme existe en deux versions:

  1. La distribution minimale ne contient que GHC (compiler) et Cabal / Stack (pour installer et compiler les paquets)
  2. La distribution complète contient en outre des outils pour le développement de projets, le profilage et l'analyse de la couverture. Un ensemble supplémentaire de packages largement utilisés est également inclus.

Ces plates-formes peuvent être installées en téléchargeant un programme d'installation et en suivant les instructions ou en utilisant le gestionnaire de paquets de votre distribution (notez que cette version n'est pas garantie pour être à jour):

  • Ubuntu, Debian, Mint:

    sudo apt-get install haskell-platform
     
  • Feutre:

    sudo dnf install haskell-platform
     
  • Chapeau rouge:

    sudo yum install haskell-platform
     
  • Arch Linux:

    sudo pacman -S ghc cabal-install haskell-haddock-api \
                   haskell-haddock-library happy alex
     
  • Gentoo:

    sudo layman -a haskell
    sudo emerge haskell-platform
     
  • OSX avec Homebrew:

    brew cask install haskell-platform
     
  • OSX avec MacPorts:

    sudo port install haskell-platform
     

Une fois installé, il devrait être possible de démarrer GHCi en ghci commande ghci n'importe où dans le terminal. Si l'installation s'est bien passée, la console devrait ressembler à

me@notebook:~$ ghci
GHCi, version 6.12.1: http://www.haskell.org/ghc/  :? for help
Prelude> 
 

éventuellement avec plus d'informations sur les bibliothèques chargées avant le Prelude> . Maintenant, la console est devenue une REPL Haskell et vous pouvez exécuter le code Haskell comme avec la REPL en ligne. Pour quitter cet environnement interactif, vous pouvez taper :q ou :quit . Pour plus d'informations sur les commandes disponibles dans GHCi , tapez :? comme indiqué dans l'écran de démarrage.

Parce qu'écrire les mêmes choses encore et encore sur une seule ligne n'est pas toujours pratique, il peut être judicieux d'écrire le code Haskell dans les fichiers. Ces fichiers ont normalement .hs pour une extension et peuvent être chargés dans la REPL en utilisant :l ou :load .

Comme mentionné précédemment, GHCi fait partie du GHC , qui est en fait un compilateur. Ce compilateur peut être utilisé pour transformer un fichier .hs avec du code Haskell en un programme en cours d'exécution. .hs fichier .hs pouvant contenir de nombreuses fonctions, une fonction main doit être définie dans le fichier. Ce sera le point de départ du programme. Le fichier test.hs peut être compilé avec la commande

ghc test.hs
 

Cela créera des fichiers d'objets et un exécutable s'il n'y avait pas d'erreurs et que la fonction main était définie correctement.

Des outils plus avancés

  1. Il a déjà été mentionné précédemment en tant que gestionnaire de paquets, mais la pile peut être un outil très utile pour le développement de Haskell. Une fois installé, il est capable de

    • installer (plusieurs versions de) GHC
    • création de projet et échafaudage
    • gestion des dépendances
    • projets de construction et d'essais
    • analyse comparative
  2. IHaskell est un noyau de haskell pour IPython et permet de combiner du code (exécutable) avec le marquage et la notation mathématique.

Déclaration des valeurs

Nous pouvons déclarer une série d'expressions dans le REPL comme ceci:

Prelude> let x = 5
Prelude> let y = 2 * 5 + x
Prelude> let result = y * 10
Prelude> x
5
Prelude> y
15
Prelude> result
150
 

Pour déclarer les mêmes valeurs dans un fichier, nous écrivons ce qui suit:

-- demo.hs

module Demo where
-- We declare the name of our module so 
-- it can be imported by name in a project.

x = 5

y = 2 * 5 + x

result = y * 10
 

Les noms de module sont en majuscules, contrairement aux noms de variables.

Factorial

La fonction factorielle est un Haskell "Hello World!" (et pour la programmation fonctionnelle en général) dans le sens où elle démontre succinctement les principes de base de la langue.

Variation 1

fac :: (Integral a) => a -> a
fac n = product [1..n]
 

Démo en direct

  • Integral est la classe des types de nombres entiers. Les exemples incluent Int et Integer .
  • (Integral a) => place une contrainte sur le type a pour être dans ladite classe
  • fac :: a -> a dit que fac est une fonction qui prend un a et retourne a
  • product est une fonction qui accumule tous les nombres d’une liste en les multipliant.
  • [1..n] est une notation spéciale qui désuère enumFromTo 1 n et représente la plage de nombres 1 ≤ x ≤ n .

Variation 2

fac :: (Integral a) => a -> a
fac 0 = 1
fac n = n * fac (n - 1)
 

Démo en direct

Cette variation utilise la correspondance de motif pour diviser la définition de fonction en cas séparés. La première définition est appelée si l'argument est 0 (parfois appelé la condition d'arrêt) et la deuxième définition autrement (l'ordre des définitions est significatif). Elle illustre également la récursivité en tant que fac .


Il est à noter que, en raison des règles de réécriture, les deux versions de fac seront compilées en un code machine identique lors de l’utilisation de GHC avec des optimisations activées. Donc, en termes d'efficacité, les deux seraient équivalents.

Fibonacci, en utilisant l'évaluation paresseuse

Une évaluation paresseuse signifie que Haskell évaluera uniquement les éléments de liste dont les valeurs sont nécessaires.

La définition récursive de base est la suivante:

f (0)  <-  0
f (1)  <-  1
f (n)  <-  f (n-1) + f (n-2)
 

S'il est évalué directement, ce sera très lent. Mais, imaginez que nous ayons une liste qui enregistre tous les résultats,

fibs !! n  <-  f (n) 
 

alors

                  ┌──────┐   ┌──────┐   ┌──────┐
                  │ f(0) │   │ f(1) │   │ f(2) │
fibs  ->  0 : 1 : │  +   │ : │  +   │ : │  +   │ :  .....
                  │ f(1) │   │ f(2) │   │ f(3) │
                  └──────┘   └──────┘   └──────┘

                  ┌────────────────────────────────────────┐
                  │ f(0)   :   f(1)   :   f(2)   :  .....  │ 
                  └────────────────────────────────────────┘
      ->  0 : 1 :               +
                  ┌────────────────────────────────────────┐
                  │ f(1)   :   f(2)   :   f(3)   :  .....  │
                  └────────────────────────────────────────┘
 

Ceci est codé comme:

fibn n = fibs !! n
    where
    fibs = 0 : 1 : map f [2..]
    f n = fibs !! (n-1) + fibs !! (n-2)
 

Ou même comme

GHCi> let fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
GHCi> take 10 fibs
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
 

zipWith crée une liste en appliquant une fonction binaire donnée aux éléments correspondants des deux listes qui lui sont données, donc zipWith (+) [x1, x2, ...] [y1, y2, ...] est égal à [x1 + y1, x2 + y2, ...] .

Une autre façon d’écrire des fibs est la fonction scanl :

GHCi> let fibs = 0 : scanl (+) 1 fibs
GHCi> take 10 fibs
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
 

scanl construit la liste des résultats partiels que foldl produirait, travaillant de gauche à droite le long de la liste d'entrée. C'est-à-dire que scanl f z0 [x1, x2, ...] est égal à [z0, z1, z2, ...] where z1 = f z0 x1; z2 = f z1 x2; ...

Grâce à l'évaluation différée, les deux fonctions définissent des listes infinies sans les calculer entièrement. C'est-à-dire que nous pouvons écrire une fonction fib , en récupérant le nième élément de la séquence Fibonacci sans limite:

GHCi> let fib n = fibs !! n  -- (!!) being the list subscript operator
-- or in point-free style:
GHCi> let fib = (fibs !!)
GHCi> fib 9
34
 

Bonjour le monde!

Un basique "Bonjour, Monde!" programme en Haskell peut être exprimé de manière concise en une ou deux lignes:

main :: IO ()
main = putStrLn "Hello, World!"
 

La première ligne est une annotation de type facultative, indiquant que main est une valeur de type IO () , représentant une action E / S qui "calcule" une valeur de type () (lire "unit"; le tuple vide ne transmettant aucune information) en plus d'effectuer des effets secondaires sur le monde extérieur (ici, imprimer une chaîne au terminal). Cette annotation de type est généralement omise pour main car c'est son seul type possible.

Mettez ceci dans un fichier helloworld.hs et compilez-le en utilisant un compilateur Haskell, tel que GHC:

ghc helloworld.hs
 

L'exécution du fichier compilé entraînera la sortie "Hello, World!" en cours d'impression sur l'écran:

./helloworld
Hello, World!
 

Sinon, runhaskell ou runghc permettent d'exécuter le programme en mode interprété sans avoir à le compiler:

runhaskell helloworld.hs
 

La REPL interactive peut également être utilisée au lieu de la compilation. Il est livré avec la plupart des environnements Haskell, tels que ghci fourni avec le compilateur GHC:

ghci> putStrLn "Hello World!"
Hello, World!
ghci> 
 

Vous pouvez également charger des scripts dans ghci à partir d'un fichier en utilisant load (ou :l ):

ghci> :load helloworld
 

:reload (ou :r ) recharge tout en ghci:

Prelude> :l helloworld.hs 
[1 of 1] Compiling Main             ( helloworld.hs, interpreted )

<some time later after some edits>

*Main> :r
Ok, modules loaded: Main.
 

Explication:

Cette première ligne est une signature de type, déclarant le type de main :

main :: IO ()
 

Les valeurs de type IO () décrivent des actions pouvant interagir avec le monde extérieur.

Étant donné que Haskell a un système de type Hindley-Milner à part entière qui permet l'inférence automatique de type, les signatures de type sont techniquement facultatives: si vous omettez simplement main :: IO () , le compilateur pourra déduire le type de lui-même en analyser la définition du main . Cependant, il est considéré comme un mauvais style de ne pas écrire les signatures de type pour les définitions de niveau supérieur. Les raisons incluent:

  • Les signatures de type dans Haskell sont une documentation très utile car le système de types est si expressif que vous pouvez souvent voir à quoi sert une fonction simplement en regardant son type. Cette «documentation» est facilement accessible avec des outils tels que GHCi. Et contrairement à la documentation normale, le vérificateur de type du compilateur s'assurera qu'il correspond réellement à la définition de la fonction!

  • Les signatures de type gardent les bogues locaux . Si vous faites une erreur dans une définition sans fournir sa signature de type, le compilateur peut ne pas signaler immédiatement une erreur, mais simplement en déduire un type absurde, avec lequel il vérifie réellement. Vous pouvez alors recevoir un message d'erreur crypté lorsque vous utilisez cette valeur. Avec une signature, le compilateur est très efficace pour détecter les bogues là où ils se produisent.

Cette deuxième ligne fait le travail réel:

main = putStrLn "Hello, World!"
 

Si vous venez d'un langage impératif, il peut être utile de noter que cette définition peut également être écrite comme suit:

main = do {
   putStrLn "Hello, World!" ;
   return ()
   }
 

Ou de manière équivalente (Haskell a une analyse basée sur la mise en page, mais méfiez-vous du mélange incohérent des onglets et des espaces, ce qui compliquerait ce mécanisme):

main = do
    putStrLn "Hello, World!"
    return ()
 

Chaque ligne d'un bloc do représente un calcul monadique (ici, E / S), de sorte que tout do bloc do représente l'action globale composée de ces sous-étapes en les combinant de manière spécifique à la monade donnée (pour les E / S). cela signifie simplement les exécuter l'un après l'autre).

La syntaxe do est elle-même un sucre syntaxique pour les monades, comme IO ici, et return est une action sans opération produisant son argument sans effectuer aucun effet secondaire ou calcul supplémentaire pouvant faire partie d'une définition de monade particulière.

Ce qui précède est identique à la définition de main = putStrLn "Hello, World!" , parce que la valeur putStrLn "Hello, World!" a déjà le type IO () . Vu comme une "déclaration", putStrLn "Hello, World!" peut être vu comme un programme complet, et vous définissez simplement main pour faire référence à ce programme.

Vous pouvez rechercher la signature de putStrLn ligne :

putStrLn :: String -> IO ()
-- thus,
putStrLn (v :: String) :: IO ()
 

putStrLn est une fonction qui prend une chaîne comme argument et putStrLn une action E / S (c'est-à-dire une valeur représentant un programme que le moteur d'exécution peut exécuter). Le moteur d'exécution exécute toujours l'action nommée main , il suffit donc de la définir comme étant égale à putStrLn "Hello, World!" .

Primes

Quelques variantes les plus saillantes :

Moins de 100

import Data.List ( (\\) )

ps100 = ((([2..100] \\ [4,6..100]) \\ [6,9..100]) \\ [10,15..100]) \\ [14,21..100]

   -- = (((2:[3,5..100]) \\ [9,15..100]) \\ [25,35..100]) \\ [49,63..100]

   -- = (2:[3,5..100]) \\ ([9,15..100] ++ [25,35..100] ++ [49,63..100])
 

Illimité

Tamis d'Eratosthenes, en utilisant le paquet data-ordlist :

import qualified Data.List.Ordered

ps   = 2 : _Y ((3:) . minus [5,7..] . unionAll . map (\p -> [p*p, p*p+2*p..]))

_Y g = g (_Y g)   -- = g (g (_Y g)) = g (g (g (g (...)))) = g . g . g . g . ...
 

Traditionnel

(un tamis de division d'essai sous-optimal)

ps = sieve [2..]
     where
     sieve (x:xs) = [x] ++ sieve [y | y <- xs, rem y x > 0]

-- = map head ( iterate (\(x:xs) -> filter ((> 0).(`rem` x)) xs) [2..] )
 

Division d'essai optimale

ps = 2 : [n | n <- [3..], all ((> 0).rem n) $ takeWhile ((<= n).(^2)) ps]

-- = 2 : [n | n <- [3..], foldr (\p r-> p*p > n || (rem n p > 0 && r)) True ps]
 

De transition

De la division d'essai au tamis d'Eratosthène:

[n | n <- [2..], []==[i | i <- [2..n-1], j <- [0,i..n], j==n]]
 

Le code le plus court

nubBy (((>1).).gcd) [2..]          -- i.e., nubBy (\a b -> gcd a b > 1) [2..]
 

nubBy provient également de Data.List , comme (\\) .