Warum wird Haskell (manchmal) als "Beste Imperative Sprache" bezeichnet?

83

(Ich hoffe, diese Frage ist themenbezogen. Ich habe versucht, nach einer Antwort zu suchen, aber keine endgültige Antwort gefunden. Wenn diese nicht zum Thema gehört oder bereits beantwortet wurde, moderieren / entfernen Sie sie bitte.)

Ich erinnere mich, dass ich den halb scherzhaften Kommentar über Haskell als beste imperative Sprache ein paar Mal gehört / gelesen habe , was natürlich seltsam klingt, da Haskell normalerweise am besten für seine funktionalen Merkmale bekannt ist.

Meine Frage ist also, welche Eigenschaften / Merkmale (wenn überhaupt) von Haskell Anlass geben, zu rechtfertigen, dass Haskell als die beste imperative Sprache angesehen wird - oder ist es eigentlich eher ein Witz?

hvr
quelle
3
donsbot.wordpress.com/2007/03/10/… <- Programmierbares Semikolon.
Vivian
10
Dieses Zitat stammt wahrscheinlich aus dem Ende der Einführung in die Bekämpfung des umständlichen Teams: monadische Eingabe / Ausgabe, Parallelität, Ausnahmen und fremdsprachige Aufrufe in Haskell, in der es heißt: "Kurz gesagt, Haskell ist die weltweit beste zwingende Programmiersprache."
Russell O'Connor
@Russel: Danke, dass Sie auf den wahrscheinlichsten Ursprung (wie es scheint, SPJ selbst zu sein) dieses Sprichworts hingewiesen haben!
hvr
Mit Haskell können Sie streng zwingende OO durchführen: ekmett / structs
Janus Troelsen

Antworten:

89

Ich halte es für eine halbe Wahrheit. Haskell hat eine erstaunliche Fähigkeit zu abstrahieren, und dazu gehört auch die Abstraktion über imperative Ideen. Zum Beispiel hat Haskell keine eingebaute imperative while-Schleife, aber wir können sie einfach schreiben und jetzt tut es:

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

Diese Abstraktionsebene ist für viele imperative Sprachen schwierig. Dies kann in imperativen Sprachen erfolgen, die Verschlüsse haben; z.B. Python und C #.

Haskell hat aber auch die (höchst einzigartige) Fähigkeit, zulässige Nebenwirkungen zu charakterisieren mithilfe der Monad-Klassen . Zum Beispiel, wenn wir eine Funktion haben:

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

Dies kann eine "zwingende" Funktion sein, aber wir wissen, dass sie nur zwei Dinge tun kann:

  • Einen Strom von Strings "ausgeben"
  • Rückgabe eines Int

Es kann nicht auf der Konsole gedruckt oder Netzwerkverbindungen usw. hergestellt werden. In Kombination mit der Abstraktionsfunktion können Sie Funktionen schreiben, die auf "jede Berechnung, die einen Stream erzeugt" usw. wirken.

Es geht wirklich nur um Haskells Abstraktionsfähigkeiten, die es zu einer sehr guten imperativen Sprache machen.

Die falsche Hälfte ist jedoch die Syntax. Ich finde Haskell ziemlich ausführlich und umständlich in einem imperativen Stil zu verwenden. Hier ist ein Beispiel für eine Berechnung im imperativen Stil unter Verwendung der obigen whileSchleife, die das letzte Element einer verknüpften Liste findet:

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

All dieser IORef-Müll, das doppelte Lesen, das Ergebnis eines Lesevorgangs binden zu müssen, fmapping ( <$>), um das Ergebnis einer Inline-Berechnung zu verarbeiten ... alles sieht nur sehr kompliziert aus. Es macht sehr viel Sinn von einem funktionalen Sicht ist dies , aber zwingende Sprachen neigen dazu, die meisten dieser Details unter den Teppich zu kehren, um ihre Verwendung zu vereinfachen.

Zugegeben, vielleicht, wenn wir einen anderen verwendet haben while wäre es sauberer Kombinator Stils verwenden würden. Wenn Sie diese Philosophie jedoch weit genug bringen (indem Sie eine Vielzahl von Kombinatoren verwenden, um sich klar auszudrücken), gelangen Sie wieder zur funktionalen Programmierung. Haskell im imperativen Stil "fließt" einfach nicht wie eine gut gestaltete imperative Sprache, z. B. Python.

Zusammenfassend könnte Haskell mit einem syntaktischen Facelifting die beste imperative Sprache sein. Aber aufgrund der Natur von Facelifts würde es etwas innerlich Schönes und Reales durch etwas äußerlich Schönes und Falsches ersetzen.

