2016-12-10 4 views
1

Ich mache einen Taschenrechner auf abstrakte ganze Zahlen und ich mache eine Menge Mustervergleich. Ich kannHaskell pattern-matching idiom

add Zero x = x 
add (P x) y = next $ add (prev $ P x) y 
add (N x) y = prev $ add (next $ N x) y 

oder

add Zero x = x 
add x y = case x of 
    P _ -> next $ add (prev x) y 
    _ -> prev $ add (next x) y 

Während der erste Weg ist kürzer, etwas in die zweite Möglichkeit spricht mich mehr schreiben.

Welches ist der bevorzugte Weg, dies zu tun?

+0

'prev' sieht aus wie' prev (N x) = x; prev x = P x' aber sollte es nicht 'add (P x) sein y = prev $ add x y'? – Gurkenglas

+0

Eigentlich ist es viel komplizierter als das; Ich habe 'Daten AbInt = Zero | P Nat | N Nat, wo 'Daten Nat = Eins | S Nat ', simuliert positiv und negativ mit' P 'und' S 'Wrapper, also' prev (N x) = N $ S x 'und so weiter. –

+1

Sie könnten die Null als natürliche Zahl mit 'data Nat = Zero | S Nat'. Dies erlaubt eine etwas undurchsichtigere, aber wesentlich einfachere Definition von "Daten Int" = Z Nat Nat, da Sie nicht mehr explizit zwischen positiven, negativen und ganzen Zahlen Null unterscheiden müssen. Zum Beispiel ist "next (Z a b) = Z (S a) b" und "prev (Z a b) = Z a (S b)". Noch besser: 'intAdd (Z a b) (Z x y) = Z (natAdd a x) (natAdd b y)' für geeignet definierte 'natAdd :: Nat -> Nat -> Nat'. – chepner

Antwort

3

Verwenden Sie as-patterns.

add Zero y = y 
add [email protected](P _) y = next $ add (prev x) y 
add [email protected](N _) y = prev $ add (next x) y 

ich auch unter Hinweis auf die gemeinsame Struktur Ihrer beiden rekursiven Zweige abstrahiert aus betrachten würde, dass Sie die Rollen der prev und next Funktionen tauschen nur davon abhängt, ob x positiv oder negativ ist:

add Zero x = x 
add x y = f $ add (g x) y 
    where (f, g) = case x of 
         P _ -> (next, prev) 
         N _ -> (prev, next) 
+0

Diese zweite Option ist sehr interessant, und etwas, das ich völlig ignoriert hatte. (Nun, ich hatte auch nicht an die erste gedacht, aber ich wusste nichts davon.) Ist die konzeptionelle Klarheit Ihrer Meinung nach die zwei zusätzlichen Zeilen wert? –

+0

@Canyon Ich denke nicht, dass es besonders wichtig ist. Die zweite Implementierung betont, dass das Hinzufügen negativer Zahlen das "Gegenteil" des Hinzufügens positiver Zahlen ist, besonders wenn Sie bessere Namen als "f" und "g" finden können. Aber die Duplizierung in der ersten Implementierung ist nicht _groß, weil es nur zwei Zeilen ist und sie sind direkt nebeneinander –

1

über diesen Stil:

add Zero x = x 
add x y = case x of 
    P _ -> next $ add (prev x) y 
    _ -> prev $ add (next x) y 

Auf der positiven Seite, vermeidet es einige Wiederholungen, das ist gut.

Auf der negativen Seite, die case scheint auf den ersten Blick nicht erschöpfend. In der Tat, um sich davon zu überzeugen, dass die Musterübereinstimmung wirklich erschöpfend ist, müssen wir über die möglichen Werte für die x in case x of diskutieren, und sehen, dass zur Laufzeit das Zero nicht sein kann, weil das oben behandelt wurde. Dies erfordert weit mehr geistige Anstrengung als das erste Snippet, das offensichtlich erschöpfend ist.

Schlimmer noch, beim Einschalten von Warnungen, wie wir es immer tun sollten, beschwert sich GHC, da es nicht davon überzeugt ist, dass die case erschöpfend ist.

Persönlich wünschte ich, die Designer von Haskell hätten nicht vollständige Spiele gänzlich verboten. Ich würde einen -Werror-on-non-exhaustive-matches verwenden, wenn es einen gab. Ich würde gerne gezwungen werden, z.B.

als die letzte Verzweigung, die vom Compiler für mich stillgelegt wird.

+0

mindestens können Sie '-Wincomplete-Muster -Werror 'verwenden – luqui

+0

@luqui Ja, das stimmt. Ich kompiliere jedoch normalerweise mit "-Wall", und ich finde "-Wall -Werror" zu übertrieben. – chi

1

Betrachten wir die math-style definition of integers als Kongruenzklassen von Paaren von naturals unter der Äquivalenzbeziehung unter Verwendung von:

{((a,b), (c,d)) | b+c == d+a} 

Die Intuition ist, dass das Paar von naturals (a,b)b-a darstellt. Wie im Wikipedia-Artikel erwähnt, reduziert dies oft die Anzahl der Sonderfälle im Vergleich zur "0/positiv/negativ" -Definition. Insbesondere die Additionsoperation Sie über die Implementierung bitten wird zu einem Einzeiler:

-- both Int and Integer are taken 
data Int' = Int Nat Nat 

instance Num Int' where 
    -- b-a + d-c = (b+d)-(a+c) 
    Int a b + Int c d = Int (a + c) (b + d) 

Es ist eine Art Spaß durch die verschiedenen Operationen mit dieser Darstellung zu arbeiten.Zum Beispiel Eq kann mit der oben angegebenen Gleichung implementiert werden und Ord ist ähnlich:

instance Eq Int' where 
    -- b-a == d-c = b+c == d+a 
    Int a b == Int c d = b+c == d+a 

instance Ord Int' where 
    -- compare (b-a) (d-c) = compare (b+c) (d+a) 
    compare (Int a b) (Int c d) = compare (b+c) (d+a) 

Gelegentlich kann es nützlich sein, diese Dinge zu normalisieren. Genau wie Brüche durch Multiplizieren von Zähler und Nenner um die gleiche Zahl reduziert werden können, bis sie relativ prim sind, können diese Dinge reduziert werden, indem die gleiche Zahl zu beiden Teilen addiert oder subtrahiert wird, bis (wenigstens) einer von ihnen Null ist.

normalize (Int (S a) (S b)) = normalize (Int a b) 
normalize v = v