Funzioni nascoste di Haskell [chiuso]
-
03-07-2019 - |
Domanda
Quali sono le funzioni meno conosciute ma utili del linguaggio di programmazione Haskell. (Capisco che il linguaggio stesso è meno conosciuto, ma lavoro con me. Anche le spiegazioni delle cose semplici in Haskell, come la definizione della sequenza di Fibonacci con una riga di codice, verranno annullate da me.)
- Prova a limitare le risposte al core di Haskell
- Una funzione per risposta
- Fornisci un esempio e una breve descrizione della funzione, non solo un collegamento alla documentazione
- Etichetta la funzione usando il titolo in grassetto come prima riga
Soluzione
Il mio cervello è appena esploso
Se si tenta di compilare questo codice:
{-# LANGUAGE ExistentialQuantification #-}
data Foo = forall a. Foo a
ignorefoo f = 1 where Foo a = f
Verrà visualizzato questo messaggio di errore:
$ ghc Foo.hs Foo.hs:3:22: My brain just exploded. I can't handle pattern bindings for existentially-quantified constructors. Instead, use a case-expression, or do-notation, to unpack the constructor. In the binding group for Foo a In a pattern binding: Foo a = f In the definition of `ignorefoo': ignorefoo f = 1 where Foo a = f
Altri suggerimenti
Strutture di controllo definite dall'utente
Haskell non ha un operatore sternale stenografico. Il if
- then
- else
incorporato è sempre ternario ed è un'espressione (le lingue imperative tendono ad avere ?:
= espressione, if
= istruzione). Se vuoi, però,
True ? x = const x
False ? _ = id
definirà (?)
come operatore ternario:
(a ? b $ c) == (if a then b else c)
Dovresti ricorrere a macro nella maggior parte delle altre lingue per definire i tuoi operatori logici di corto circuito, ma Haskell è un linguaggio completamente pigro, quindi funziona e basta.
-- prints "I'm alive! :)"
main = True ? putStrLn "I'm alive! :)" $ error "I'm dead :("
Hoogle
Hoogle è tuo amico. Lo ammetto, non fa parte del "core", quindi cabal install hoogle
Ora sai come " se stai cercando una funzione di ordine superiore, è già lì " ( commento dell'ephemient ). Ma come trovi quella funzione? Con hoogle!
$ hoogle "Num a => [a] -> a"
Prelude product :: Num a => [a] -> a
Prelude sum :: Num a => [a] -> a
$ hoogle "[Maybe a] -> [a]"
Data.Maybe catMaybes :: [Maybe a] -> [a]
$ hoogle "Monad m => [m a] -> m [a]"
Prelude sequence :: Monad m => [m a] -> m [a]
$ hoogle "[a] -> [b] -> (a -> b -> c) -> [c]"
Prelude zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
Il programmatore hoogle-google non è in grado di scrivere i suoi programmi su carta da solo come fa con l'aiuto del computer. Ma lui e la macchina insieme sono costretti a non essere * da non sottovalutare.
A proposito, se ti è piaciuto hoogle assicurati di dare un'occhiata hlint!
Teoremi gratuiti
Phil Wadler ci ha fatto conoscere l'idea di teorema gratuito e da allora li stiamo abusando ad Haskell.
Questi meravigliosi manufatti dei sistemi di tipo Hindley-Milner aiutano con il ragionamento equazionale usando la parametria per dirti su cosa una funzione non .
Ad esempio, ci sono due leggi che ogni istanza di Functor dovrebbe soddisfare:
- forall f g. fmap f. fmap g = fmap (f. g)
- fmap id = id
Ma il teorema gratuito ci dice che non dobbiamo preoccuparci di provare il primo, ma dato il secondo viene per 'libero' solo dalla firma del tipo!
fmap :: Functor f => (a -> b) -> f a -> f b
Devi essere un po 'attento con la pigrizia, ma questo è parzialmente coperto nel documento originale e in documento più recente sui teoremi liberi in presenza di seq
.
Abbreviazione per un'operazione di elenco comune
Sono equivalenti:
concat $ map f list
concatMap f list
list >>= f
Modifica
Poiché sono stati richiesti maggiori dettagli ...
concat :: [[a]] -> [a]
concat
prende un elenco di elenchi e li concatena in un unico elenco.
map :: (a -> b) -> [a] -> [b]
map
mappa una funzione su un elenco.
concatMap :: (a -> [b]) -> [a] -> [b]
concatMap
è equivalente a (.) concat. map
: mappa una funzione su un elenco e concatena i risultati.
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
Una Monade
ha un'operazione bind , che si chiama > > =
in Haskell (o il suo zuccherato fa
-equivalent). Elenco, noto anche come []
, è un Monad
. Se sostituiamo []
con m
in quanto sopra:
instance Monad [] where
(>>=) :: [a] -> (a -> [b]) -> [b]
return :: a -> [a]
Qual è la cosa naturale che le operazioni Monade
devono fare su un elenco? Dobbiamo soddisfare le leggi della monade,
return a >>= f == f a
ma >>= (\a -> return a) == ma
(ma >>= f) >>= g == ma >>= (\a -> f a >>= g)
Puoi verificare che queste leggi siano valide se utilizziamo l'implementazione
instance Monad [] where
(>>=) = concatMap
return = (:[])
return a >>= f == [a] >>= f == concatMap f [a] == f a
ma >>= (\a -> return a) == concatMap (\a -> [a]) ma == ma
(ma >>= f) >>= g == concatMap g (concatMap f ma) == concatMap (concatMap g . f) ma == ma >>= (\a -> f a >>= g)
Questo è, in effetti, il comportamento di Monad []
. Come dimostrazione,
double x = [x,x]
main = do
print $ map double [1,2,3]
-- [[1,1],[2,2],[3,3]]
print . concat $ map double [1,2,3]
-- [1,1,2,2,3,3]
print $ concatMap double [1,2,3]
-- [1,1,2,2,3,3]
print $ [1,2,3] >>= double
-- [1,1,2,2,3,3]
Commenti multilinea annidabili .
{- inside a comment,
{- inside another comment, -}
still commented! -}
Tipi di dati algebrici generalizzati. Ecco un interprete di esempio in cui il sistema dei tipi ti consente di coprire tutti i casi:
{-# LANGUAGE GADTs #-}
module Exp
where
data Exp a where
Num :: (Num a) => a -> Exp a
Bool :: Bool -> Exp Bool
Plus :: (Num a) => Exp a -> Exp a -> Exp a
If :: Exp Bool -> Exp a -> Exp a -> Exp a
Lt :: (Num a, Ord a) => Exp a -> Exp a -> Exp Bool
Lam :: (a -> Exp b) -> Exp (a -> b) -- higher order abstract syntax
App :: Exp (a -> b) -> Exp a -> Exp b
-- deriving (Show) -- failse
eval :: Exp a -> a
eval (Num n) = n
eval (Bool b) = b
eval (Plus e1 e2) = eval e1 + eval e2
eval (If p t f) = eval $ if eval p then t else f
eval (Lt e1 e2) = eval e1 < eval e2
eval (Lam body) = \x -> eval $ body x
eval (App f a) = eval f $ eval a
instance Eq a => Eq (Exp a) where
e1 == e2 = eval e1 == eval e2
instance Show (Exp a) where
show e = "<exp>" -- very weak show instance
instance (Num a) => Num (Exp a) where
fromInteger = Num
(+) = Plus
Pattern in associazioni di livello superiore
five :: Int
Just five = Just 5
a, b, c :: Char
[a,b,c] = "abc"
Che bello! Ti salva quella chiamata a da Just
e head
di tanto in tanto.
Layout opzionale
È possibile utilizzare parentesi graffe e punti e virgola espliciti anziché spazi bianchi (aka layout) per delimitare i blocchi.
let {
x = 40;
y = 2
} in
x + y
... o equivalentemente ...
let { x = 40; y = 2 } in x + y
... invece di ...
let x = 40
y = 2
in x + y
Poiché il layout non è richiesto, i programmi Haskell possono essere prodotti in modo diretto da altri programmi.
seq
e ($!)
valuta solo abbastanza da verificare che qualcosa non sia in fondo.
Il seguente programma stamperà solo " there " ;.
main = print "hi " `seq` print "there"
Per chi non ha familiarità con Haskell, Haskell non è rigoroso in generale, il che significa che un argomento per una funzione viene valutato solo se necessario.
Ad esempio, le seguenti stampe " ignorate " e termina con successo.
main = foo (error "explode!")
where foo _ = print "ignored"
seq
è noto per cambiare quel comportamento valutando in basso se il suo primo argomento è in basso.
Ad esempio:
main = error "first" `seq` print "impossible to print"
... o equivalentemente, senza infisso ...
main = seq (error "first") (print "impossible to print")
... esploderà con un errore al "primo". Non verrà mai stampato "impossibile stampare".
Quindi potrebbe essere un po 'sorprendente che anche se seq
sia rigoroso, non valuterà qualcosa come valutano le lingue desiderose. In particolare, non tenterà di forzare tutti gli interi positivi nel seguente programma. Al contrario, controllerà che [1 ..]
non sia in basso (che può essere trovato immediatamente), stampa "quot" fatto "ed esce.
main = [1..] `seq` print "done"
Correzione dell'operatore
Puoi usare le infix, infixl o infixr per definire gli operatori associatività e precedenza. Esempio tratto dal riferimento :
main = print (1 +++ 2 *** 3)
infixr 6 +++
infixr 7 ***,///
(+++) :: Int -> Int -> Int
a +++ b = a + 2*b
(***) :: Int -> Int -> Int
a *** b = a - 4*b
(///) :: Int -> Int -> Int
a /// b = 2*a - 3*b
Output: -19
Il numero (da 0 a 9) dopo l'infisso consente di definire la precedenza dell'operatore, essendo 9 il più forte. Infix non significa associatività, mentre infixl associa a sinistra e infixr associa a destra.
Ciò consente di definire operatori complessi per eseguire operazioni di alto livello scritte come espressioni semplici.
Nota che puoi anche usare le funzioni binarie come operatori se le posizioni tra i backtick:
main = print (a `foo` b)
foo :: Int -> Int -> Int
foo a b = a + b
E come tale, puoi anche definire la precedenza per loro:
infixr 4 `foo`
Evitare parentesi
Le funzioni (.)
e ($)
in Prelude
hanno fissaggi molto convenienti, che ti permettono di evitare le parentesi in molti punti. Sono equivalenti:
f (g (h x))
f $ g $ h x
f . g $ h x
f . g . h $ x
Anche flip
aiuta, i seguenti sono equivalenti:
map (\a -> {- some long expression -}) list
flip map list $ \a ->
{- some long expression -}
Pretty guards
Prelude
definisce altrimenti = True
, facendo leggere le condizioni di protezione complete in modo molto naturale.
fac n
| n < 1 = 1
| otherwise = n * fac (n-1)
Enumerazioni in stile C
La combinazione della corrispondenza dei pattern di livello superiore e delle sequenze aritmetiche ci offre un modo pratico per definire valori consecutivi:
foo : bar : baz : _ = [100 ..] -- foo = 100, bar = 101, baz = 102
Composizione di funzioni leggibili
Prelude
definisce (.)
come composizione di funzioni matematiche; cioè g. f
prima applica f
, quindi applica g
al risultato.
Se importi Control.Arrow
, equivalgono:
g . f
f >>> g
Control.Arrow
fornisce una istanza Arrow (- >)
, e questo è utile per le persone a cui non piace leggere l'applicazione di funzioni all'indietro.
let 5 = 6 in ...
è Haskell valido.
Elenchi infiniti
Dato che hai menzionato fibonacci, esiste un modo molto elegante di generare numeri di fibonacci da un elenco infinito come questo:
fib@(1:tfib) = 1 : 1 : [ a+b | (a,b) <- zip fib tfib ]
L'operatore @ ti consente di usare la corrispondenza dei pattern sulla struttura 1: tfib pur facendo riferimento all'intero pattern come fib.
Nota che l'elenco di comprensione entra in una ricorsione infinita, generando un elenco infinito. Tuttavia, è possibile richiedere elementi o gestirli, purché si richieda un importo limitato:
take 10 fib
Puoi anche applicare un'operazione a tutti gli elementi prima di richiederli:
take 10 (map (\x -> x+1) fib)
Questo grazie alla valutazione pigra di Haskell di parametri ed elenchi.
Specifica flessibile di importazioni ed esportazioni di moduli
L'importazione e l'esportazione sono utili.
module Foo (module Bar, blah) -- this is module Foo, export everything that Bar expored, plus blah
import qualified Some.Long.Name as Short
import Some.Long.Name (name) -- can import multiple times, with different options
import Baz hiding (blah) -- import everything from Baz, except something named 'blah'
Se stai cercando un elenco o una funzione di ordine superiore, è già lì
Ci sono così tante comodità e funzioni di ordine superiore nella libreria standard.
-- factorial can be written, using the strict HOF foldl':
fac n = Data.List.foldl' (*) 1 [1..n]
-- there's a shortcut for that:
fac n = product [1..n]
-- and it can even be written pointfree:
fac = product . enumFromTo 1
Ragionamento equazionale
Haskell, essendo puramente funzionale ti permette di leggere un segno di uguale come un vero segno di uguale (in assenza di schemi non sovrapposti).
Ciò ti consente di sostituire le definizioni direttamente nel codice e, in termini di ottimizzazione, offre molto margine di manovra al compilatore su quando accadono cose.
Un buon esempio di questa forma di ragionamento può essere trovato qui:
http://www.haskell.org/pipermail /haskell-cafe/2009-March/058603.html
Ciò si manifesta bene anche sotto forma di leggi o pragmi di REGOLE previsti per i membri validi di un'istanza, ad esempio le leggi della Monade:
- ricomincia a > > = f == f a
- m > > = return == m
- (m > > = f) > > = g == m > > = (\ x - > f x > > = g)
può spesso essere usato per semplificare il codice monadico.
Pigrizia
Pigrizia ubiquitaria significa che puoi fare cose come definire
fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
Ma ci offre anche molti più sottili vantaggi in termini di sintassi e ragionamento.
Ad esempio, a causa della severità ML deve fare i conti con restrizione di valore , ed è molto attento a tenere traccia dei binding circolari let, ma in Haskell possiamo lasciare che ogni let sia ricorsivo e non abbiamo bisogno di distinguere tra val
e fun
. Questo rimuove una grande verruca sintattica dalla lingua.
Questo indirettamente dà origine alla nostra adorabile clausola where
, perché possiamo spostare in sicurezza calcoli che possono o meno essere utilizzati dal flusso di controllo principale e lasciare che la pigrizia si occupi della condivisione dei risultati.
Possiamo sostituire (quasi) tutte quelle funzioni di stile ML che devono prendere () e restituire un valore, con solo un calcolo pigro del valore. Ci sono motivi per evitare di farlo di tanto in tanto per evitare perdite di spazio con CAFs , ma tali i casi sono rari.
Infine, consente una riduzione eta illimitata ( \ x - > f x
può essere sostituito con f). Questo rende la programmazione orientata al combinatore per cose come i combinatori parser molto più piacevoli che lavorare con costrutti simili in un linguaggio rigoroso.
Questo ti aiuta a ragionare sui programmi in stile privo di punti o a riscriverli in stile senza punti e riduce il rumore degli argomenti.
Comprensione parallela dell'elenco
(funzione GHC speciale)
fibs = 0 : 1 : [ a + b | a <- fibs | b <- tail fibs ]
Corrispondenza di modelli ottimizzata
- Modelli pigri
-
Schemi irrefutabili
let ~(Just x) = someExpression
Enumerazioni
Qualsiasi tipo che sia un'istanza di Enum può essere utilizzato in una sequenza aritmetica, non solo numeri:
alphabet :: String
alphabet = ['A' .. 'Z']
Includendo i propri tipi di dati, basta derivare da Enum per ottenere un'implementazione predefinita:
data MyEnum = A | B | C deriving(Eq, Show, Enum)
main = do
print $ [A ..] -- prints "[A,B,C]"
print $ map fromEnum [A ..] -- prints "[0,1,2]"
Monadi
Non sono così nascosti, ma sono semplicemente ovunque, anche dove non ci pensi (Liste, Forse-Tipi) ...