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


(Ich hoffe, dass diese Frage zum Thema ist - ich habe versucht, nach einer Antwort zu suchen, habe aber keine definitive Antwort gefunden. Wenn dies nicht am Thema oder bereits beantwortet ist, bitte moderiere / entferne es.)

Ich erinnere mich, dass ich den halb scherzhaften Kommentar über Haskell gehört oder gelesen hatte beste imperative Sprache ein paar Mal, was natürlich komisch klingt, da Haskell normalerweise am besten bekannt ist funktional Eigenschaften.

Meine Frage ist also, welche Qualitäten / Eigenschaften (falls überhaupt) von Haskell einen Grund zur Rechtfertigung von Haskell rechtfertigen beste imperative Sprache - Oder ist es eher ein Scherz?


75
2017-07-08 09:31


Ursprung


Antworten:


Ich halte es für eine Halbwahrheit. Haskell hat eine erstaunliche Fähigkeit zu abstrahieren, und dazu gehört die Abstraktion über imperative Ideen. Zum Beispiel hat Haskell keine eingebaute while-Schleife, aber wir können es 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 Imperativsprachen schwierig. Dies kann in imperativen Sprachen geschehen, die Closures haben; z.B. Python und C #.

Aber Haskell hat auch die (sehr einzigartige) Fähigkeit, charakterisieren erlaubte Nebenwirkungenmit den Monad-Klassen. Zum Beispiel, wenn wir eine Funktion haben:

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

Dies kann eine "Imperativ" -Funktion sein, aber wir wissen, dass es nur zwei Dinge tun kann:

  • "Ausgabe" ein Strom von Strings
  • einen Int

Es kann nicht auf der Konsole drucken oder Netzwerkverbindungen usw. herstellen. Kombiniert mit der Abstraktionsfähigkeit können Sie Funktionen schreiben, die auf "jede Berechnung, die einen Stream erzeugt" usw., wirken.

Es geht wirklich um Haskells Abstraktionsfähigkeiten, die es zu einer sehr schönen Imperativsprache machen.

Die falsche Hälfte ist jedoch die Syntax. Ich finde Haskell ziemlich ausführlich und ungeschickt in einem imperativen Stil zu verwenden. Hier ist eine Beispiel-Berechnung im imperativen Stil, die das obige verwendet while Schleife, 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 das IORef Müll, das doppelte Lesen, muss das Ergebnis eines Lesevorgangs fmapping (<$>) um das Ergebnis einer Inline-Berechnung zu verarbeiten ... es ist alles nur sehr kompliziert aussehend. Es macht sehr viel Sinn von a funktional Perspektive, aber Imperativ Sprachen neigen dazu, die meisten dieser Details unter den Teppich zu kehren, um sie einfacher zu bedienen.

Zugegeben, vielleicht, wenn wir ein anderes verwenden while-Style-Kombinator wäre es sauberer. Aber wenn Sie diese Philosophie weit genug nehmen (mit einem reichen Satz von Kombinatoren, um sich klar auszudrücken), dann kommen Sie wieder zur funktionalen Programmierung. Imperativ-Stil Haskell "fließt" einfach nicht wie eine gut entworfene imperative Sprache, z.B. Python.

Zusammenfassend kann man sagen, dass Haskell mit einem syntaktischen Face-Lift die beste Imperativsprache ist. Aber durch die Art von Facelifts würde es etwas, das innerlich schön und real ist, durch etwas Äußerlich Schönes und Fälschliches ersetzen.

BEARBEITEN: Kontrast lastElt mit 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 ein bisschen weniger Rauschen.


BEARBEITEN 2

Für was es wert ist, ist dies wie rein Ersatz in Haskell sieht so aus:

lastElt = return . last

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

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

Oder, wenn Sie wollen, dass es an irgendwelchen arbeitet Foldable Datenstruktur und erkennen, dass Sie nicht wirklich brauchen  IO um Fehler zu behandeln:

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

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

