Haskell-Typen frustrieren eine einfache "durchschnittliche" Funktion

76

Ich spiele mit Anfänger Haskell herum und wollte eine durchschnittliche Funktion schreiben. Es schien das Einfachste auf der Welt zu sein, oder?

Falsch.

Es scheint, als ob das Typensystem von Haskell es dem Durchschnitt verbietet, an einem generischen numerischen Typ zu arbeiten - ich kann ihn dazu bringen, an einer Liste von Integralen oder einer Liste von Brüchen zu arbeiten, aber nicht an beiden.

Ich will:

average :: (Num a, Fractional b) => [a] -> b
average xs = ...

Aber ich kann nur bekommen:

averageInt :: (Integral a, Fractional b) => [a] -> b
averageInt xs = fromIntegral (sum xs) / fromIntegral (length xs)

oder

averageFrac :: (Fractional a) => [a] -> a
averageFrac xs = sum xs / fromIntegral (length xs)

und der zweite scheint zu funktionieren. Bis ich versuche, eine Variable zu übergeben.

*Main> averageFrac [1,2,3]
2.0
*Main> let x = [1,2,3]
*Main> :t x
x :: [Integer]
*Main> averageFrac x

<interactive>:1:0:
    No instance for (Fractional Integer)
      arising from a use of `averageFrac ' at <interactive>:1:0-8
    Possible fix: add an instance declaration for (Fractional Integer)
    In the expression: average x
    In the definition of `it': it = averageFrac x

Anscheinend ist Haskell sehr wählerisch in Bezug auf seine Typen. Das macht Sinn. Aber nicht, wenn sie beide [Num] sein könnten

Vermisse ich eine offensichtliche Anwendung von RealFrac?

Gibt es eine Möglichkeit, Integrale in Brüche zu zwingen, die nicht ersticken, wenn sie eine Bruchteileingabe erhalten?

Gibt es eine Möglichkeit, eine polymorphe Durchschnittsfunktion zu verwenden Eitherund eitherzu erstellen, die für jede Art von numerischem Array funktioniert?

Verbietet das Typensystem von Haskell, dass diese Funktion jemals existiert?

Haskell zu lernen ist wie Kalkül zu lernen. Es ist wirklich kompliziert und basiert auf Bergen der Theorie, und manchmal ist das Problem so verblüffend komplex, dass ich nicht einmal genug weiß, um die Frage richtig zu formulieren, sodass jede Einsicht sehr akzeptiert wird.

(Auch Fußnote: Dies basiert auf einem Hausaufgabenproblem. Alle sind sich einig, dass durchschnittliches Frac oben volle Punkte erhält, aber ich habe den schleichenden Verdacht, dass es eine Möglichkeit gibt, es sowohl auf Integral- als auch auf Fractional-Arrays zum Laufen zu bringen.)

Jakebman
quelle

Antworten:

105

Grundsätzlich sind Sie also durch die Art von (/) eingeschränkt:

(/) :: (Fractional a) => a -> a -> a

Übrigens möchten Sie auch Data.List.genericLength

genericLength :: (Num i) => [b] -> i

Wie wäre es also mit dem Entfernen von Integral für etwas Allgemeineres:

import Data.List

average xs = realToFrac (sum xs) / genericLength xs

das hat nur eine echte Einschränkung (Int, Integer, Float, Double) ...

average :: (Real a, Fractional b) => [a] -> b

Das wird also jeden Real in einen Bruchteil bringen.

Und beachten Sie alle Poster, die von den polymorphen numerischen Literalen in Haskell erfasst werden. 1 ist keine ganze Zahl, es ist eine beliebige Zahl.

Die Real-Klasse bietet nur eine Methode: die Fähigkeit, einen Wert in der Klasse Num in einen rationalen Wert umzuwandeln. Welches ist genau das, was wir hier brauchen.

Und somit,

Prelude> average ([1 .. 10] :: [Double])
5.5
Prelude> average ([1 .. 10] :: [Int])
5.5
Prelude> average ([1 .. 10] :: [Float])
5.5
Prelude> average ([1 .. 10] :: [Data.Word.Word8])
5.5
Don Stewart
quelle
aber was ist, wenn ich auf einer Liste von beispielsweise Doppel als Durchschnitt bezeichnen möchte? Gibt es eine Funktion wie numToFrac, die entweder einen Real oder einen Bruch nimmt und einen Bruch zurückgibt? Könnten wir einen schreiben?
Jakebman
5
Sie können ihm eine Liste von Doubles geben, da Double in Real ist. "Durchschnitt ([1 .. 10] :: [Double])". Die Real-Klasse fügt genau die Fähigkeit hinzu, aus Dingen in Num einen rationalen Wert zu konstruieren. Genau das brauchen Sie.
Don Stewart
Du hast recht! Vielen Dank für die Klarstellung! Gibt es unter Num Typen, bei denen realToFrac nicht funktioniert? Ich kann nicht verstehen, warum es nicht numToFrac ist.
Jakebman
4
Es ist nicht möglich, numToFrac zu schreiben, da Num keine Konvertierungsfunktionen bietet. Real ist das Nächste, was wir haben (Num-Typen, die in ein Rational konvertiert werden können) oder Integral (Num-Typen, die in unbegrenzte Ganzzahlen konvertiert werden können).
Don Stewart
Vielen Dank! Ich wurde durch das Diagramm auf der letzten Seite von cs.ut.ee/~varmo/MFP2004/PreludeTour.pdf irregeführt , das zeigt, dass Floating KEINE Eigenschaften von Real erbt, und ich ging dann davon aus, dass sie keine gemeinsamen Typen haben würden. Zumindest kann ich aus meinen Fehlern lernen.
Jakebman
25

