web-dev-qa-db-fra.com

Pourquoi Haskell est-il (parfois) appelé le "meilleur langage impératif"?

(J'espère que cette question est sur le sujet - j'ai essayé de chercher une réponse mais je n'ai pas trouvé de réponse définitive. Si cela se trouve être hors sujet ou déjà répondu, veuillez modérer/supprimer.)

Je me souviens avoir entendu/lu le commentaire à moitié plaisantant sur le fait que Haskell est le meilleur langage impératif plusieurs fois, ce qui semble bien sûr bizarre car Haskell est généralement mieux connu pour son fonctionnel Caractéristiques.

Donc ma question est, quelles qualités/caractéristiques (le cas échéant) de Haskell donnent des raisons pour justifier que Haskell soit considéré comme le meilleur langage impératif - ou est-ce en fait plus une blague?

81
hvr

Je considère cela comme une demi-vérité. Haskell a une capacité étonnante d'abstraire, et cela inclut l'abstraction sur les idées impératives. Par exemple, Haskell n'a pas de boucle while impérative intégrée, mais nous pouvons simplement l'écrire et maintenant il le fait:

while :: (Monad m) => m Bool -> m () -> m ()
while cond action = do
    c <- cond
    if c 
        then action >> while cond action
        else return ()

Ce niveau d'abstraction est difficile pour de nombreuses langues impératives. Cela peut être fait dans des langues impératives qui ont des fermetures; par exemple. Python et C #.

Mais Haskell a également la capacité (très unique) de caractériser les effets secondaires autorisés, en utilisant les classes Monad. Par exemple, si nous avons une fonction:

foo :: (MonadWriter [String] m) => m Int

Cela peut être une fonction "impérative", mais nous savons qu'elle ne peut faire que deux choses:

  • "Sortie" d'un flux de chaînes
  • retourner un Int

Il ne peut pas imprimer sur la console ou établir des connexions réseau, etc. Combiné à la capacité d'abstraction, vous pouvez écrire des fonctions qui agissent sur "tout calcul qui produit un flux", etc.

Tout dépend des capacités d'abstraction de Haskell qui en font un langage impératif très fin.

Cependant, la fausse moitié est la syntaxe. Je trouve Haskell assez verbeux et maladroit à utiliser dans un style impératif. Voici un exemple de calcul de style impératif utilisant la boucle while ci-dessus, qui trouve le dernier élément d'une liste chaînée:

lastElt :: [a] -> IO a
lastElt [] = fail "Empty list!!"
lastElt xs = do
    lst <- newIORef xs
    ret <- newIORef (head xs)
    while (not . null <$> readIORef lst) $ do
        (x:xs) <- readIORef lst
        writeIORef lst xs
        writeIORef ret x
    readIORef ret

Toutes ces ordures IORef, la double lecture, devant lier le résultat d'une lecture, fmapping (<$>) pour opérer sur le résultat d'un calcul en ligne ... c'est tout simplement très compliqué à regarder. Cela a beaucoup de sens du point de vue fonctionnel, mais les langages impératifs ont tendance à balayer la plupart de ces détails sous le tapis pour les rendre plus faciles à utiliser.

Certes, si nous utilisions un combinateur de style while différent, ce serait plus propre. Mais si vous prenez cette philosophie assez loin (en utilisant un riche ensemble de combinateurs pour vous exprimer clairement), vous arrivez à nouveau à la programmation fonctionnelle. Haskell de style impératif ne "coule" pas comme un langage impératif bien conçu, par ex. python.

En conclusion, avec un lifting syntaxique, Haskell pourrait bien être le meilleur langage impératif. Mais, par la nature des lifting, cela remplacerait quelque chose de beau et de réel en interne par quelque chose de beau et de faux en externe.

[~ # ~] modifier [~ # ~] : contraste lastElt avec ceci python translittération :

def last_elt(xs):
    assert xs, "Empty list!!"
    lst = xs
    ret = xs.head
    while lst:
        ret = lst.head
        lst = lst.tail
    return ret 

Même nombre de lignes, mais chaque ligne a un peu moins de bruit.


EDIT 2

Pour ce que ça vaut, voici à quoi ressemble un remplacement pur dans Haskell:

lastElt = return . last

C'est ça. Ou, si vous m'interdisez d'utiliser Prelude.last:

lastElt [] = fail "Unsafe lastElt called on empty list"
lastElt [x] = return x
lastElt (_:xs) = lastElt xs

Ou, si vous voulez qu'il fonctionne sur n'importe quelle structure de données Foldable et reconnaissez que vous n'avez pas réellement besoinIO to gérer les erreurs:

import Data.Foldable (Foldable, foldMap)
import Data.Monoid (Monoid(..), Last(..))

lastElt :: (Foldable t) => t a -> Maybe a
lastElt = getLast . foldMap (Last . Just)

avec Map, par exemple:

λ➔ let example = fromList [(10, "spam"), (50, "eggs"), (20, "ham")] :: Map Int String
λ➔ lastElt example
Just "eggs"

Le (.) L'opérateur est composition de la fonction .

87
luqui

Ce n'est pas une blague, et je le crois. J'essaierai de garder cela accessible à ceux qui ne connaissent pas Haskell. Haskell utilise la notation do (entre autres) pour vous permettre d'écrire du code impératif (oui, il utilise des monades, mais ne vous inquiétez pas à ce sujet). Voici quelques-uns des avantages que Haskell vous offre:

  • Création facile de sous-programmes. Disons que je veux qu'une fonction imprime une valeur sur stdout et stderr. Je peux écrire ce qui suit, définissant le sous-programme avec une courte ligne:

    do let printBoth s = putStrLn s >> hPutStrLn stderr s
       printBoth "Hello"
       -- Some other code
       printBoth "Goodbye"
    
  • Facile à passer du code. Étant donné que j'ai écrit ce qui précède, si je veux maintenant utiliser la fonction printBoth pour imprimer toute une liste de chaînes, cela se fait facilement en passant mon sous-programme à la fonction mapM_:

    mapM_ printBoth ["Hello", "World!"]
    

    Un autre exemple, bien que non impératif, est le tri. Supposons que vous souhaitiez trier les chaînes uniquement par longueur. Tu peux écrire:

    sortBy (\a b -> compare (length a) (length b)) ["aaaa", "b", "cc"]
    

    Ce qui vous donnera ["b", "cc", "aaaa"]. (Vous pouvez aussi l'écrire plus court que cela, mais tant pis pour l'instant.)

  • Code facile à réutiliser. Cette fonction mapM_ Est très utilisée et remplace les boucles for-each dans d'autres langues. Il y a aussi forever qui agit comme un certain temps (vrai), et diverses autres fonctions qui peuvent être passées du code et l'exécuter de différentes manières. Les boucles dans d'autres langages sont donc remplacées par ces fonctions de contrôle dans Haskell (qui ne sont pas spéciales - vous pouvez les définir vous-même très facilement). En général, cela rend difficile d'obtenir une condition de boucle incorrecte, tout comme les boucles for-each sont plus difficiles à se tromper que les équivalents d'itérateur à longue main (par exemple en Java), ou les boucles d'indexation de tableau (par exemple en C).

  • Reliure et non affectation. Fondamentalement, vous ne pouvez attribuer à une variable qu'une seule fois (plutôt comme une affectation statique unique). Cela supprime beaucoup de confusion sur les valeurs possibles d'une variable à un moment donné (sa valeur n'est définie que sur une seule ligne).
  • Effets secondaires contenus. Disons que je veux lire une ligne de stdin et l'écrire sur stdout après lui avoir appliqué une fonction (nous l'appellerons foo). Tu peux écrire:

    do line <- getLine
       putStrLn (foo line)
    

    Je sais immédiatement que foo n'a pas d'effets secondaires inattendus (comme la mise à jour d'une variable globale, ou la désallocation de mémoire, ou autre), car son type doit être String -> String, ce qui signifie que c'est une fonction pure ; quelle que soit la valeur que je passe, elle doit retourner le même résultat à chaque fois, sans effets secondaires. Haskell sépare bien le code à effets secondaires du code pur. Dans quelque chose comme C, ou même Java, ce n'est pas évident (cette méthode getFoo () change-t-elle d'état?.

  • Collecte des ordures. Beaucoup de langues sont récupérées ces jours-ci, mais méritent d'être mentionnées: pas de soucis d'allocation et de désallocation de mémoire.

Il y a probablement quelques autres avantages en plus, mais ce sont ceux qui me viennent à l'esprit.

22
Neil Brown

En plus de ce que d'autres ont déjà mentionné, il est parfois utile d'avoir des actions à effets secondaires de première classe. Voici un exemple idiot pour montrer l'idée:

f = sequence_ (reverse [print 1, print 2, print 3])

Cet exemple montre comment vous pouvez créer des calculs avec des effets secondaires (dans cet exemple print), puis placer les structures de données dans ou les manipuler d'autres manières, avant de les exécuter.

16
tibbe