Softwerkskammer

 

Kapitel 3 - Zusammenfassung

Hier die Zusammenfassung der ersten Hälfte des dritten Kapitels von "Real World Haskell". Außerdem haben wir ein paar Compiler-Fehlermeldungen besprochen und analysiert wie sie auf den konkreten Fehler hindeuten.

Teil I

Eigene Typen

data MyType = MyConstructor Int String
              deriving(Show)
  • Mit dem Schlüsselwort 'data' wird ein neuer Typ definiert
  • Dem Typ wird eine Constructor-Funktion übergeben
  • Dieser Konstruktor erwartet alle Parameter des Typs in der 'richtigen Reihenfolge'
  • deriving wurde noch nicht erklärt.
ghci>MyConstructor 1 "me"

ghci>:type it
it :: MyType

TypeSynonyms

type MyId = Int
  • Erlaubt die bessere Lesbarkeit von Code

Algebraic Types

data MyType = MyFirstConstructor | MySecondConstructor
  • Algebraic Types erlauben es unterschiedliche Konstruktoren
    für einen Typen zu definieren. Im Gegensatz zur Konstruktor-Überladung
    von klassischen Sprachen her, können die einzelnen Konstruktoren
    später noch unterschieden werden.
  • Mit Algebraic Types lassen sich außerdem Enum-Ähnliche Typen leicht erstellen.

PatternMatching

Um aus einem vordefinierten Datentyp oder Tupel einzelne Werte zu extrahieren
kann mit einer entsprechenden Funktionsdefinition der Typus 'dekonstruiert' werden:

getID :: MyType -> Int
getID (MyConstructor id _) = id

getName :: MyType -> String
getName (MyConstructor _ name) = name

Da das Wunschdatum von seiner Position im Datentyp abhängt, müssen die Variablen
entsprechend der Reihenfolge im Constructor definiert werden. Sollten bestimmte
Variablen nicht interessieren, kann der Platzhalter _ auch mehrfach benutzt werden,
um Variablen zu ignorieren.

Mit dem : lassen sich Listen im Funktionsaufruf dekonstruieren:

mySum [] = 0
mySum (x:xs) = x + mySum xs

Record Syntax

Die Record Syntax ist eine convienience-Schreibweise, mit der neben der
Typen-Deklaration auch automatisch die Getter der einzelnen Typ-Komponenten erstellt werden
können:

data MyOtherType = MyOtherType {
     myId :: Integer
   , name :: String
   } deriving (Show)

ghci> name (MyOtherType 5 "my name")
"my name"

Hier sei erwähnt, dass die Namensräume für Typen und für Funktionen getrennt sind, so dass es nicht nur kein Problem darstellt, den Konstruktor zu nennen wie den Typen, es ist obendrein auch gängige Praxis in der Haskell-Welt.

Parametrized Types

Parametrisierte Typen legen ihre Parameter nicht auf einen Basistypen fest sondern können
ähnlich wie Listen und Tupel unterschiedliche Datentypen aufnehmen.

Was will uns der

Compiler damit sagen? I

mySum :: Num a => [a] -> a
mySum [] = 0
mySum (n:ns) = n + mySum

ghci> :load mySum.hs
[1 of 1] Compiling Main             ( mySum.hs, interpreted )

mySum.hs:13:20:
    Could not deduce (a ~ ([Integer] -> Integer))
    from the context (Num a)
      bound by the type signature for mySum :: Num a => [a] -> a
      at mySum.hs:11:10-26
      `a' is a rigid type variable bound by
          the type signature for mySum :: Num a => [a] -> a
          at myLength.hs:11:10
    In the second argument of `(+)', namely `mySum'
    In the expression: n + mySum
    In an equation for `mySum': mySum (n : ns) = n + mySum
Failed, modules loaded: none.
ghci>

Hier war der Fehler, dass mySum rekursiv ohne ns, also ohne Liste aufgerufen wurde. Die entscheidenden Zeilen aus der Kompiler-Fehlermeldung weisen darauf hin, dass der zweite Parameter der Funktion (+)
den Typen einer Funktion aufweist und nicht den eines Nums, wie es erwartet wird:

  `a' is a rigid type variable bound by
          the type signature for mySum :: Num a => [a] -> a
          at myLength.hs:11:10
    In the second argument of `(+)', namely `mySum'

Was will uns der

Compiler damit sagen? II

myMean :: Num a => [a] -> Double
myMean ns = fromIntegral(sum(ns))/fromIntegral(length(ns))

ghci> :load myMean.hs
[1 of 1] Compiling Main             ( myMean.hs, interpreted )

myMean.hs:19:13:
    Could not deduce (Integral a) arising from a use of `fromIntegral'
    from the context (Num a)
      bound by the type signature for myMean :: Num a => [a] -> Double
      at myMean.hs:18:11-32
    Possible fix:
      add (Integral a) to the context of
        the type signature for myMean :: Num a => [a] -> Double
    In the first argument of `(/)', namely `fromIntegral (sum (ns))'
    In the expression:
      fromIntegral (sum (ns)) / fromIntegral (length (ns))
    In an equation for `myMean':
        myMean ns = fromIntegral (sum (ns)) / fromIntegral (length (ns))
Failed, modules loaded: none.
ghci>

Die Fehlermeldung besagt lediglich, dass im ersten Parameter der (/)-Funktion fromIntegral mit einem nicht Integral-Typen aufgerufen wird. Der eigentliche Fehler ist aber etwas versteckter, da die Funktion ohne Typen-Deklaration funktionierte
aber dann folgende Typensignatur inferiert wurde:

myMean :: (Fractional a, Integral a1) => [a1] -> a

Wie kommt diese Signatur zu Stande, wo doch für sum eine Liste von Num erwartet wird:

ghci> :t sum
sum :: Num a => [a] -> a

und für length der eigentliche Typus der Liste überhaupt keine Rolle spielt:

*Main> :t length
length :: [a] -> Int

? Hier wird fromIntegral als "Casting"-Funktion eingesetzt um aus einem Integral-Type ein
Num zu machen. In herkömlichen Sprachen würde man erwarten, dass eine Casting-Funktion irgend
einen Typ in den Typ konvertiert nach dem sie benannt ist. In Haskell fordert fromIntegral, dass
ihm ein Integral übergeben wird. Das bedeutet, dass fromIntegral(sum xs) das eher ungezwungenere sum
auffordert ihm Integral zu liefern und schränkt dadurch den Typenbereich den sum sonst überdecken würde ein.

Die richtige Lösung lautet daher, fromIntegral für den erste Operanden von (/) nicht zu verwenden:

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

So kann man mit Fractionals arbeiten, die diverse Literale erlauben:

ghci> myMean [1,2,3]
2.0
ghci> myMean [1.2,2.3,3.4]
2.3
ghci> :module Data.Ratio
ghci> myMean [1%2, 3%4, 5%4]
5 % 6