Die Frage wurde von Dons sehr gut beantwortet, ich dachte, ich könnte etwas hinzufügen.

Wenn Sie den Durchschnitt folgendermaßen berechnen:

average xs = realToFrac (sum xs) / genericLength xs

Ihr Code durchläuft die Liste zweimal, einmal, um die Summe seiner Elemente zu berechnen, und einmal, um ihre Länge zu ermitteln. Soweit ich weiß, ist GHC noch nicht in der Lage, dies zu optimieren und sowohl die Summe als auch die Länge in einem einzigen Durchgang zu berechnen.

Es tut selbst Anfängern nicht weh, darüber und über mögliche Lösungen nachzudenken. Beispielsweise könnte die Durchschnittsfunktion mit einer Falte geschrieben werden, die sowohl die Summe als auch die Länge berechnet. auf ghci:

:set -XBangPatterns

import Data.List

let avg l=let (t,n) = foldl' (\(!b,!c) a -> (a+b,c+1)) (0,0) l in realToFrac(t)/realToFrac(n)

avg ([1,2,3,4]::[Int])
2.5
avg ([1,2,3,4]::[Double])
2.5

Die Funktion sieht nicht so elegant aus, aber die Leistung ist besser.

Weitere Informationen auf Dons Blog:

http://donsbot.wordpress.com/2008/06/04/haskell-as-fast-as-c-working-at-a-high-altitude-for-low-level-performance/

David V.
quelle
4
+1 für den Vorschlag einer Falte, um eine bessere Leistungssteigerung zu erzielen, aber ich würde nur empfehlen, die Leistung zu versuchen, wenn Sie Haskell vollständig verstanden haben und der Polymorphismus von ganzen Zahlen für Sie ein Kinderspiel ist.
Robert Massaioli
11
+1 für Robert Massaioli Kommentar, da dieser Durchschnitt in der Tat sehr schlecht ist ... foldl 'ist streng im Akkumulator, was für Haskell im Grunde genommen "schwache Kopfnormalform" bedeutet, streng bis zum ersten Datenkonstruktor. So garantiert beispielsweise foldl 'hier, dass der Akkumulator ausreichend ausgewertet wird, um festzustellen, ob es sich um ein Paar handelt (erster Datenkonstruktor), aber der Inhalt wird nicht ausgewertet, und sammelt daher Thunks. Die Verwendung foldl' (\(!b,!c) a -> ...eines strengen Paartyps data P a = P !a !aist in diesem Fall der Schlüssel für eine gute Leistung.
Jedai
+1 für Jedais Kommentar habe ich den Beitrag entsprechend bearbeitet.
David V.
9

Da Dons bei der Beantwortung Ihrer Frage so gute Arbeit geleistet hat, werde ich daran arbeiten, Ihre Frage zu stellen ...

Zum Beispiel in Ihrer Frage, in der Sie zuerst einen Durchschnitt für eine bestimmte Liste ermitteln und eine gute Antwort erhalten. Dann nehmen Sie genau die gleiche Liste , weisen sie einer Variablen zu und verwenden dann die Funktion Variable ..., die dann explodiert.

Was haben Sie in hier laufen ist ein Set-up in den Compiler, der DMR genannt: die D readed M onomorphic R estriction. Als Sie die Liste direkt an die Funktion übergeben haben, hat der Compiler keine Annahme darüber getroffen, um welchen Typ es sich bei den Zahlen handelt. Er hat lediglich abgeleitet, welche Typen auf der Verwendung basieren könnten, und dann einen ausgewählt, sobald das Feld nicht mehr eingegrenzt werden konnte. Es ist so etwas wie das direkte Gegenteil von Entenschreiben.

Wenn Sie die Liste einer Variablen zugewiesen haben, wurde der DMR aktiviert. Da Sie die Liste in eine Variable eingefügt haben, aber keine Hinweise darauf gegeben haben, wie Sie sie verwenden möchten, hat der DMR den Compiler dazu gebracht, einen Typ auszuwählen In diesem Fall wurde eine ausgewählt, die der Form entsprach und zu passen schien : Integer. Da Ihre Funktion in ihrer /Operation keine Ganzzahl verwenden konnte (sie benötigt einen Typ in der FractionalKlasse), macht sie genau diese Beschwerde: Es gibt keine Instanz Integerin der FractionalKlasse. Es gibt Optionen, die Sie in GHC festlegen können, damit Ihre Werte nicht in eine einzige Form ("monomorph", verstanden?) Erzwungen werden, bis dies erforderlich ist, aber es macht Fehlermeldungen etwas schwieriger, dies herauszufinden.

