1. Nouveau type et lentilles dans Haskell

    2016-08-20
    Source

    Définir un nouveau type dans Haskell

    On utilise data pour définir un nouveau type dans Haskell. Quelques exemples élémentaires sont donnés ci-dessous.

    > -- ##~~ define a new type ~~## --
    > 
    > -- #~ a new type with unnamed fields ~# -- 
    > data Point = Point Float Float deriving (Show)
    > -- variable of type Point:
    > let point = (Point 3 4)
    > point
    Point 3.0 4.0
    > :t point
    point :: Point
    > -- 'Point' is like a function:
    > :t Point
    Point :: Float -> Float -> Point
    > -- function acting on 'Point' variables:
    > :{
    > let squareNorm :: Point -> Float
    >     squareNorm (Point x y) = x^2+y^2
    > :}
    > squareNorm point
    25.0
    > 
    > -- #~ a new type with named fields ~# -- 
    > data NamedPoint = NamedPoint{ _x :: Float, _y :: Float } deriving (Show)
    > let point = (NamedPoint 3 4) 
    > point
    NamedPoint {_x = 3.0, _y = 4.0}
    > -- we can set the field values by their name:
    > NamedPoint { _y = 0, _x = 1 }
    NamedPoint {_x = 1.0, _y = 0.0}
    > -- get the value of a field:
    > _x point 
    3.0
    > -- update a field (this creates a new 'NamedPoint'):
    > point { _y = 5 }
    NamedPoint {_x = 3.0, _y = 5.0}
    > 
    > -- #~ field with unpredefined type ~# --
    > data Foo a = Foo{ _bar :: Int, _baz :: a } deriving (Show)
    > let w = Foo { _bar = 1, _baz = "hello" }
    > w
    Foo {_bar = 1, _baz = "hello"}
    > :t w
    w :: Foo [Char]
    > -- function acting on 'Foo String':
    > :{
    > let reverseBaz :: Foo String -> Foo String
    >     reverseBaz (Foo bar baz) = Foo { _bar = bar, _baz = reverse baz }
    > :}
    > reverseBaz w
    Foo {_bar = 1, _baz = "olleh"}
    

    La fonction reverseBaz définie dans le code ci-dessus a pour action de renverser la chaîne de caractères contenue dans le deuxième champ d’un objet de type Foo String (= Foo [Char]). Elle n’agit pas sur le premier champ, et cependant avons dû recopier ce champ :

    Foo { _bar = bar, _baz = reverse baz }

    Dans ce cas où la classe Foo ne contient que deux champs, ce n’est pas très dérangeant. Mais ce serait plus problématique s’il y avait plus de champs. Une méthode permettant d’agir sur un champ sans avoir besoin de recopier les autres est souhaitable.

    Lentilles

    Pour cela, on utilise des lentilles, de la librairie lens. Ci-dessous, des exemples de la lentille _2, la lentille qui se concentre sur la 2ème composante d’une paire :

    > -- #~ lens example: _2 ~# --
    > import Control.Lens
    > let a = ("hello", 0::Int)
    > -- to get the 2nd element:
    > view _2 a 
    0
    > -- more concisely:
    > a ^. _2
    0
    > -- to set a value to the 2nd element:
    > set _2 "world" a
    ("hello","world")
    > -- more concisely:
    > _2 .~ "world" $ a
    ("hello","world")
    > -- to apply a function 
    > over _2 (+1) $ a
    ("hello",1)
    

    Créer ses lentilles

    On crée des lentilles de la façon suivante :

    -- NewType_makeLenses.hs
    {-# LANGUAGE TemplateHaskell #-}
    import Control.Lens hiding (element)
    data Foo a = Foo{ _bar :: Int, _baz :: a } deriving (Show)
    makeLenses ''Foo
    

    Cela crée deux nouvelles lentilles, bar et baz qui, respectivement, se concentrent sur le champ bar et le champ baz. On les utilise alors comme la lentille _2.

    > -- load the file creating the lenses
    > :l NewType_makeLenses
    > 
    > -- the makeLenses function has defined new lenses:
    > :i bar
    bar :: Lens' (Foo a0) Int  -- Defined at NewType_makeLenses.hs:8:1
    > :i baz
    baz :: Lens (Foo a0) (Foo a1) a0 a1    -- Defined at NewType_makeLenses.hs:8:1
    > 
    > -- they can be used as any other lens:
    > let x = Foo { _bar = 1, _baz = "hello" }
    > x ^. bar
    1
    > baz .~ "hi" $ x 
    Foo {_bar = 1, _baz = "hi"}
    > 
    > -- the 'reverseBaz' function again: 
    > :{
    > let reverseBaz :: Foo String -> Foo String
    >     reverseBaz = over baz reverse
    > :}
    > reverseBaz x
    Foo {_bar = 1, _baz = "olleh"}
    

    Ainsi, la fonction reverseBaz a pu être définie de façon bien plus commode.