Haskell: Llenguatge de programació

Haskell és un llenguatge de programació funcional estandarditzat de semàntica no estricta i avaluació tardana de les expressions (ang: lazy evaluation) en el moment que se'n demana el valor i pren el nom del matemàtic Haskell Curry.

Haskell: Programa Hola Món, Cercadors de lAPI, Característiques Per a altres significats, vegeu «Haskell (desambiguació)».
Infotaula de llenguatge de programacióHaskell
Haskell: Programa Hola Món, Cercadors de lAPI, Característiques
Tipusllenguatge de programació purament funcional, llenguatge de programació no estricte, llenguatge de programació modular, llenguatge interpretat, llenguatge de regles fora de joc, Llenguatge de programació compilat i llenguatge de programació Modifica el valor a Wikidata
Data de creació1990; fa 34 anys (1990)
DissenyLennart Augustsson, Warren Burton, Kevin Hammond, Paul Hudak, John Hughes, Thomas Johnsson, Simon Peyton Jones, John Launchbury, Erik Meijer, Alastair Reid (en) Tradueix i Philip Wadler Modifica el valor a Wikidata
DesenvolupadorPaul Hudak, Lennart Augustsson, John Hughes, Simon Peyton Jones, Erik Meijer i Philip Wadler Modifica el valor a Wikidata
EpònimHaskell Curry Modifica el valor a Wikidata
Paradigma de programacióprogramació funcional estandarditzat de semàntica no estricta i avaluació tardana
Darrera versió estableHaskell 2010
Majors implementacionsGHC, Hugs, NHC, JHC, Yhc, UHC
DialectesHelium, Gofer
Influenciat perClean, FP, Gofer, Hope and Hope+, Id, ISWIM, KRC, Lisp, Miranda, ML and ML Estàndard, Orwell, SASL, SISAL, Scheme
Ha influenciatAgda, Bluespec, C++11/Concepts, C#/LINQ, Cayenne, Clean, Clojure, CoffeeScript, Curry, Escher, F#, Frege, Hack, Idris, Isabelle, Java/Generics, LiveScript, Mercury, Perl 6, Python, Scala, Swift, Timber, Visual Basic 9.0
Sistema operatiumultiplataforma
Extensió dels fitxers.hs, .lhs
Etiqueta d'Stack ExchangeEtiqueta Modifica el valor a Wikidata
Pàgina webhaskell.org

Es diu que és un llenguatge funcional pur. El cert és que admet variables d'estat però permet encapsular-ne els canvis (context ST), o bé circumscriure'n els efectes col·laterals al nivell superficial (context IO).

Haskell basa el polimorfisme en el requeriment d'implementació d'interfícies pels tipus dels paràmetres. Les interfícies amb un paràmetre de tipus t defineixen una partició de l'espai dels tipus en classes segons si les implementen o no, i per això s'anomenen classes de tipus.

-- classe de tipus (la interfície) class Eq t where  (==) :: t -> t -> Bool -- iguals  (/=) :: t -> t -> Bool -- desiguals  -- implementació per defecte  x == y = not (x /= y)  x /= y = not (x == y)  -- caldrà especificar només una de les operacions en definir la implementació  data Bool = False | True -- definició del tipus Bool  -- instància (la implementació) de la classe Eq per al tipus Bool instance Eq Bool where  (==) False False = True  (==) True True = True  (==) _ _ = False  -- definició del tipus (Llista a) = Nil | Cons a (Llista a) -- els símbols '[' i ']' designen una llista data [a] = [] -- el constructor Nil es denota amb "[]"  | a : [a] -- el constructor Cons es denota amb ':' en posició infix -- * per sintaxi, si un símbol comença per ':', va infix -- * tot identificador de funció es pot posar en infix si s'envolta de cometes revesses  -- exemple d'ús: "Eq t =>" es llegeix: per aquells tipus t tals que (Eq t) ésMembre :: Eq t => t -> [t] -> Bool --  ésMembre x [] = False  ésMembre x (cap : cua) = x == cap   || x `ésMembre` cua -- notació infix amb cometes revesses 
    • les classes de tipus són com un mòdul genèric, amb el tipus com a paràmetre o índex, que defineix la signatura de les operacions on intervé el tipus indexat.

Derivació automàtica d'instàncies (implementacions d'interfícies): També podem demanar al compilador que, per a classes de tipus bàsiques, derivi una instància partint de la representació interna del tipus (clàusula deriving en la definició de tipus estructurals (clàusula data).

data Color = Verd | Blau | Lila deriving (Eq, Show) -- el compilador deriva instàncies de les classes esmentades, obtenint la posició i el nom dels valors 

Els literals no tenen tipus associat sinó que poden pertànyer a aquells tipus que implementin una determinada classe:

$ ghci -- a l'intèrpret Prelude> :t 1 1 :: Num a => a -- 1 pot assignar-se a vars. d'aquells tipus que implementin la classe Num (que correspon a l'estructura algebraica d'anell) Prelude> :t 1.5 1.5 :: Fractional a => a -- 1.5 pot assignar-se a vars. d'aquells tipus que implementin la classe Fractional (que correspon a l'estructura algebraica de cos)  -- la clàusula "default" permet l'assignació de tipus tradicional als literals, esmentant una seqüència de tipus a provar default (Int, Double) 

La sobrecàrrega de literals fa possible incorporar-los a diferents estructures:

{-# LANGUAGE OverloadedStrings, OverloadedLists #-} import Data.Semigroup((<>)) import Data.Text (Text) -- text com a vectors de UTF-16 import Data.Set (Set) import Data.Map (Map)  tiraDeText = ("abc" :: Text) <> "def"  llista = ([1..3] :: [Int]) <> [4..6] cjt = ([1..3] :: Set Int) <> [4..6] dicc = ([(1,"a"), (2, "b")] :: Map Int String) <> [(3,"c")] 

Tipus derivats: Corresponent a la derivació de classes de la P.O.O. Haskell possibilita la derivació de tipus amb la declaració newtype, heretant del tipus base les implementacions d'aquelles classes de tipus que esmentem a la clàusula deriving. Caldrà l'extensió de llenguatge GeneralizedNewtypeDeriving.

Les implementacions no tenen nom. Un tipus no pot tenir diverses implementacions d'una classe de tipus, per ex. diverses ordenacions o diversos monoides per un mateix tipus. Caldrà crear-ne un tipus derivat amb newtype per cadascuna de les implementacions.

{-# LANGUAGE GeneralizedNewtypeDeriving #-} -- amplia les instàncies heretables del Haskell98  -- tipus derivat per implementar un Monoide per a la suma, parametritzat pel tipus base newtype Sum a = Sum { getSum :: a }  deriving (Eq, Ord, Read, Show, Bounded, Generic, Generic1, Num) -- classes de les instàncies a heretar  -- el constructor obté, del tipus base, el tipus derivat  -- l'accessor obté, del tipus derivat, el tipus base  instance Num a => Monoid (Sum a) where -- per aquells `a` que implementin Num (un anell algebraic)  mempty = Sum 0 -- element neutre del Monoide  Sum x `mappend` Sum y = Sum (x + y) -- operació associativa 

Les col·leccions heterogènies d'altres llenguatges, aquí es tipifiquen per les operacions requerides, mitjançant tipus existencials (aquells tipus quins components implementin les classes de tipus requerides per al seu tractament).

{-# LANGUAGE ExistentialQuantification #-}  llistaHomo :: [Int] llistaHomo = [1, 3, 4] -- llista homogènia  -- tipus "existencial": amb un component de tipus variable que compleixi una restricció data ObjPresentable = forall a. (Show a) => Obj a -- per aquells tipus 'a' tals que "(Show a)"  presentaObj :: ObjPresentable -> String presentaObj (Obj x) = "Obj " ++ show x   -- llista d'elements amb components heterogenis,  -- als quals podrem aplicar les operacions de les classes especificades a la restricció  llistaHetero :: [ObjPresentable] llistaHetero = [Obj 1, Obj "abc"] 

El control de les operacions sobre els elements dels contenidors es pot fer de diverses maneres:

    • Mapeig: Si el contenidor implementa un Functor podrem estalviar múltiples recorreguts dels contenidors en aplicar diversos morfismes als elements, perquè per les lleis del Functor l'aplicació de la composició serà equivalent a la composició de les aplicacions individuals de cadascun dels morfismes. Els conjunts no són Functor
    Si no hi ha efectes laterals
    • Els catamorfismes o plegaments són operacions que redueixen una col·lecció de valors a un de sol, altrament dit, una gramàtica A* -> A, a la classe Data.Foldable.
    • Inversament, els anamorfismes son operacions de desplegament que partint d'un valor anomenat llavor genera una col·lecció A -> A*. No tenen una classe específica. Es descriuen als mòduls de cada col·lecció.
    • Un Hylomorfisme és l'aplicació consecutiva d'un anamorfisme seguida d'un catamorfisme. Per exemple el càlcul del factorial desplega una seqüència de crides que és plegada en un valor final.
    • Un Metamorfisme és l'aplicació consecutiva d'un catamorfisme seguida d'un anamorfisme.
    • Un Paramorfisme és l'extensió del concepte de catamorfisme. Modela la recursió sobre un tipus de dades inductiu.
    • Un Apomorfisme és l'extensió del concepte d'anamorfisme, modelant la corecursió sobre un tipus coinductiu.
    Quan hi ha efectes laterals cal controlar la seqüència de les operacions
      la classe Traversable facilita l'avaluació seqüencial dels elements d'un contenidor d'accions o bé del mapeig dels elements amb una funció d'efectes.
    L'avaluació d'accions es pot fer de tres maneres possibles
  • Per combinació de resultats (les accions es podrien paral·lelitzar):
    • Els Functors aplicatius permeten combinar els resultats d'un seguit d'accions sense restringir-ne la temporalitat i componen les accions aplicant un resultat combinador de la primera, al resultat de l'acció següent. L'obtenció d'un resultat combinador es pot fer mitjançant l'aplicació d'un combinador de més d'un paràmetre, amb `fmap` a la primera acció, o també, elevant el combinador com a valor amb `pure` al tipus de l'efecte.
  • Per encadenament de resultats (serialització temporal):
    • Les Mònades componen les accions per encadenament del resultat doncs la segona acció pren forma de funció (amb efectes laterals) sobre el resultat de la precedent Hi ha una sintaxi específica (els blocs do) per descriure la composició de manera imperativa.
    • Les Fletxes generalitzen les funcions d'efectes laterals de les mònades, i componen les accions com un encadenament de resultats d'efectes funció d'una entrada que aplicarem a un valor inicial. Hi ha una sintaxi específica (els blocs proc) per descriure'n la composició.
    Mònades amb efectes laterals funcionals
    • La mònada Reader possibilita l'encapsulament d'una funció d'un entorn que podem consultar (ask) i executar localment (amb local) una acció subordinada passant-li l'entorn modificat. L'entorn l'haurem de proporcionar inicialment en executar l'acció.
    • La mònada Writer possibilita l'encapsulament d'una sortida incrementable com a Monoide que es pot consultar (listen), actualitzar (tell) i incrementar (censor).
    • La mònada State possibilita l'encapsulament d'un estat que podrem consultar (get) i actualitzar (put).
    • Els transformadors de mònades possibiliten l'encapsulament de la funcionalitat de diverses mònades.
    Programació genèrica

La programació genèrica, parametritzada per tipus, es concreta o bé

    • afegint paràmetres de tipus a les classes de tipus,
    • o bé, cas de tipus dependents, definint-los com a tipus associats a la classe
    • o bé mitjançant #Famílies de tipus (especificació a nivell global d'un tipus dependent quan es vol comú a diferents classes, per exemple el tipus de l'Element per a diferents classes de contenidors).

Haskell destaca en la facilitat per al paral·lelisme, per aprofitar la potència dels processadors multicor.

A finals dels anys 1980 es va constituir un comitè amb l'objectiu de recollir en un llenguatge les característiques dels múltiples llenguatges funcionals de l'època, Miranda, ML i altres.

La primera versió va sortir el 1990. La versió més estesa actualment és la que correspon a l'informe Haskell 98. Tanmateix el compilador GHC incorpora l'estàndard Haskell2010 per defecte a partir de la versió 7.0

A principi de 2006 va començar el procés per definir-ne una nova revisió amb el nom de Haskell' ("Haskell prima", ang:"Haskell prime"). Diversos compiladors incorporen extensions del llenguatge que per a fer-les servir caldrà precedir el codi font amb la pragma {-# LANGUAGE extensió1, extensió2, ... #-} o bé el senyal corresp. de compilació (-Xextensió per al GHC). El wiki de "Haskell Prima" esmenta per cada extensió els compiladors que la implementen.

Haskell 2010: Actualment ha finalitzat el procés de discussió de les incorporacions a la nova versió de l'estàndard, així com un nou procés d'evolució amb revisions (bi-)anuals del llenguatge.

Propostes d'evolució del llenguatge aquí.

Crítiques:

  • Haskell té un desavantatge important en la dificultat de depuració, que obliga a un esforç especial en la prevenció de fallades:
    • El model d'execució de Haskell fa que no hi hagi traça de pila de crides. Però se n'ha desenvolupat una de simulada per quan es compila per l'ajustatge (profiling). Per aquest cas, cal disposar de compilacions per l'ajustatge de totes les biblioteques relligades.
    • Les petades per crides a error de les funcions parcials (proveu head []) donen, en compilació de producció, informació molt escassa: ni situació de l'error (a partir de GHC 8.0 error ja dona la posició, excepte al paquet base on s'ha mantingut la versió antiga, reanomenada errorWithoutStackTrace), ni la de la crida que incompleix la precondició. Per això es recomana no utilitzar-les en funcions parcials i convertir-les en totals amb resultat opcional caçant el cas no previst en l'anàlisi del retorn. Recentment des de GHC 7.10.2 hi ha la possibilitat d'obtenir el punt de crida d'origen mitjançant paràmetres implícits especials (Vegeu exemple). Alternativa més segura: evitar les funcions parcials (La biblioteca Safe ofereix alternatives a les funcions parcials predefinides del Prelude; el mòdul Data.List.NonEmpty ofereix llistes no buides com a tipus NonEmpty per aplicar de manera segura les funcions que petarien sobre llistes buides, per ex.: head).
    • Els errors en funcions parcials de col·leccions no imprimeixen els valors causants, perquè la textualització dels elements (Show elem) no s'exigeix.
    • L'ús de traces per depurar es revela un maldecap per la irregularitat en l'ordre d'impressió que segueix l'ordre d'execució i no el d'especificació, indeterminat (per tant aleatori) en codi funcional pur, subjecte a l'avaluació tardana en traces en expressions let del codi monàdic (seqüencial).
    • El pas de paràmetres en les crides recursives (aval. tardana per defecte) pot fer petar la pila (pila d'avaluacions pendents) mentre no s'arriba al cas simple no-recursiu, si no s'avaluen de manera estricta els paràmetres acumuladors de manera completa, en profunditat (#Modes d'avaluació d'expressions i #Avaluació estricta explícita).
  • S'ha criticat que no hi hagi un sistema d'extensió de registres i/o especialització. A l'intèrpret Hugs se'n va desenvolupar un anomenat TRex (exemple) que GHC no ha incorporat.
    • En realitat es pot aconseguir facilitat en l'especialització de comportament, desacoblant la funcionalitat de l'estructura amb classes de propietats (getters/setters) com es descriu a l'Encapsulament estil O.O..
    • La implementació recent de polimorfisme de registres amb camps específics permet especificar els accessors com a requeriment de context, mitjançant la classe HasField (GHC 8.2), estalviant definir classes getters per aconseguir el polimorfisme (exemple verificat a #Polimorfisme de registres amb camps específics - la classe HasField).
    • Les referències funcionals, anomenades lents, permeten l'actualització funcional d'estructures complexes en els seus components com un tot, reduint la necessitat de dividir la funció en els mòduls corresponents als components afectats com faríem a la POO.

Problemàtiques:

Programa Hola Món

El GHC (Compilador Haskell de Glasgow) és el compilador / intèrpret amb més possibilitats. Instruccions per obtenir-lo, les trobareu aquí. o potser millor descarregant la Plataforma Haskell (La versió que incorpora la e./s. de caràcters no anglosaxons ja està disponible).

Per a Windows i altres podem descarregar-lo d'aquí.

  • Una altra opció és descarregar el meta-gestor stack que comprova el suport de biblioteques als rebostos #Stackage d'imatges de conjunts de versions compatibles de les biblioteques, i descarrega la versió més recent de GHC que s'hi ajusta, generant un fitxer stack.yaml amb les opcions escollides.
{-| fitxer hola-mon.hs  * si no hi ha la clàusula ''module'', es pressuposa "module Main where" -} import Data.Function ((&)) -- importa l'operador (&): aplicació cap enrere import Control.Category ((>>>)) -- importa l'op. composició d'esquerre a dreta  -- l'execució comença sempre per la funció inicial ''main'' del mòdul principal -- alternatives:  main = putStrLn "Hola Món"  main = putStrLn $ "Hola" ++ " Món" -- ($) aplicació endavant, estalvia parèntesis  main = "Hola Món" & putStrLn -- (&) aplicació cap enrere   main = ["Hola", "Món"] -- les claus designen una llista  & unwords -- words separa paraules retornant-ne la llista, unwords és la inversa  & putStrLn   main = print 5 main = putStrLn . show $ 5 -- equivalent main = show >>> putStrLn $ 5 -- equivalent main = 5 & (show >>> putStrLn) -- equivalent  -- nota: 'print' converteix a String i imprimeix, equival a (putStrLn. show)  -- show a una String li afegeix l'entrecomillat, amb 'print' una String acaba impresa amb duplicació de cometes. 

En un sistema Linux podem tenir GHC i altres compiladors de Haskell i runhaskell es pot configurar per executar una alternativa o altra amb un selector d'alternatives de sistema o bé amb la interfície gràfica galternatives (normalment apunta a runghc). A MSWindows podem fer servir indistintament runhaskell o runghc.

Execució directa, sense generació d'executable, amb runhaskell

$ runhaskell hola-mon # cerca el nom de fitxer amb l'extensió .hs (haskell source) o bé .lhs (literary haskell source)  Hola Món 

Amb el meta-gestor stack:

# stack templates -- llista plantilles # stack new projecte nom-de-plantilla stack new projecte simple cd projecte # caldrà actualitzar el fitxer Main.hs al programa Hola-món stack setup # configura, i descarrega el compilador stack build # compila i relliga l'executable stack exec projecte[.exe] 

Amb l'intèrpret GHCi del mateix paquet, comanda ghci. Amb el meta-gestor escriuríem stack ghci.

A l'intèrpret no serveix la mateixa sintaxi del nivell declaratiu dels programes, sinó només la dels blocs "do" que permet especificar seqüencialment l'encadenament de resultats d'efectes mònadics; a banda de les comandes específiques que comencen pel caràcter dos-punts i les expressions a avaluar.

Això canvia a partir de GHC 7.4.1 que admet tota mena de declaracions a l'intèrpret GHCi.

$ ghci GHCi, version 6.10.1: http://www.haskell.org/ghc/ :? for help Loading package ghc-prim ... linking ... done. Loading package integer ... linking ... done. Loading package base ... linking ... done. Prelude> putStrLn "Hola Mon" Hola Mon Prelude> let str = "abc" Prelude> :type str str :: [Char] Prelude> str "abc" Prelude> let f x y = x + y Prelude> :type f f :: Num a => a -> a -> a Prelude> 

Avaluador en-línia, a l'intèrpret d'expressions funcionals tryhaskell.org.

 reverse "Hola"  > "aloH"  :: [Char] 

Cercadors de l'API

  • Hayoo Arxivat 2010-03-22 a Wayback Machine. (servei interromput): on es podia consultar quines biblioteques exporten un identificador, a les biblioteques del rebost oficial (Hackage).
    Permet cercar operadors no alfanumèrics com ara (>>=)
    Disponible per a instal·lacions de cerca local
  • Hoogle: permet, a més a més, cerca de funcions per signatura. Exemples de cerca:
map (a -> b) -> [a] -> [b] Ord a => [a] -> [a] Data.Map.insert 
  • Hoogle5 permet alternatives de rebost (biblio estàndard, plataforma Haskell, el rebost Stackage) i cercar també en paquets específics, o bé per autor.

Característiques

Afinant el rendiment

Modes d'avaluació d'expressions

En Forma Normal els valors estan avaluats completament.

Els termes Head Normal Form i Weak Head Normal Form són normalitzacions d'expressions del càlcul lambda.

En català està explicat al llibre pdf "Introducció a la programació funcional" de la Universitat de Girona.

La definició del càlcul lambda segueix el document original d'Alonzo Church.

Terminologia del càlcul lambda (en notació Haskell):

  • E1 E2: aplicació de l'expressió E1 sobre el terme E2. L'aplicació és associativa per l'esquerra (E1 E2 E3) ≡ (E1 E2) E3
  • \ x -> E: abstracció lambda, quin cos pot contenir la variable lligada 'x' i altres de lliures. La var. lligada es refereix a l'argument de la lambda de var. coincident més pròxima que tapa el nom d'altres lambdes de nom coincident. L'abstracció lambda és associativa per la dreta.
(\ x -> \ y -> E)  (\ x -> (\ y -> E)) 

Cal tenir en compte que

(\ x -> M N)  (\ x -> (M N)) -- el cos de l'abstracció abasta el màxim cap a la dreta  (\ x y -> E)  (\ x -> \ y -> E) -- abreviació d'abstraccions 
  • redex: contracció de l'anglès "reducible expression"
  • conversió alfa: es pot canviar el nom de la variable d'una abstracció lambda mentre el nom nou no coincideixi amb el de cap variable lliure del seu cos alterant-ne la condició.
  • beta-redex: expressió reduïble mitjançant una reducció beta.
  • eta-redex: expressió reduïble mitjançant una reducció eta.

reducció beta del càlcul lambda en notació Haskell

Una expressió és beta-reduïble (beta-redex) si consisteix en l'aplicació d'una abstracció lambda (funció anònima d'una variable com a mínim) a un paràmetre. És de la forma

 (\ x -> E) P -- expressió "beta-reduïble" -- la reducció 'beta' és la substitució de la variable 'x' pel paràmetre P en l'expressió E -- el resultat ('reducte') en notació del càlcul lambda: E[x := P] 
  • En cas que hi hagi variables lligades a E que alterin la condició de variables lliures homònimes de P a la substitució E[x := P], caldrà reanomenar-les aplicant la conversió alfa del càlcul lambda.
    • La notació d'índexs de De Bruijn evita el problema de col·lisió de noms en les substitucions. En comptes de noms les variables lligades es refereixen a l'argument per un índex d'aniuament de la variable respecte a l'abstracció lambda referida.
\ x -> \ y -> \ z -> x y z -- amb notació de l'índex de De Bruijn seria \ -> \ -> \ -> v3 v2 v1 -- la variable es refereix a l'argument per l'índex d'aniuament d'abstraccions 

Una expressió beta reduïble està en posició de capçalera si és de la forma (en notació Haskell)

\ x1 x2 ... xi -> (\ y -> E) P1 P2 ... Pj -- l'expressió en capçalera és: (\ y -> E) P1 ... Pj 

La seva reducció seria

\ x1 x2 ... xi -> E[y := P1] P2 ... Pj 

reducció eta del càlcul lambda en notació Haskell

Amb notació Haskell, de la definició del càlcul lambda.

Una abstracció lambda de la forma (\ x -> E x) és eta-reduïble (substituïble per E) si

  • la variable de l'abstracció coincideix amb el paràmetre més allunyat del cos,
  • i el nom de la variable (x) no pertany a les variables lliures de E.
-- reducció 'eta': l'expressió "eta-reduïble" (\ x -> E x)  -- és substituïble per  E -- sempre que 'x' no sigui una variable lliure de E 
definició tàcita o point-free

La programació tàcita o point-free (sense arguments) estalvia arguments en les definicions aplicant la reducció eta del càlcul lambda.

-- definició arrelQuad x = sqrt x  -- en forma de lambda arrelQuad = \x -> sqrt x   -- per la reducció eta, atès que x no és una variable lliure de l'expressió (sqrt) arrelQuad = sqrt -- versió tàcita o 'point-free' (estalviant arguments) 

Head Normal Form

Una expressió lambda està en "forma normal en capçalera" (HNF) si

  1. ja no té beta-redexs (expressions beta-reduïbles) en posició de capçalera, perquè s'han aplicat totes les reduccions beta possibles en aquesta posició (Forma normal beta).
  2. l'expressió en posició de capçalera no ha d'ésser tampoc una abstracció lambda amb el cos reduïble, per què aquest cas es tipifica com a "forma normal dèbil en capçalera" (#Weak Head Normal Form).

Weak Head Normal Form

Vegeu i

Una expressió lambda amb el cos reduïble està en WHNF però no en HNF. El terme va ser encunyat per Simon Peyton Jones per explicitar la diferència entre HNF i el que la reducció de grafs fa a la pràctica: posposar la reducció del cos.

Una expressió està en WHNF si està en HNF o bé és una expressió lambda amb el cos reduïble.

Una expressió en WHNF està en una de les formes següents:

  • Un Constructor, eventualment aplicat a expressions no reduïdes dels components
  • Una funció primitiva simple (d'implementació interna, ang: built-in, que no es basa en definicions) aplicada a menys paràmetres del que correspon a la seva aritat (per ex. "(+) 1" no es pot reduir mentre no disposem de tots els paràmetres que li manquen, doncs no hi ha cap definició de (+) on substituir els paràmetres insuficients)
  • Una abstracció lambda

Exemples de situacions d'avaluació a WHNF

  • evaluate de Control.Exception avalua com a efecte una expressió funcional llançadora d'excepcions, forçant l'avaluació a WHNF.
  • L'avaluació estricta de paràmetres quan es prefixen amb (!) els paràmetres formals (extensió BangPatterns) es fa a WHNF.

Forma Normal

Una expressió està en forma normal si no s'hi poden aplicar més reduccions. Està completament avaluada (en profunditat).

Una expressió que comença per un constructor està en HNF, i això impedeix l'avaluació de les expressions dels components (avaluant amb seq). Perquè quedin avaluades, cal, o bé definir-les estrictes als tipus, o bé avaluar amb deepseq o l'equivalent $!! a "forma normal".

La classe NFData (contracció de NormalFormData) del paquet deepseq (a partir de GHC 7.4.1 forma part del conjunt bàsic de biblioteques de GHC) defineix la signatura d'una funció rnf (inicials de Reduce to Normal Form) per l'avaluació en profunditat. Per crear una instància de NFData per a un tipus algebraic, caldrà una implementació de rnf que apliqui recursivament rnf a les variables d'encaix corresponents als components dels constructors.

-- la funció ''rnf'' inicials de la traducció anglesa de "Reducció a Forma Normal" class NFData a where  rnf :: a -> () -- rnf ha d'avaluar el paràmetre en profunditat a Forma Normal 
import Control.DeepSeq  data NomDelTipus = Cons1 T1 T2 | Cons2 T3  instance NFData NomDelTipus where  rnf (Cons1 t1 t2) = (rnf t1) `seq` (rnf t2) `seq` ()  rnf (Cons2 t3) = (rnf t3) `seq` () 

Automatitzable amb el travessament genèric de les estructures algebraiques.

{-# LANGUAGE DeriveGeneric #-}  import Control.DeepSeq import Control.DeepSeq.Generics (genericRnf) import GHC.Generics  data NomDelTipus = Cons1 T1 T2 | Cons2 T3 deriving Generic  instance NFData NomDelTipus where rnf = genericRnf 

Avaluació estricta explícita

A part de l'optimització en estrictesa dels compiladors (vegeu article "Haskell és un llenguatge estricte") es pot forçar l'avaluació estricta de les següents maneres:

Aplicació estricta (Crida per valor)

Avaluació del paràmetre abans de la substitució (càlcul lambda). Amb l'operador d'aplicació estricta ($!) com a alternativa de ($) d'avaluació tardana. (seq) avalua el primer operand i retorna el segon. Al Haskell98 (seq) avaluava a HNF però al Haskell2010 avalua a WHNF (posposa la reducció de les lambdes i de les expressions dels constructors).

 f $! x = x `seq` (f x) 

Aplicació estricta a Forma Normal

($!!) amb doble signe d'exclamació, avalua l'operand en profunditat (amb deepseq) a Forma Normal, per als casos on l'operand és un tipus que implementa la classe NFData. La generació d'instàncies de NFData per a tipus algebraics es pot fer amb mecanismes de derivació genèrica (amb recorregut genèric dels tipus algebraics).

import Control.DeepSeq  -- ($!!) :: NFData a => (a -> b) -> a -> b -- infixr 0 $!!  f $!! x = x `deepseq` (f x) 

Estrictesa als components dels tipus de dades

data T = C T1 !T2 ... -- sense prefix al component => aval. tardana, amb (!) avaluació estricta 

Quan en una definició de tipus de dades es prefixa el tipus del component amb '!' l'avaluació de l'aplicació del constructor a una expressió corresponent al component es fa amb ($!) i si no hi ha el prefix '!' es fa amb ($).

Vegeu Eficiència i rendiment en tipus de dades

  • La nova extensió de llenguatge StrictData de GHC 8.0 capgira el mode d'avaluació per defecte dels components, prefixant amb el caràcter titlla aquells que volguem d'avaluació tardana. Els newtype no queden afectats.
{-# LANGUAGE StrictData #-} -- des de GHC 8.0 data T = C ~T1 T2 ... -- sense prefix al comp. => aval. estricta, amb (~) avaluació tardana 

Estrictesa als paràmetres formals

Avaluació estricta als paràmetres formals amb l'extensió {-# LANGUAGE BangPatterns #-}

{-# LANGUAGE BangPatterns #-} f !x !y = 2 * x * y -- avalua estrictament (!) els paràmetres actuals (a WHNF)  {-# LANGUAGE Strict #-} -- des de GHC 8.0 f x y = 2 * x * y -- amb Strict avaluació estricta no-irrefutable per defecte, prefix (~) per l'avaluació com abans 

Estrictesa als patrons d'encaix

Lligams amb avaluació estricta a les variables dels patrons

let !v = expr ... -- avalua estrictament l'expressió (amb (seq)) let (!x, !y) = (exprX, exprY) -- avalua estrict. les expr. de les variables del patró 
  • La nova extensió de llenguatge Strict de GHC 8.0 capgira el mode d'avaluació per defecte d'un mòdul a estricte. Per obtenir l'avaluació tardana caldrà prefixar amb el caràcter titlla (~).

foldl': iteracions amb plegat per l'esquerra estricte

l'operació foldl (foldLeft) permet fer "transformacions reiterades sobre una estructura de tipus a amb els valors d'una seqüència [b] ((a -> b -> a)), de manera equivalent als bucles for|forEach de la prog. imperativa tradicional.

 foldl :: (a -> b -> a) -> a -> [b] -> a -- essent ''a'' l'estructura i ''[b]'' la llista de valors a iterar 
-- versió tardana, cost O(n) en allotjament d'operacions pendents d'avaluar foldl op estructInicial (x:xs) = foldl op (op estructInicial x) xs  -- versió estricta, avaluació primerenca foldl' op estructInicial (x:xs) = let aplega_x = op estructInicial x  in aplega_x `seq` foldl' op aplega_x xs -- (seq) avalúa el primer operand i retorna el segon -- si el tipus de ''aplega_x'' és algebraic (conté constructors (aturen l'avaluació))) 

L'ús de tipus algebraics en l'operació, aturant l'avaluació en els constructors, implica la generació d'una pila de thunks (expressions pendents d'avaluar) que poden comportar el sobreiximent de la pila.

Treballar a espai constant de la pila

El llibre RealWorldHaskell ofereix un exemple on l'operació nucli del plegat estricte foldl' plega sobre un parell, és a dir un tipus algebraic com a acumulador (constructor del parell amb les seves expressions). Els constructors en posició de capçalera (vegeu #Head Normal Form) estan en HNF, per tant les expressions dels components no són avaluades. Això fa que s'amuntegui la seva avaluació pendent a la pila.

  • o bé s'avalua estrictament els components de l'acumulador compost dins l'operació del plegat
  • o bé es pot definir una versió de foldl'/foldr' que avaluï estrictament, però en profunditat a #Forma Normal.
import Control.DeepSeq (NFData, ($!!)) import Data.Foldable  -- de la definició de foldl' i foldr', substituint ($!) per ($!!) de DeepSeq per avaluar a Forma Normal  foldl'' :: (Foldable t, NFData b) => (b -> a -> b) -> b -> t a -> b foldl'' f z0 xs = foldr f' id xs z0  where f' x k z = k $!! f z x -- ($!) substituït per ($!!)  foldr'' :: (Foldable t, NFData b) => (a -> b -> b) -> b -> t a -> b foldr'' f z0 xs = foldl f' id xs z0  where f' k x z = k $!! f x z -- ($!) substituït per ($!!) 

Estrictesa en les crides recursives

Vegeu "Recursivitat final al Haskell".

  • La "recursivitat final mòdulo cons" (recursivitat diferida) quan la crida recursiva és el component d'una dada que es retorna, no serà avaluada perquè els constructors de Tipus de dades algebraics aturen l'avaluació HNF.
duplicaLlista :: [a] -> [a] duplicaLlista [] = [] duplicaLlista (x : xs) = x : duplicaLlista xs -- crida recursiva "mòdulo cons" 
  • En cas de recursivitat final normal, el compilador optimitza alguns casos per evitar generar una pila d'expressions pendents d'avaluar (thunks) però cal assegurar-se'n. Vegeu "Limitations of strictness analysis".

Avaluació tardana explícita en els patrons d'encaix

Els encaixos de patrons s'avaluen normalment de manera estricta (primerenca).

    » pattern matching is usually strict. So trying a pattern match forces evaluation to happen at least far enough to accept or reject the match.

Per avaluar un encaix de manera tardana (lazy) cal prefixar-lo amb el caràcter titlla '~'.

L'avaluació tardana de l'encaix (vegeu exemple tot seguit) converteix l'encaix en irrefutable (no rebutjable, no parcial) evitant l'avaluació d'altres opcions.

L'ús de l'avaluació tardana explícita amb tipus amb més d'un constructor és perillós:

f :: [a] -> [a] f ~(x:xs) = x:xs -- és traduït internament a f ys = head ys : tail ys -- compte! funcions parcials!! -- l'encaix (f ys), encaix amb variable, sempre té èxit.  -- si, per filtrar el cas [] no definit anteriorment, l'avaluem primer ... f [] = [] f ~(x:xs) = x:xs -- l'avaluació tardana aquí és estúpida perquè l'encaix  -- ja ha estat avaluat estrictament pel cas precedent []  -- si haguéssim posat el cas [] després:  f ~(x:xs) = x:xs f [] = [] -- el segon encaix (f []) no s'avalua mai, perquè l'anterior és irrefutable! -- L'ordre és rellevant!!! El compilador ens avisarà del codi inabastable. 

Programació de nivell baix

Tipus primitius que es passen per valor (no encapsulats, ang:unboxed)

Vegeu-ho a GHC

Tuples no encapsulades per al retorn de múltiples valors

Vegeu-ho a GHC

Especialització d'operacions sobrecarregades

Vegeu-ho a GHC

Referència ràpida de la biblioteca

A GHC.

Haskell com a llenguatge Script

Execució de Haskell com a Script a Unix (mitjançant una directiva Shebang)., mitjançant l'eina stack:

#!/usr/bin/env stack # directiva "Shebang" amb l'intèrpret seguit de paràmetres per a `stack script` {- stack script  --resolver lts-11.22   --package shelly -}  {-# LANGUAGE OverloadedStrings #-}  import Shelly (Sh, shelly, echo)  hola :: Sh () hola = echo "hola"  main :: IO () main = shelly $ hola 

També admet mòduls externs posicionats segons la nomenclatura jeràrquica NomCarpeta.NomDeMòdul respecte del programa script.

Eines per les Comprovacions de Qualitat

  • Biblioteca QuickCheck de comprovació de predicats sobre sèries de valors aleatoris amb acotació progressiva del domini del problema, mitjançant la instanciació de classes específiques (Arbitrary).
  • Biblioteca HSpec inspirada per la biblioteca de Ruby RSpec.
    Verificació formal estàtica
  • La biblio. LiquidHaskell facilita la verificació formal estàtica mitjançant anotacions de "tipus refinats amb predicats" (ang: Logically Qualified Data Types) i un comprovador de lògica SMT (de l'anglès "Satisfiability modulo theories") determina si el programa és una solució d'una especificació teòrica formal que s'inclou.

Problemàtiques

L'ús simultani de diferents biblioteques que utilitzin diferents versions d'una mateixa tercera biblioteca comporta:

Un cop has generat l'executable, el següent maldecap són les petades de les funcions parcials, que criden a error per als valors per als quals no estan definides, (head []), sense cap pista del culpable:

Les crides recursives i els plegats poden generar piles d'expressions pendents d'avaluar, fent petar la pila, si no s'explicita correctament l'avaluació estricta dels acumuladors i dels components de les estructures intermèdies:

La modificació de l'estat global (preferències) en temps d'execució dona lloc al:

Perquè la imatge en memòria del programa no creixi més del compte cal tenir en compte:

Gestor de projectes i empaquetatge de biblioteques i aplicacions

El Haskell utilitza un gestor de projectes per resoldre les dependències d'altres biblioteques i facilitar-ne la compilació on s'instal·len, amb independència del compilador emprat.

Aquest gestor es diu Cabal (Common Architecture for Building Applications and Libraries) i utilitza arxius de descripció de projecte amb l'extensió ".cabal". Vegeu "Com crear un paquet executable o bé biblioteca"

El nom del paquet consta de nom-versió, per ex. shakespeare-css-1.0.1.2, on la versió major són els dos primers components numèrics (un canvi en la versió major indica trencament de compatibilitat amb l'API anterior), la versió menor és el tercer (identifica la API, un canvi significa un augment de l'API sense trencament)), i el quart indica modificacions sense variar l'API.

Els paquets es descarreguen en directoris dins del $HOME/.cabal, els executables de les aplicacions quedaran a $HOME/.cabal/bin que caldrà afegir a la variable d'entorn PATH.

Els paquets ja instal·lats per GHC es gestionen amb el programa ghc-pkg.

Al Linux podem començar treballant amb les biblioteques precompilades en paquets de la distribució de Linux. Si us cal una versió més recent del compilador, caldrà instal·lar-lo prèviament i, en acabat, instal·lar #The Haskell Platform, contràriament al Windows on el paquet #The Haskell Platform l'incorpora.

El rebost oficial de biblioteques i aplicacions és el Hackage

  • instal·lació manual: Si no es disposa de l'executable cabal del paquet cabal-install, per instal·lar un paquet cal descarregar-lo, buscar el fitxer Setup.hs o bé Setup.lhs i executar
 runhaskell Setup clean # neteja arxius producte de compilacions anteriors  runhaskell Setup configure —help # mostra opcions generals i específiques del paquet  runhaskell Setup configure —user —altres-opcions  runhaskell Setup build  [sudo] runhaskell Setup install # ''sudo'' (privilegi d'admin. del sistema) si configures amb —global en comptes de —user  #, per actualitzar el rebost del sistema 
  • instal·lació automatitzada d'un paquet del rebost Hackage:

Amb "cabal" descarrega, configura, compila i instal·la d'una sola tacada, després d'instal·lar automàticament els paquets dels quals depèn. Preguntes freqüents aquí.

 # l'ajuda és per comanda  cabal help install  # --with-compiler=... permet seleccionar el compilador  #  =  {'.' } ['.' '*']  #  :== nom | nom-versió | "nom < versió" | "nom == versió"  cabal install paquet1 paquet2 --opcions 
  • per llistar els paquets que ja tenim a GHC, "ghc-pkg list". No existeix encara l'equivalent "hugs-pkg"
  • en cas de problemes per biblioteques que no compilen bé, és útil limitar-ne la versió amb l'opció—constraint=dependència
# restricció de dependències en compilar --constraint="nom_biblio < versió"  # o bé  --constraint=nom_biblio==versió # (Els 3 primers components de la versió identifiquen l'API)  # exemples (les cometes només fan falta si hi ha espais o bé # si l'operador de comparació conté símbols '<' '>' de l'intèrpret de comandes per a la redirecció): cabal install QuickCheck --constraint=BiblioDeLaQualDepen==2.3.4.* 
  • a partir de cabal-install v.1.16 cabal admet paral·lelisme en la instal·lació (opció -j)
cabal install -j "wx == 0.13.*" 

Conflictes de dependències

Tanmateix cal tenir en compte que l'ús simultani de biblioteques que facin servir diferents versions d'un mateix paquet és molt problemàtic. Cal evitar absolutament la comanda cabal upgrade que ens abocaria al problema esmentat.

Entorn aïllat de desenvolupament amb cabal sandbox

cabal amb el verb sandbox crea un dipòsit de biblioteques específic del projecte en el subdirectori amagat ".cabal-sandbox". Una sandbox (capsa de sorra literalment) és un entorn tancat per a proves per evitar que els errors que s'hi produeixin repercuteixin a l'exterior. Aquesta funcionalitat es va desenvolupar al programa cabal-dev que ara ha quedat obsolet.

  • els executables queden al directori ./.cabal-sandbox/bin que caldrà afegir a la var. d'entorn d'executables PATH.
  • s'hi poden associar subprojectes amb la comanda
    cabal sandbox add-sources camí/al/subprojecte

Per a més info. consulteu cabal sandbox --help.

  • Aquesta funcionalitat ha quedat superada pel meta-gestor stack que allotja les dependències locals (definides al projecte.cabal) en una capsa-de proves "./.stack-work" i les globals en subcarpetes d'usuari "~./stack" per versions, evitant recompilar-les per cada projecte.

El rebost Stackage - rebost alternatiu amb dependències verificades

Stackage proporciona imatges de conjunts de biblioteques amb compatibilitat verificada de versions, entre elles i una versió específica de GHC, amb noves imatges dels rebostos a mesura que sorgeixen noves versions de dependències o del compilador.

Vegeu Compilador Haskell de Glasgow#Stackage - rebost alternatiu amb dependències verificades

Creació del fitxer de projecte .cabal

    cabal init
    interroga l'usuari a la cònsola de comandes i genera un esquelet de projecte que caldrà editar posteriorment per afegir-hi els noms dels fonts i els paquets dels quals depèn. Caldrà afegir-hi manualment la llista de mòduls exportats si és una biblioteca (secció exposed-modules), dels mòduls no exportats (secció other-modules) i de les dependències (secció build-depends).
    • superat pel sistema de plantilles de stack: stack new projecte plantilla
    leksah
    Entorn gràfic d'usuari (IDE) que conté un formulari d'edició del fitxer de projecte.

Un cop creat, la comanda cabal sense nom de paquet treballa sobre el fitxer de projecte del directori de treball:

cabal sandbox init # genera un rebost de biblioteques específic del projecte  # els executables quedaran a la carpeta ".cabal-sandbox/bin" cabal sandbox add-sources camí/al/subprojecte # afegir subprojecte cabal sandbox delete # elimina el rebost específic  cabal clean # neteja tots els fitxers generats per la compilació cabal configure # estableix opcions i comprova compatibilitat de les dependències cabal build # compila cabal install # configura, compila, relliga i instal·la cabal freeze # fixa les versions de les dependències com a restricció  # al fitxer "cabal.config" de restriccions a la carpeta del projecte cabal repl # engega l'intèrpret 'ghci' havent carregat mòduls i opcions esmentades al fitxer de projecte 

Vegeu també "Com escriure (i empaquetar) un programa en Haskell". També "Actualitzant un paquet a noves versions de GHC i biblioteques".

Gestió dels paquets instal·lats

Amb ghc-pkg comprova paquets instal·lats a les BDD de paquets de l'usuari i del sistema.

$ ghc-pkg list   $ ghc-pkg check # comprova dependències trencades per la reinstal·lació d'aquestes 

En un entorn de desenvolupament aïllat en capsa-de-proves (sandbox):

cabal sandbox hc-pkg ... 

Instal·lació des del GitHub

L'aplicació cabalg descarrega (amb git-clone a subcarpeta) i instal·la (amb cabal).

cabalg https://github.com/usuari/projecte-github # descarrega a subcarpeta de nom "projecte-github" 

Stack - Meta-gestor de projectes

L'aplicació Stack sobreposa la seva operativa a la del gestor cabal millorant-la en diversos aspectes:

  • proporciona plantilles per a diversos tipus d'aplicacions
  • utilitza els rebostos Stackage de dependències de compilació verificada amb versions específiques de GHC
  • cerca automàticament el rebost Stackage més recent que contingui les dependències (biblioteques) esmentades al fitxer .cabal del projecte
  • descarrega la versió de GHC corresponent al rebost, si encara no ho ha fet
  • millora el rendiment del desenvolupament aïllat en "Sandbox" (Capsa de proves): la Sandbox (a la carpeta ./.stack-work) només guarda les biblioteques definides localment, al fitxer de projecte .cabal, mentre que les dependències es compilen un sol cop per a diversos projectes, guardant dins la carpeta d'usuari (~/.stack) les compilacions de dependències requerides segons la versió d'origen del rebost.
stack templates # llista les plantilles de fonts per a nous projectes stack new projecte