Ce document est destiné à devenir, à terme, une série complète de fiches sur le langage Haskell.
Elles n’ont pas la prétention d’être un support d’apprentissage autonome, et présupposent une connaissance, même élémentaire, du langage. Elles ont vocation à présenter, de manière extrêmement synthétique, pour mémoire ou référence, l’essentiel de Haskell, et pas à se substituer à un apprentissage sérieux.
J’ai décidé de préparer ces fiches pour combler un manque dans la documentation sur Haskell. Les livres dont je dispose sur le langage sont soit trop superficiels, soit insuffisamment systématiques. Certains présentent une version superficelle du langage, d’autres, les plus élaborés, souffrent au contraire du syndrome de l’enseignant-passionné-mais-pas-méthodique : ils ne cessent de s’interrompre et de proposer des apartés hors de propos qui mobilisent soudainement des notions qui ne seront explicitées que plusieurs chapitres plus loin. (Ce qui précède est devenu nettement moins vrai avec le livre d'Allen et Moronuki).
Ces fiches tentent donc une présentation plus rigoureusement systématique, mais sans prétention pédagogique. L’ordre d’exposition n'y est absolument pas linéaire, et l'organisation tente d'être plus logique que pédagogique.
Ce travail est à peine entamé. À terme, il devrait offrir un aperçu complet du langage, et de quelques une des extensions de GHC.
L'implémentation de Haskell la plus répandue est GHC, The Glasgow Haskell Compiler. GHC fournit trois binaires: un compilateur (ghc
), un interpréteur (runghc
) et un interpréteur ligne à ligne (REPL) (ghci
).
GHC reconnaît les formats d'entrées suivants:
Extension | Format |
---|---|
.hs |
Source Haskell |
.lhs |
Source Literate Haskell |
La liaison d'un exécutable avec ghc a tendance à être très longue, d'une part parce que ld est en général assez lent, d'autre part parce que ghc tend à lier plus de symboles qu'il n'est nécessaire. Pour accélérer la liaison (uniquement lors du développement et pas pour produire des binaires destinés à la distribution, évidemment), on peut invoquer ghc avec le drapeau -dynamic
.
Haskell dispose de deux outils de construction (make): Cabal et Stack. Ces deux outils implémentent la même spécification pour automatiser le processus de compilation. Ils tentent chacun d'assurer la reproductibilité de la compilation et de la résolution des dépendances, et de faciliter la cohabitation de plusieurs versions de la même librairie.
Le nom «Cabal» (pour Common Architecture for Building Applications and Libraries) désigne à la fois:
cabal-install
)Stack est une implémentation alternative de la même spécification. Un projet construit pour n'importe lequel des deux moteurs devrait compiler avec l'autre.
Dans la mesure où les deux outils gèrent les dépendances de façon différente, et d'une façon différente de GHC, ils fournissent chacun une commande pour invoquer le REPL avec les bonnes dépendances: cabal repl
et stack repl
.
.cabal
cabal-install
)Cabal-le-programme est le programme de construction «historique» de Haskell. Jusqu'à la version 1.24, il reposait sur un mécanisme de «bac à sable» pour isoler les dépendances nécessaires à un projet. Le coût de cette solution était une initialisation relativement longue pour les projets ayant de nombreuses dépendances. À partir de la version 1.24, Cabal fournit new-build
, qui utilise un dépôt centralisé.
Stack est un moteur de construction alternatif qui suit la spécification Cabal, mais fournit sa propre implémentation.
@TODO
Haskell connaît deux structures conditionnelles: if
et case
.
if
Une clause if
est une expression, pas une structure de contrôle. La syntaxe est if a then b else c
, où a
est une expression de type Bool
, b
et c
des expressions d'un type quelconque. Si a
est vraie, l'expression vaut b
, sinon c
.
Comme c'est une expression, on peut affecter son résultat directement à une variable:
= if even x then "pair" else "impair" a
Que if
soit une primitive du compilateur n'est justifié que par le gain de clarté qu'il apporte. L'implémenter en Haskell directement est trivial:
if' :: Bool -> a -> a
True a _ = a
if' = b if' _ _ b
case
let a = [1..] -- a est la liste de l'ensemble des entiers positifs
let b = map ((^^) 2) a
L'évaluation paresseuse a un prix, qui est une plus grande consommation de mémoire : au lieu d'évaluer 2 + 2
, Haskell stocke un thunk, c'est à dire en gros un calcul différé. Mais sur les gros traitements récursifs, l'accumulation de thunk peut entrainer rapidement un débordement de mémoire. La commande seq
force l'évaluation et permet d'éviter un débordement de mémoire.
L'évaluation paresseuse obéit à des règles strictes.
Il est possible de déterminer avec précision si une expression va être évaluée, et si oui quand. C'est parce qu'il est garanti qu'une expression dont le résultat n'est pas utilisé ne sera pas évaluée qu'on peut, par exemple, programmer des opérateurs logiques court-circuitants directement en Haskell, ou manipuler des suites infinies.
N'importe quelle fonction ou type peut accepter des paramètres d'un type non défini. Sa signature remplace dans ce cas le nom d'un type par un paramètre de type, qui commence par une minuscule.
Le type Maybe
, qui représente une valeur possible, est un exemple de type polymorphique. Il a deux constructeurs : Nothing
et Just
a
. Nothing
ne prend pas de paramètre, et représente l'absence de valeur. Just a
prend un paramètre du type quelconque a
.
> :type Just 3
ghciJust 3 :: Num a => Maybe a
> :type Just "Une chaîne"
ghciJust "Une chaîne" :: Maybe [Char]
> :type Nothing
ghciNothing :: Maybe a
Une fonction peut accepter, ou renvoyer, des types non-définis.
third :: [a] -> Maybe a
:_:x:_) = Just x
third (_= Nothing third _
«Théorèmes gratuits»
Comme une fonction polymorphique n'a pas accès au type réel de son paramètre, on peut déduire (au sens strict) ce qu'elle peut faire à sa seule signature.
[a] -> a~ n'a pas accès au type a
, et par conséquent ne peut ni construire un nouvel a
, ni modifier un des a
de la liste [a]
d'entrée: elle doit en renvoyer un tel quel. On peut donc déduire que head b `elem` b
.
(a, b) -> a~ ne peut rien faire d'autre que renvoyer le premier élément de la paire qui lui est passée, et ignorer le second.
explicite le soubassement logico-mathématique de ce principe et montre des applications à des cas beaucoup plus complexes que ces quelque exemples.
Un module a le même nom que le fichier .hs qui le contient, et ce nom commence par une majuscule. La déclaration de module a la syntaxe suivante:
-- MyModule.hs
module Mod ( x, y, z ) where -- code
Cette déclaration exporte les identifiants x, y et z du code qui la suit. On exporterait la totalité des noms en enlevant la parenthèse, et aucun en la laissant vide.
Ce n'est pas une erreur de syntaxe que de ne pas respecter la règle nom du module = nom du fichier. Un tel module ne serait simplement pas importable, puisque le compilateur ne saurait pas où le trouver.
TODO exporter un type mais pas ses constructeurs.
-- Commande -- Importé
import Mod -- x, y, z, Mod.x, Mod.y, Mod.z
import Mod () -- Uniquement les instances, voir ci-dessous.
import Mod (x,y) -- x, y, Mod.x, Mod.y
import qualified Mod -- Mod.x, Mod.y, Mod.z
import qualified Mod (x,y) -- Mod.x, Mod.y
import Mod hiding (x,y) -- z, Mod.z
import qualified Mod hiding (x,y) -- Mod.z
import Mod as Foo -- x, y, z, Foo.x, Foo.y, Foo.z
import Mod as Foo (x,y) -- x, y, Foo.x, Foo.y
import qualified Mod as Foo -- Foo.x, Foo.y, Foo.z
import qualified Mod as Foo (x,y) -- Foo.x, Foo.y
Même sans importer aucun nom (c'est le cas de import Mod ()
), tout import
importe les instances de classes de types définies dans le module importé.
Les instances de classes de types définies dans un module sont toujours exportées et importées, quels que soient les noms explicitement importés ou exportés, et même s'il n'y en a aucun.
Le Prélude (Prelude
) est la librairie fondamentale d'Haskell. Contrairement aux autres modules, il est importé implicitement (cette importation peut néanmoins être contrôlée avec une clause import
explicite).
L'implémentation de référence est écrite en Haskell.
Il est particulièrement intéressant de noter que parmi les définitions fournies par le Prélude, un certain nombre sont, dans la plupart des langages procéduraux, définies au niveau du compilateur. Parmi celles-ci, on trouve notamment les opérateurs booléens court-circuitants, dont l'implémentation est rendue triviale par le principe d'évaluation paresseuse.
module Prelude (
module PreludeList, module PreludeText, module PreludeIO,
Bool(False, True),
Maybe(Nothing, Just),
Either(Left, Right),
Ordering(LT, EQ, GT),
Char, String, Int, Integer, Float, Double, Rational, IO,
-- These built-in types are defined in the Prelude, but are
-- denoted by built-in syntax, and cannot legally appear in an
-- export list. List type: []((:), []) Tuple types: (,)((,)),
-- (,,)((,,)), etc. Trivial type: ()(()) Functions: (->)
Eq((==), (/=)),
Ord(compare, (<), (<=), (>=), (>), max, min),
Enum(succ, pred, toEnum, fromEnum, enumFrom, enumFromThen,
enumFromTo, enumFromThenTo),
Bounded(minBound, maxBound),
Num((+), (-), (*), negate, abs, signum, fromInteger),
Real(toRational),
Integral(quot, rem, div, mod, quotRem, divMod, toInteger),
Fractional((/), recip, fromRational),
Floating(pi, exp, log, sqrt, (**), logBase, sin, cos, tan,
asin, acos, atan, sinh, cosh, tanh, asinh, acosh, atanh),
RealFrac(properFraction, truncate, round, ceiling, floor),
RealFloat(floatRadix, floatDigits, floatRange, decodeFloat,
encodeFloat, exponent, significand, scaleFloat, isNaN,
isInfinite, isDenormalized, isIEEE, isNegativeZero, atan2),
Monad((>>=), (>>), return, fail),
Functor(fmap),
mapM, mapM_, sequence, sequence_, (=<<),
maybe, either,
&&), (||), not, otherwise,
(subtract, even, odd, gcd, lcm, (^), (^^),
fromIntegral, realToFrac,
fst, snd, curry, uncurry, id, const, (.), flip, ($), until,
asTypeOf, error, undefined,
seq, ($!)
where )
Haskell fait partie des rares langages à gérer nativement la programmation lettrée. Les fichiers sources ont l'extension .lhs
(au lieu de .hs
) et les blocs de code peuvent être délimités de deux façons
>
. Chaque bloc de code doit être précédé d’au moins une ligne vide.\begin{code}
et \end{code}
.La fonction de composition est (.) :: (b -> c) -> (a -> b) -> a ->
c
.
Le point-free style est un style de programmation dans lequel une fonction n'identifie pas les arguments sur lesquels elle s'opère, mais compose simplement d'autres fonctions.
Par exemple:
reverseTailPointFree :: [a] -> [a]
reverseTail,= reverse . tail $ x
reverseTail x = reverse . tail reverseTailPointFree
Les différents folds sont des catamorphismes.
foldr :: Foldable t => (a -> b -> b) -> b
-> t a -> b
foldl :: Foldable t => (b -> a -> b) -> b
-> t a -> b
Data.List.foldl' :: Foldable
t => (b -> a -> b) -> b -> t a -> b
Cette version
Voir le tutorial de .
Haskell connaît deux espèces essentielles de type: les types de données et les types de fonctions. Cette section traite uniquement des premiers, les seconds ont leur propre section.
En Haskell, la casse du premier caractère d'un identifiant a une importance. Un nom de type ou un constructeur de type commencent toujours par une majuscule, un nom de fonction ou de variable par une minuscule.
Si on comprend qu'un type est un ensemble (de valeurs possibles), on voit aisément pourquoi on parle de types produit ou somme:
Une alternative entre type (data OuBien = Chaine String | Entier
Int
) a pour cardinal la somme des cardinaux des types qui le constituent.
Une combinaison de types (data ChaineEtEntier = ChaineEtEntier
String Int
) a pour cardinal le produit des cardinaux des types qui le constituent.
Une application de types (TypeA -> TypeB
) a pour cardinal le cardinal du type du résultat élevé à la puissance du cardinal du type du paramètre. Le cardinal de a -> b
est donc \#b^{\#a}.
(Le cardinal d'un type défini avec data
est simplement une somme de produits.)
Haskell fournit un grand nombre de types élémentaires, dont les plus importants sont résumés dans ce tableau:
Type | Description |
---|---|
Double |
Virgule flottante, double précision |
Float |
Virgule flottante, simple précision |
Integer |
Entier signé en précision arbitraire |
Int |
Entier signé à précision fixe, intervalle minimum [-2^{29} ; 2^{29}-1] |
Int8 , Int16 , Int32 , Int64 |
Entier signé de n bits |
Word8 , Word16 , Word32 , Word64 |
Entier non signé de n bits |
Rational, ~Ratio a |
Nombre rationnel de précision arbitraire |
data Book = NewBook String [String] Int
-- ^ ^__ Année de publication
-- | ^___________ Auteurs
-- |__________________ Titre
Cette ligne définit un type nommé Book
qui fournit un unique constructeur NewBook
. Le constructeur NewBook
se comporte comme une fonction qui prend trois paramètres et qrenvoie un Book
: NewBook :: String -> [String] -> Int -> Book
. Pour construire un nouveau Book
, on écrit donc book = NewBook "Critique of Pure
Reason" ["Immanuel Kant"] 1781
. Dans cette syntaxe, les arguments du constructeur sont positionnels et doivent être fournis dans l'ordre de la déclaration.
Il est légal et très courant, quand un type ne fournit qu'un seul constructeur, de donner à ce constructeur le nom du type. On aurait alors data Book = Book String [String] Int
. L'exemple précédent les distingue par souci de clarté, mais n'est pas vraiment idiomatique.
Un type somme présente une alternative en offrant plusieurs constructeurs.
data Bool = True | False
data Maybe a = Nothing | Just a
Les types produits et sommes ne sont pas mutuellement exclusifs: data Something = This String Int | That String Bool
.
La syntaxe d'enregistrement permet de nommer les champs.
data Book = Book {
bookTitle :: String, -- bookTitle :: Book -> String
bookAuthors :: [String], -- bookAuthors :: Book -> [String]
bookYear :: Int -- bookYear :: Book -> Int
}
Un type qui utilise cette syntaxe peut être instantié avec des arguments positionnels ou des arguments nommés. Ces derniers peuvent être fournis dans n'importe quel ordre:
= Book "Critique de la Raison Pure" ["Immanuel Kant"] 1781
crp = Book {
tlp = 1921,
bookYear = ["Ludwig Wittgenstein"],
bookAuthors = "Tractatus Logico-Philosophicus"
bookTitle }
Il définit automatiquement une fonction accesseur pour chacun de ses champs. Le type Book
ci-dessus fournit ainsi trois fonctions bookYear :: Book -> Int
, bookAuthors :: Book -> [String]
et bookTitle :: Book -> String
:
> bookYear tlp 1921 ghci
Enfin, il permet de construire une nouvelle valeur à partir des champs d'une valeur existante:
= tlp {bookTitle = "Recherches philosophiques", bookYear=1953} rp
@TODO (On peut considérer un type algébrique comme un contexte sémantique pour un type natif.)
Un type peut faire référence à lui-même. On peut construire un type liste identique au type natif de la façon suivante:
data List a = Empty | Cons a (List a) list = (Cons 1 (Cons 2 (Cons 3
Empty)))
Un arbre binaire:
data BTree a = Node a (BTree a) (BTree a) | Empty deriving Show
Haskell permet de définir des synonymes pour des types existants. Les synonymes de type permettent d'augmenter la lisibilité du code ou de masquer des détails d'implémentation. Contrairement aux types définis avec data
, les informations des synonymes ne sont pas conservées à la compilation.
type
crée un synonyme d'un type existant. Le synonyme et le type auquel ils renvoient sont interchangeables.
type ObjectId = Int16
Les synonymes créés avec type
peuvent servir:
type ISBN = Int
pour un type Book
, par exemple):type Authors = [String]
type Title = String
type ISBN = Int
type Year = Int
data Book2 = Authors Title Year ISBN
type Weird = (Int -> String) -> (Int -> Int) -> [Int] -> [(Int, String, Int)]
Le mot-clé newtype
permet de dupliquer un type, et crée un type distinct de l'original. Les synonymes créés avec newtype
ne sont pas substituables avec le type dont ils sont synonymes. De plus, il n'appartiennent pas automatiquement aux types de classe de ce dernier. Leur syntaxe est très proche de celle de data
:
newtype MyType = MyType Int
Contrairement à data
, newtype
:
n'autorise qu'un seul constructeur et un seul champ.
ne conserve pas les informations du type après la compilation. Dans le programme compilé, MyType
ci-dessus est traité comme un
simple Int
:
Contrairement à type
,
il ne maintient pas la substituabilité du nouveau type et du type dont il est un synonyme. *Alors que type
sert à faciliter la lecture, newtype
est plutôt utilisé pour masquer l'implémentation*. Il permet notamment de masquer un type sous-jacent sans la perte de performances liée à l'usage de data
, par exemple: newtype ResourceHandle = ResourceHandle
Int16
. Comme il est possible à un module de n'exporter que le type mais pas le constructeur, un programme peut recevoir et transmettre des données de type ResourceHandle
sans connaître leur type réel.
Il permet aussi, sans perte de performances, de fournir des instances différentes d'une unique classe de type pour un type donné:
-- Data.Monoid
-- Booléen selon la conjonction
newtype All = All { getAll :: Bool }
deriving (Eq, Ord, Read, Show, Bounded)
instance Monoid All where
mempty = All True
mappend (All x) (All y) = All (x && y)
-- Booléen selon la disjonction
newtype Any = Any { getAny :: Bool }
deriving (Eq, Ord, Read, Show, Bounded)
instance Monoid Any where
mempty = Any False
mappend (Any x) (Any y) = Any (x || y)
Enfin, newtype
est strict.
Les classes de type ne sont pas des classes au sens que ce terme possède en POO. Elles sont plus proches de ce qu'on nomme des interfaces : elles décrivent des fonctions pour lesquelles un type qui appartient à la classe fournit une implémentation.
class Parsable a where
parse :: String -> a
Une implémentation par défaut peut être fournie. La classe de type Eq
par exemple est définie comme:
class Eq a where
==), (/=) :: a -> a -> Bool
(/= y = not (x == y)
x == y = not (x /= y) x
Version triviale:
data CanardLapin = { canard :: Bool, lapin :: Bool }
instance Show CanardLapin where
show (CanardLapin True False) = "Seulement un canard"
show (CanardLapin False True) = "Seulement un lapin"
show _ = "Un canard lapin!"
Plus marrant: dériver un type higher-kinded en fonction de son paramètre:
{-# LANGUAGE FlexibleInstances #-}
class Magique a where
magie :: a -> a
data CanardLapin' = Canard | Lapin
instance Functor f, Magique m => Magique (f m) where
= fmap magie
magie
instance Magique CanardLapin' where
Canard = Lapin
magie Lapin = Canard magie
À certaines classes de type sont associées des lois (lois des foncteurs, des monades, etc.) que le compilateur ne peut pas nécessairement contrôler. Il est donc possible de construire des instances pathologiques de classes de type sans recevoir d'avertissement du compilateur. Autrement dit, le système des classes de types est en partie formel, en partie contractuel. Il convient de vérifier les dimensions contractuelles dans la documentation de la classe de type. Il est aussi possible, dans une certaine mesure, d'automatiser la vérification de conformité.
Les types crées avec data
et newtype
peuvent dériver automatiquement certaines classes avec le mot clé deriving
:
data Something = Something Integer Integer
deriving (Show)
La dérivation automatique est implémentée au niveau du compilateur, et ne concerne que quelques classes de type du Prélude.
Les Kinds sont aux types ce que les types sont aux valeurs. Autrement dit, c'est le type d'un constructeur de type. Un type ordinaire a @TODO
Haskell n'a pas de notion de variable au sens qu'a ce terme en programmation procédurale. Il est possible d'assigner une expression ou une valeur à un nom, avec la syntaxe nom = expression
, mais nom
est immuable, et est donc plus proche d'une constante (c'est une variable au sens mathématique du terme).
En combinant ceci avec les principes de transparence référentielle, d'évaluation paresseuse et d'application partielle, on voit facilement qu'il n'existe aucune différence stricte entre une fonction et une ariable, donc qu'il n'existe pas de variables. Par exemple:
= 3 * 2
a = 3 * x
times3 x = times3 2
b = 6 c
Ici, times3
est une fonction, a
, b
et c
des variables. Dans la mesure où la valeur d'aucune n'est évaluée tant qu'elle n'est pas utilisée, la variable a
a strictement la même valeur que b
, qui n'est pas 6, mais le thunk 3 * 2
.
Cette identité n'est vraie que des fonctions pures. Les fonctions impures, comme par exemple getLine
, peuvent évidemment renvoyer un résultat différent à chaque invocation. Voir la section portant sur les entrées-sorties.
La suite de cette fiche ne s'intéresse donc qu'aux fonctions, puisque les «variables» n'en sont qu'un cas particulier.
La signature a la forme f :: TypeA -> TypeRet
, ce qui signifie que la fonction prend un paramètre de type TypeA
et renvoie une valeur de type TypeRet
.
Une fonction définie avec plusieurs paramètres a pour signature f ::
TypeA -> TypeB -> TypeC -> TypeRet
. Cette syntaxe est explicitée fiche . Les fonctions d'ordre supérieur utilisent les parenthèses pour indiquer qu'elles prennent une autre fonction en paramètre. Par exemple, le type map ::
(a -> b) -> [a] -> [b]
se lit : map
prend comme premier paramètre une fonction quelconque x :: a -> b
. Une variable ou une fonction sans paramètres a pour type nom :: Type
.
Une fonction se définit de la façon suivante:
add :: Int -> Int -> Int -- Signature de type, généralement optionnel.
= a + b add a b
Une fonction infixe se définit en entourant son nom de parenthèses, comme pour l'utiliser en préfixe:
+/) a b = a + b + a / b (
Une fonction est dite préfixe si son nom est placé avant ses arguments, et infixe si son nom est placé entre ses arguments. map
est une fonction préfixe, +
est infixe. La distinction est syntaxique, et se fait au niveau des caractères qui constituent le nom de la fonction.
Une fonction infixe a un nom composé uniquement de symboles non alphanumériques: +
, *
ou >>=
sont infixes. On peut utiliser une fonction infixe comme préfixe en entourant son nom de parenthèses : (+) 1 1
.
Une fonction préfixe a un nom composé de caractères alphanumériques. map
, elem
ou foldr
sont préfixes. On peut utiliser une fonction préfixe comme infixe en entourant son nom de : 1 `elem` [1..10]
.
On peut définir des fonctions dont la visibilité est limitée à une fonction. C'est utile pour définir des constantes, ou fournir des fonctions utilitaires qui n'ont pas besoin d'être disponibles au niveau du module. Haskell propose deux syntaxes: let
, qui place les variables locales avant le code de la fonction, et where
, qui les positionne après.
circLet :: Fractional a => a -> a
= let pi = 3.14
circLet radius = 2 * radius
diam in pi * diam
circWhere :: Fractional a => a -> a
= pi * diam
circWhere radius where pi = 3.141592653589793
= 2 * radius diam
Le choix de l'une ou de l'autre syntaxe est une question de goût et de lisibilité.
On peut les imbriquer: une fonction locale peut à son tour définir des fonctions locales, etc.
La visiblité des fonctions locales est limitée à la définition englobante.
L'associativité et la précédence sont collectivement nommées «fixité». La fixité d'une fonction infixe (et de n'importe quelle fonction préfixe dans sa forme infixe, comme `elem`
) est fixée par une déclaration infixl
(associatif à gauche), infixr
(associatif à droite) ou infix
(non-associatif), suivie de l'ordre de précédence compris entre 0 et 9 et du nom de la fonction:
(+/) :: Num a => a -> a -> a
infixl 9 +/
+/) a b = a + b + a / b (
Il est possible de définir la fixité d'une fonction locale, directement dans la clause let
ou where
où elle est définie.
«Déconstruire» un argument d'une fonction permet d'obtenir directement les arguments du constructeur. Par exemple, la fonction suivante déconstruit un constructeur de paire (tuple de deux éléments) pour en intervertir les termes:
toggle :: (a, b) -> a
= (y, x) toggle (x, y)
Un paramètre non utilisé peut être remplacé par un _
:
duplFirst :: (a, b) -> (a, a)
= (x, x) duplFirst (x, \_)
On n'a pas besoin du second membre de la paire: on la décompose donc en évitant de nommer cet élément.
De la même façon, si le paramètre est un Maybe
, on peut récupérer directement sa valeur en déconstruisant Just
:
double :: Maybe Int -> Int
Just x) = x * 2 double (
On peut avoir besoin de déconstruire un paramètre selon un motif en conservant le paramètre entier. Les motifs nommés permettent d'éviter des suites déconstruction-reconstruction redondantes. La fonction suffixes
(d'après ) renvoie tous les suffixes d'une liste. Elle peut s'écrire:
suffixes :: [a] -> [[a]] suffixes xs(/:xs') = xs : suffixes xs'
/ = [] suffixes
Le filtrage par motifs et l'emploi de gardes permettent de proposer différentes implémentations d'une même fonction selon les paramètres qui y sont passés, de façon similaire à l'emploi de cas en notation mathématique :
f(x) = \begin{cases} f(x-1) + x & \text{si } x > 0 \\ 1 & \text{sinon} \end{cases}
Le filtrage par motifs permet de choisir une implémentation selon le type et dans une certaine mesure la valeur des paramètres, les gardes selon une expression arbitraire.
Le filtrage par motif et les gardes permettent de définir plusieurs cas qui se recouvrent. Par exemple, une fonction peut fournir une implémentation pour n'importe quelle liste, et une autre pour n'importe quelle liste non vide. Haskell utilise toujours la première implémentation qui s'applique aux paramètres, dans l'ordre de déclaration : il faut donc déclarer les moins générales en premier.
Le filtrage par motifs permet de filtrer selon un constructeur ou selon une valeur arbitraire.
Le filtrage par constructeurs permet de sélectionner quel constructeur d'un type algébrique correspond à quelle implémentation.
maybeIntToStr :: Maybe Int -> String
Just a) = show a
maybeIntToStr (Nothing = "NaN" maybeIntToStr
mySum :: (Num a) => [a] -> a
:xs) = x + mySum xs
mySum (x= 0 mySum []
Le filtrage par valeur littérale est le plus simple. Il choisit une implémentation si un paramètre a une valeur déterminée.
compte :: String -> String -> Int -> String
0 = "Aucun(e) " ++ singulier
compte singulier pluriel 1 = "Un(e) " ++ singulier
compte singulier pluriel = show quantite ++ " " ++ pluriel compte singulier pluriel quantite
Une valeur littérale doit être littérale et ne peut pas, pour des raisons syntaxiques, être une variable. Un nom de variable dans une définition de fonction est toujours un paramètre de la fonction, jamais un filtre par valeur.
Certaines implémentations d'une fonction peuvent ne pas faire usage de tous les paramètres. On ignore un paramètre dans la définition avec le symbole _
: La fonction compte
ci-dessus pourrait s'écrire:
compte :: String -> String -> Int -> String
0 = "Aucun(e) " ++ singulier
compte singulier _ 1 = "Un(e) " ++ singulier
compte singulier _ = show quantite ++ " " ++ pluriel compte _ pluriel quantite
_
n'est pas un nom de variable mais la mention explicite que le paramètre ne sera pas utilisé.
Un garde est une expression de type Bool
. Si l'expression s'évalue à True
, l'implémentation qui suit est utilisée.
Leur syntaxe est:
| garde = impl func args
Par exemple, une fonction qui détermine si un nombre est pair, qui s'implémenterait naïvement sous la forme isEven x = if x `mod` 2 == 0
then True else False
peut s'écrire plus lisiblement:
| x mod 2 == 0 = True
isEven x = False isEven _
La partie à gauche du garde peut être omise si elle est identique à celle qui précède (c'est-à-dire si l'éventuel motif est le même):
| x mod 2 == 0 = True
isEven x | otherwise = False
otherwise
est une variable définie dans le Prélude. Sa valeur est simplement True
.
Haskell 2010 étend la syntaxe des gardes
gardes :: Int -> String gardes a | odd a, a =mod= 5 == 0 = "Impair et/ou
multiple de 5" | even a = "Pair mais pas multiple de 5"
@TODO
Une fonction, quel que soit le nombre de paramètres avec lequel elle a été déclarée, ne prend qu'un seul paramètre et renvoie une autre fonction. Le type de +
, par exemple, est : Num a => Num a -> Num a
-> Num a
, ce qui signifie que +
prend un premier paramètre d'un type de type Num
Les fonctions anonymes se notent:
-> a + b + c \a b c
Le Prélude fournit quelques fonctions qui peuvent remplacer des formes de lambdas très courantes: const :: a -> b -> a
et id :: a -> a
.
Données et contrôle
Cette fiche résume quelques unes des fonctions essentielles applicables à des listes. La plupart sont dans le Prélude, les autres dans Data.List
.
(++) , (<>) |
[a] -> [a] -> [a] |
|
head , last |
[a] -> a |
|
tail |
[a] -> [a] |
s |
drop , take |
Int -> [a] -> [a] |
|
dropWhile , takeWhile |
(a -> Bool) -> [a] -> [a] |
|
span |
:: (a -> Bool) -> [a] -> ([a], [a]) |
Regroupe les éléments qui vérifient la fonction dans une liste séparée |
group |
:: Eq a => [a] -> [[a]] |
Regroupe les éléments identiques |
map |
(a -> b) -> [a] -> [b]~ |
Applique une fonction sur chaque élément |
zip |
[a] -> [b] -> [(a, b)] |
|
zip3 |
[a] -> [b] -> [c] -> [(a, b, c)] |
|
zipWith |
(a -> b -> c) -> [a] -> [b] -> [c] |
|
zipWith3 |
(a -> b -> c -> d) -> [a] -> [b] -> [c] -> [d] |
Généralisé à n: zipWith4 , zipWith5 … |
concatMap |
(a -> [b]) -> [a] -> [b] |
|
fmap |
Functor f => (a -> b) -> f a -> f b |
Un Foldable
est un conteneur dont les éléments peuvent être «repliés» en une valeur unique. Informellement, c'est un type qui implémente foldr
, mais la définition est beaucoup plus dense:
class Foldable t where
{-# MINIMAL foldMap | foldr #-} -- Ce pragma signifie que foldMap ou
-- foldr suffisent à définir une instance.
fold :: Monoid m => t m -> m
= foldMap id
fold
foldMap :: Monoid m => (a -> m) -> t a -> m
foldMap f = foldr (mappend . f) mempty
foldr :: (a -> b -> b) -> b -> t a -> b
foldr f z t = appEndo (foldMap (Endo #. f) t) z
foldr' :: (a -> b -> b) -> b -> t a -> b
= foldl f' id xs z0
foldr' f z0 xs where f' k x z = k $! f x z
foldl :: (b -> a -> b) -> b -> t a -> b
foldl f z t = appEndo (getDual (foldMap (Dual . Endo . flip f) t)) z
foldl' :: (b -> a -> b) -> b -> t a -> b
= foldr f' id xs z0
foldl' f z0 xs
foldr1 :: (a -> a -> a) -> t a -> a
foldr1 f xs = fromMaybe (errorWithoutStackTrace "foldr1: empty structure")
foldr mf Nothing xs)
(where
= Just (case m of
mf x m Nothing -> x
Just y -> f x y)
foldl1 :: (a -> a -> a) -> t a -> a
foldl1 f xs = fromMaybe (errorWithoutStackTrace "foldl1: empty structure")
foldl mf Nothing xs)
(where
= Just (case m of
mf m y Nothing -> y
Just x -> f x y)
toList :: t a -> [a]
{-# INLINE toList #-}
= build (\ c n -> foldr c n t)
toList t
null :: t a -> Bool
null = foldr (\_ _ -> False) True
length :: t a -> Int
length = foldl' (\c _ -> c+1) 0
elem :: Eq a => a -> t a -> Bool
elem = any . (==)
maximum :: forall a . Ord a => t a -> a
maximum = fromMaybe (errorWithoutStackTrace "maximum: empty structure") .
. foldMap (Max #. (Just :: a -> Maybe a))
getMax
minimum :: forall a . Ord a => t a -> a
minimum = fromMaybe (errorWithoutStackTrace "minimum: empty structure") .
. foldMap (Min #. (Just :: a -> Maybe a))
getMin
sum :: Num a => t a -> a
sum = getSum #. foldMap Sum
product :: Num a => t a -> a
product = getProduct #. foldMap Product
Mathématiquement, un monoïde est une structure algébrique (un ensemble et une ou plusieurs lois de composition) pourvue d'une opération binaire associative (c'est-à-dire telle que a `op` (b `op` c) = (a
`op` b) `op` c
) et d'un élément neutre (ici, `x` tel que `op a x = op x a = a`).
En Haskell, Monoid
est une classe de type définie de la façon suivante:
class Monoid a where
mempty :: a -- Élément neutre
mappend :: a -> a -> a -- Opération associative
mconcat :: [a] -> a -- Plie une liste selon le monoïde
mconcat = foldr mappend mempty
-- Data.Monoid
<> y = mappend x y -- Synonyme infixe x
Les monoïdes ont deux lois:
Loi d'identité:
<> mempty = x
x mempty <> x = x
L'application de mappend sur mempty
et une valeur quelconque x
retourne toujours x
, quel que soit l'ordre des paramètres.
(Attention, ça n'implique pas qu'un monoïde est nécessairement commutatif! Beaucoup le sont, mais String
est un monoïde avec mempty = ""
et mappend = (++)
, et ++
n'est évidemment pas commutatif.)
Loi d'associativité:
<> y) <> z = x <> (y <> z) (x
Informellement: une série d'applications de `mappend` renvoie le même résultat quel que soit l'ordre d'application.
Certains types ont plus d'un monoïde «naturel»: Int
(addition et multiplication) et Bool
(disjonction et conjonction ) par exemple. Dans ce cas, on crée généralement un synonyme newtype
pour chaque monoïde. Data.Monoid
définit ainsi deux synonymes de Int
, Sum
et Product
, et deux synonymes de Bool
, Any
et All
.
Il s'agit d'une classe de type, définie comme suit:
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
(<$) :: a -> f b -> f a
fmap id === id
: Mapper id
sur un foncteur renvoie le même foncteur. Autrement dit, fmap
ne peut pas introduire d'autres modifications dans le foncteur que l'application de la fonction.fmap (f . g) == fmap f . fmap g
La métaphore la plus répandue pour décrire un foncteur consiste à le comparer à une boîte qui contient une valeur. La métaphore est un peu courte. Plus abstraitement, un foncteur est un type de sorte * -> *
qui permet l'application d'une fonction sur les données du type encapsulée dans le foncteur. Ainsi ->
(la définition de fonction) a un foncteur. Par exemple:
= (*) 2 -- Application partielle
a = fmap (*2) a -- fmap
b 2 -- == 8 b
fmap
est une généralisation de map
, et peut donc toujours le remplacer.
Un Traversable
est un foncteur foldable qui peut être traversé «de gauche à droite».
class (Functor t, Foldable t) => Traversable t where
{-# MINIMAL traverse | sequenceA #-}
-- | Map each element of a structure to an action, evaluate these
-- actions from left to right, and collect the results. For a
-- version that ignores the results see 'Data.Foldable.traverse_'.
traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
traverse f = sequenceA . fmap f
-- | Evaluate each action in the structure from left to right, and
-- and collect the results. For a version that ignores the results
-- see 'Data.Foldable.sequenceA_'.
sequenceA :: Applicative f => t (f a) -> f (t a)
sequenceA = traverse id
-- | Map each element of a structure to a monadic action, evaluate
-- these actions from left to right, and collect the results. For
-- a version that ignores the results see 'Data.Foldable.mapM_'.
mapM :: Monad m => (a -> m b) -> t a -> m (t b)
mapM = traverse
-- | Evaluate each monadic action in the structure from left to
-- right, and collect the results. For a version that ignores the
-- results see 'Data.Foldable.sequence_'.
sequence :: Monad m => t (m a) -> m (t a)
sequence = sequenceA
Un foncteur applicatif est une structure intermédiaire entre un foncteur et une monade.
La classe de type est définie comme suit:
class Functor f => Applicative f where
-- | Lift a value.
pure :: a -> f a
-- | Sequential application.
(<*>) :: f (a -> b) -> f a -> f b
-- | Sequence actions, discarding the value of the first argument.
(*>) :: f a -> f b -> f b
*> a2 = (id <$ a1) <*> a2
a1 -- This is essentially the same as liftA2 (const id), but if the
-- Functor instance has an optimized (<$), we want to use that instead.
-- | Sequence actions, discarding the value of the second argument.
(<*) :: f a -> f b -> f a
<*) = liftA2 const (
class Applicative m => Monad m where
-- | Sequentially compose two actions, passing any value produced
-- by the first as an argument tothe second.
(>>=) :: forall a b. m a -> (a -> m b) -> m b
-- | Sequentially compose two actions, discarding any value produced
-- by the first, like sequencing operators (such as the semicolon)
-- in imperative languages.
(>>) :: forall a b. m a -> m b -> m b
>> k = m >>= \_ -> k -- See Note [Recursive bindings for Applicative/Monad]
m {-# INLINE (>>) #-}
-- | Inject a value into the monadic type.
return :: a -> m a
return = pure
-- | Fail with a message. This operation is not part of the
-- mathematical definition of a monad, but is invoked on pattern-match
-- failure in a do expression.
fail :: String -> m a
fail s = error s
@TODO Déf propre, exemples, >>, >>=
TODO
La gestion des entrées/sorties requiert un traitement spécifique dans un langage fonctionnel. Contrairement aux fonctions pures du langage, les fonctions d'E/S produisent des effets de bord, et violent le principe de transparence référentielle.
Le mécanisme d'E/S d'Haskell est implémenté sous la forme d'une monade nommée IO
.
Contrairement à ce qui se fait en général dans les bouquins sur Haskell, il vaut mieux avoir vraiment compris les types, les classes de types et les monades avant de se lancer dans l'exploration du mécanisme d'entrée/sortie.
Prelude |
h* |
Fonctions | Description |
---|---|---|---|
✓ | ✓ | getChar :: IO Char |
Lit un caractère. |
✓ | ✓ | getLine :: IO String |
Lit une ligne. |
✓ | ✓ | getContents :: IO String |
Lit le contenu d'un fichier. |
Maybe
et Either
Les extensions s'activent fichier par fichier avec le pragma {-#
LANGUAGE NameOfExtension #-}
{-# LANGUAGE EmptyDataDecls #-}
Déclarations de type sans constructeurs: data S
, data S a
. La seule valeur possible est alors \bot.
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MagicHash #-}
Autorise #
comme suffixe pour les identifiants. Généralement utilisé pour les types et les valeurs natives (unboxed): par exemple, GHC.Prim
déclare Int#
.
{-# LANGUAGE MultiParamTypeClasses #-}
Classes de type avec plusieurs arguments: class Monad m => VarMonad m
v where…
{-# LANGUAGE NegativeLiterals #-}
{-# LANGUAGE OverlappingInstances #-}
{-# LANGUAGE UnicodeSyntax #-}
Autorise à remplacer certaines séquences ASCII par des caractères Unicode: →
pour ->
, ★
pour *
, etc.
{-# LANGUAGE TypeSynonymInstances #-}
Permet de définir des instances à partir d'un synonyme type
. Ça ne crée pas de distinction entre le synonymes et le type, ni entre synonymes du même type: ça permet d'écrire:
type A = B
instance SomeClass A where -- ...
Mais cette déclaration est équivalente à
instance SomeClass B where - ...
À intégrer, en vrac:
Une information, pour préciser un point ou renvoyer vers d'autres sources.
Un avertissement généralement important.
Ces boîtes précisent un point concernant le soubassement mathématico-logique de Haskell.
Les lois sont des propriétés attendues des instances de classes de type que le compilateur ne peut pas contrôler.
Ce site est généré avec Hakyll, une librairie de génération de sites statiques écrite en Haskell.
Le thème est compilé avec Sass et utilise Gridle.
Le corps du texte est composé en Open Sans, les titres en Open Sans Condensed.
Les icônes des différentes boîtes proviennent de différentes séries compilées sur IcoMoon
Pour l'instant, ce travail est mis à disposition sous la (relativement restrictive) [[https://creativecommons.org/licenses/by-nc-sa/2.0/fr/][Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 2.0 France]].