EDIT : Kontrast lastEltzu dieser Python-Transliteration:

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

Gleiche Anzahl von Zeilen, aber jede Zeile hat einiges weniger Rauschen.


BEARBEITEN 2

So sieht ein reiner Ersatz in Haskell aus:

lastElt = return . last

Das ist es. Oder wenn Sie mir verbieten, Folgendes zu verwenden Prelude.last:

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

Oder, wenn Sie es an die Arbeit auf jedem wollen FoldableDatenstruktur und erkennen , dass Sie nicht wirklich brauchen IO , um Griff - Fehler:

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

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

mit Mapzum Beispiel:

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

Der (.)Operator ist die Funktionszusammensetzung .

luqui
quelle
2
Sie können das IORef-Rauschen viel weniger störend machen, indem Sie mehr Abstraktionen vornehmen.
August
1
@augustss, hmm, darauf bin ich neugierig. Meinen Sie mehr Abstraktionen auf Domänenebene oder nur durch den Aufbau einer reichhaltigeren imperativen Subsprache? "Für die erstere stimme ich zu - aber mein Verstand assoziiert imperative Programmierung mit geringer Abstraktion (meine Arbeitshypothese lautet, dass mit zunehmender Abstraktion der Stil zunimmt konvergiert auf funktionale). Für letztere wäre ich wirklich interessiert zu sehen, was Sie meinen, weil ich mir nicht
vorstellen
2
@luqui Die Verwendung von ST wäre ein gutes Beispiel für die Charakterisierung zulässiger Nebenwirkungen. Als Bonus ist es möglich, von ST aus in eine reine Berechnung zurückzukehren.
Fuz
4
Die Verwendung von Python als Vergleich ist nicht ganz fair - wie Sie sagen, es ist gut gestaltet, eine der syntaktisch saubersten Imperativsprachen, mit denen ich vertraut bin. Der gleiche Vergleich würde argumentieren, dass die meisten imperativen Sprachen in einem imperativen Stil umständlich zu verwenden sind ... aber vielleicht haben Sie genau das gemeint. ;]
CA McCann
5
Eine Fußnote zum Gespräch für die Nachwelt: @augustss verwendet Ad-hoc-Polymorphismus, um IORefs implizit zu machen , oder versucht zumindest, Änderungen an GHC zu verhindern. : [
CA McCann
22

Es ist kein Witz, und ich glaube es. Ich werde versuchen, dies für diejenigen zugänglich zu machen, die kein Haskell kennen. Haskell verwendet (unter anderem) die Do-Notation, damit Sie imperativen Code schreiben können (ja, es werden Monaden verwendet, aber machen Sie sich darüber keine Sorgen). Hier sind einige der Vorteile, die Haskell Ihnen bietet:

  • Einfache Erstellung von Unterprogrammen. Angenommen, ich möchte, dass eine Funktion einen Wert an stdout und stderr druckt. Ich kann Folgendes schreiben und das Unterprogramm mit einer kurzen Zeile definieren:

    do let printBoth s = putStrLn s >> hPutStrLn stderr s
       printBoth "Hello"
       -- Some other code
       printBoth "Goodbye"
  • Einfach Code weiterzugeben. Wenn ich nun die printBothFunktion zum Ausdrucken einer ganzen Liste von Zeichenfolgen verwenden möchte, kann ich dies einfach tun, indem ich meine Unterroutine an die mapM_Funktion übergebe:

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

    Ein anderes Beispiel, obwohl nicht zwingend erforderlich, ist das Sortieren. Angenommen, Sie möchten Zeichenfolgen ausschließlich nach Länge sortieren. Du kannst schreiben:

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

    Welches gibt Ihnen ["b", "cc", "aaaa"]. (Sie können es auch kürzer schreiben, aber im Moment ist das egal.)

  • Einfach zu verwendender Code. Diese mapM_Funktion wird häufig verwendet und ersetzt for-each-Schleifen in anderen Sprachen. Es gibt auch Funktionen, foreverdie sich wie eine Weile (true) verhalten, und verschiedene andere Funktionen, denen Code übergeben und auf unterschiedliche Weise ausgeführt werden kann. Daher werden Schleifen in anderen Sprachen in Haskell durch diese Steuerfunktionen ersetzt (die nicht besonders sind - Sie können sie sehr einfach selbst definieren). Im Allgemeinen macht es dies schwierig, die Schleifenbedingung falsch zu machen, genau wie es für jede Schleife schwieriger ist, falsch zu sein als die Langhand-Iteratoräquivalente (z. B. in Java) oder Array-Indexierungsschleifen (z. B. in C).

  • Bindung nicht Zuordnung. Grundsätzlich können Sie einer Variablen nur einmal zuweisen (ähnlich wie bei einer einzelnen statischen Zuweisung). Dies beseitigt viel Verwirrung über die möglichen Werte einer Variablen an einem bestimmten Punkt (ihr Wert wird nur in einer Zeile festgelegt).
  • Enthaltene Nebenwirkungen. Nehmen wir an, ich möchte eine Zeile aus stdin lesen und auf stdout schreiben, nachdem ich eine Funktion darauf angewendet habe (wir nennen sie foo). Du kannst schreiben:

    do line <- getLine
       putStrLn (foo line)

    Ich weiß sofort, dass foodies keine unerwarteten Nebenwirkungen hat (wie das Aktualisieren einer globalen Variablen oder das Freigeben von Speicher oder was auch immer), da der Typ String -> String sein muss, was bedeutet, dass es sich um eine reine Funktion handelt. Unabhängig davon, welchen Wert ich übergebe, muss jedes Mal das gleiche Ergebnis ohne Nebenwirkungen zurückgegeben werden. Haskell trennt den Nebeneffektcode gut vom reinen Code. In so etwas wie C oder sogar Java ist dies nicht offensichtlich (ändert diese getFoo () -Methode den Status? Sie würden nicht hoffen, aber es könnte tun ...).

  • Müllabfuhr. Heutzutage werden in vielen Sprachen Müll gesammelt, aber es lohnt sich zu erwähnen: Keine Probleme beim Zuweisen und Freigeben von Speicher.

Es gibt wahrscheinlich noch ein paar weitere Vorteile, aber diese kommen mir in den Sinn.

Neil Brown
quelle
9
Ich würde in der starken Typensicherheit hinzufügen. Mit Haskell kann der Compiler eine große Klasse von Fehlern beseitigen. Nachdem ich kürzlich an Java-Code gearbeitet hatte, wurde ich daran erinnert, wie schrecklich Nullzeiger sind und wie viel OOP ohne Summentypen fehlt.
Michael Snoyman
1
Vielen Dank für Ihre Ausarbeitung! Ihre erwähnten Vorteile scheinen darauf zurückzuführen zu sein, dass Haskell "imperative" Effekte als erstklassige Objekte (die somit kombinierbar sind) zusammen mit der Fähigkeit behandelt, diese Effekte in einem begrenzten Umfang "einzudämmen". Ist dies eine angemessene komprimierte Zusammenfassung?
hvr
19
@ Michael Snoyman: Aber Summentypen sind in OOP einfach! Definieren Sie einfach eine abstrakte Klasse, die die Church-Codierung Ihres Datentyps darstellt, Unterklassen für die Fälle, Schnittstellen für Klassen, die jeden Fall verarbeiten können, und übergeben Sie dann Objekte, die jede Schnittstelle unterstützen, an ein Summenobjekt, wobei Sie den Subtyp-Polymophismus für den Kontrollfluss verwenden (as Du solltest). Einfacher geht es nicht. Warum hasst du Designmuster?
CA McCann
9
@camccann Ich weiß, dass Sie Witze machen, aber genau das habe ich in meinem Projekt implementiert.
Michael Snoyman
9
@ Michael Snoyman: Dann gute Wahl! Der wahre Witz ist, dass ich beschrieben habe, was so ziemlich die beste Codierung ist, auf eine Weise, die wie ein Witz klang. Ha, ha! Lachen bis zum Galgen ...
CA McCann
16

Zusätzlich zu dem, was andere bereits erwähnt haben, ist es manchmal nützlich, Nebenwirkungen besonders erstklassig zu haben. Hier ist ein dummes Beispiel, um die Idee zu zeigen:

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

Dieses Beispiel zeigt, wie Sie Berechnungen mit Nebenwirkungen erstellen können (in diesem Beispiel print) und diese dann in Datenstrukturen einfügen oder auf andere Weise bearbeiten können, bevor Sie sie tatsächlich ausführen.

tibbe
quelle
Ich denke, der Javascript-Code, der dem entspricht, wäre : call = x => x(); sequence_ = xs => xs.forEach(call) ;print = console.log; f = () => sequence_([()=> print(1), () => print(2), () => print(3)].reverse()). Der Hauptunterschied, den ich sehe, ist, dass wir ein paar extra brauchen () =>.
Hjulle