mit Map, beispielsweise:

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

Das (.) Betreiber ist Funktionszusammensetzung.


83
2017-07-08 09:59



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 verwendet Monaden, aber machen Sie sich darüber keine Sorgen). Hier sind einige der Vorteile, die Ihnen Haskell bietet:

  • Einfache Erstellung von Unterprogrammen Nehmen wir an, ich möchte, dass eine Funktion einen Wert an stdout und stderr ausgibt. Ich kann folgendes schreiben, indem ich das Unterprogramm mit einer kurzen Zeile definiere:

    do let printBoth s = putStrLn s >> hPutStrLn stderr s
       printBoth "Hello"
       -- Some other code
       printBoth "Goodbye"
    
  • Einfach um Code herumzugeben. Da ich das oben geschrieben habe, wenn ich jetzt das verwenden möchte printBoth Funktion, um alle eine Liste von Zeichenfolgen auszudrucken, das ist einfach durch Übergeben meiner Subroutine an die mapM_ Funktion:

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

    Ein anderes, wenn auch nicht zwingendes Beispiel ist das Sortieren. Angenommen, Sie möchten Strings nur nach Länge sortieren. Du kannst schreiben:

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

    Das gibt dir ["b", "cc", "aaaa"]. (Du kannst es auch kürzer schreiben, aber vergiss es jetzt nicht.)

  • Einfach zu Code wiederverwenden. Das mapM_ Funktion wird häufig verwendet und ersetzt for-each Loops in anderen Sprachen. Es gibt auch forever was wie eine Weile funktioniert (true), und verschiedene andere Funktionen, die Code übergeben und auf verschiedene Arten ausführen können. So werden Schleifen in anderen Sprachen durch diese Kontrollfunktionen in Haskell ersetzt (die nicht besonders sind - Sie können sie sehr einfach selbst definieren). Im Allgemeinen macht dies es schwierig, die Schleifenbedingung falsch zu machen, genau wie for-jede Schleifen sind schwerer falsch zu werden als die Lang-Hand-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 (eher wie eine einzelne statische Zuweisung). Dies beseitigt eine Menge Verwirrung über die möglichen Werte einer Variablen an einem bestimmten Punkt (ihr Wert wird nur in einer Zeile gesetzt).
  • Enthaltene Nebenwirkungen. Nehmen wir an, ich möchte eine Zeile von stdin lesen und sie auf stdout schreiben, nachdem ich eine Funktion darauf angewendet habe (wir nennen es foo). Du kannst schreiben:

    do line <- getLine
       putStrLn (foo line)
    

    Das weiß ich sofort foo hat keine unerwarteten Nebenwirkungen (wie das Aktualisieren einer globalen Variablen oder das Aufheben der Zuweisung von Speicher oder was auch immer), da der Typ String -> String sein muss, was bedeutet, dass es sich um eine reine Funktion handelt; welchen Wert ich auch immer übertrage, er muss jedes Mal das gleiche Ergebnis ohne Nebenwirkungen zurückgeben. Haskell trennt den Nebencode vom reinen Code. In etwas wie C, oder sogar Java, ist das nicht offensichtlich (ändert die getFoo () Methode den Status? Du würdest es nicht hoffen, aber es könnte ...).

  • Müllabfuhr. Eine Menge Sprachen sind Müll gesammelt in diesen Tagen, aber erwähnenswert: keine Probleme der Zuweisung und Freigabe von Speicher.

Es gibt wahrscheinlich noch ein paar weitere Vorteile, aber das sind diejenigen, die in den Sinn kommen.


22
2017-07-08 10:01



Zusätzlich zu dem, was andere bereits erwähnt haben, ist die Verwendung von Nebeneffekten manchmal von erstem Nutzen. 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 Nebeneffekten erstellen können (in diesem Beispiel) print) und dann die Datenstrukturen einfügen oder sie auf andere Weise manipulieren, bevor sie tatsächlich ausgeführt werden.


15
2017-07-08 11:20