In einem weiteren Punkt hatten Sie eine Antwort auf die Antwort von Dons, die mir aufgefallen ist:

Ich wurde durch das Diagramm auf der letzten Seite von cs.ut.ee/~varmo/MFP2004/PreludeTour.pdf irregeführt, das zeigt, dass Floating KEINE Eigenschaften von Real erbt, und ich ging dann davon aus, dass sie keine gemeinsamen Typen haben würden.

Haskell tippt anders als Sie es gewohnt sind. Realund Floatingsind Typklassen, die eher wie Schnittstellen als wie Objektklassen funktionieren. Sie sagen Ihnen, was Sie mit einem Typ tun können, der zu dieser Klasse gehört, aber das bedeutet nicht, dass ein Typ keine anderen Dinge tun kann. Eine einzige Schnittstelle bedeutet nicht, dass eine Klasse (n OO-Stil) dies nicht kann habe andere.

Haskell zu lernen ist wie Kalkül zu lernen

Ich würde sagen, Haskell zu lernen ist wie Schwedisch zu lernen - es gibt viele kleine, einfache Dinge (Buchstaben, Zahlen), die gleich aussehen und funktionieren, aber es gibt auch Wörter, die so aussehen, als sollten sie eine Sache bedeuten, wenn sie tatsächlich etwas bedeuten sonst. Aber sobald Sie es fließend beherrschen, werden Ihre regelmäßigen Freunde erstaunt sein, wie Sie dieses seltsame Zeug ausstoßen können, das wunderschöne Schönheiten dazu bringt, erstaunliche Tricks zu machen. Seltsamerweise sind in Haskell von Anfang an viele Leute involviert, die auch Schwedisch sprechen. Vielleicht ist diese Metapher mehr als nur eine Metapher ...

BMeph
quelle
2
:m Data.List
let list = [1..10]
let average = div (sum list) (genericLength list)
average
hatte das gleiche Problem
quelle
-6

Ja, Haskells Typensystem ist sehr wählerisch. Das Problem hier ist die Art von fromIntegral:

Prelude> :t fromIntegral
fromIntegral :: (Integral a, Num b) => a -> b

fromIntegral akzeptiert nur ein Integral als eine andere Art von Num. (/) akzeptiert dagegen nur Bruchteile. Wie bringen Sie die beiden zusammen?

Nun, die Summenfunktion ist ein guter Anfang:

Prelude> :t sum
sum :: (Num a) => [a] -> a

Sum nimmt eine Liste aller Num und gibt eine Num zurück.

Ihr nächstes Problem ist die Länge der Liste. Die Länge ist ein Int:

Prelude> :t length
length :: [a] -> Int

Sie müssen dieses Int auch in eine Num konvertieren. Das macht fromIntegral.

Jetzt haben Sie eine Funktion, die eine Num zurückgibt, und eine andere Funktion, die eine Num zurückgibt. Es gibt einige Regeln für die Typwerbung von Nummern, die Sie nachschlagen können , aber im Grunde können Sie jetzt loslegen:

Prelude> let average xs = (sum xs) / (fromIntegral (length xs))
Prelude> :t average
average :: (Fractional a) => [a] -> a

Probieren wir es aus:

Prelude> average [1,2,3,4,5]
3.0
Prelude> average [1.2,3.4,5.6,7.8,9.0]
5.4
Prelude> average [1.2,3,4.5,6,7.8,9]
5.25
NUR MEINE RICHTIGE MEINUNG
quelle
11
Sie sind auch in dieselbe Falle geraten wie Michael. Numerische Überladung! 5 ist kein ganzzahliger Wert. Es ist eine beliebige Zahl. Hier wird standardmäßig ein Bruchwert verwendet - Sie können weder Int noch Integer übergeben. - wie Sie keine Instanz für (Fractional Int) bekommen
Don Stewart
Ja, mein schlechtes da. Ich habe nicht genau genug aufgepasst. Ich denke, genau das ist der Grund, warum ich in Haskell nie viel mehr als Spielzeugprogrammierung geschafft habe. Selbst als Fan von Bondage- und Disziplin-Sprachen finde ich Haskell ein bisschen brutal als Meister.
Nur meine richtige Meinung
2
Haskell ist kein B & D. Es wird schmerzhaft sein, sich ihm wie B & D zu nähern. Wenn Sie Haskell lernen möchten, müssen Sie das Typensystem beherrschen. Das ist alles dazu. Ihre Verwirrung wird verschwunden sein, wenn Sie erfahren, wie Sie Typen für Sie verwenden .
Nomen