2016-05-14 12 views
2

Von http://learnyouahaskell.com/making-our-own-types-and-typeclassesVermeiden von primitiver Besessenheit in Haskell

data Person = Person { name :: String 
        , age :: Int 
        } deriving (Show) 

In einer realen Anwendung unter Verwendung von Primitiven wie String und Int für Namen und das Alter würden primitive Besessenheit Constitué, einen Code Geruch. (Auch offensichtlich geboren Datum ist vorzuziehen, Alter Int aber lassen wir das ignorieren) Stattdessen würde man so etwas wie

newtype Person = Person { name :: Name 
         , age :: Age 
         } deriving (Show) 

In einer OO-Sprache bevorzugen diese etwas aussehen würde wie

class Person { 
    Name name; 
    Age age; 
    Person(Name name, Age age){ 
    if (name == null || age == null) 
     throw IllegalArgumentException(); 
    this.name = name; 
    this.age = age; 
    } 
} 

class Name extends String { 
    Name(String name){ 
    if (name == null || name.isEmpty() || name.length() > 100) 
     throw IllegalArgumentException(); 
    super(name); 
    } 
} 

class Age extends Integer { 
    Age(Integer age){ 
    if (age == null || age < 0) 
     throw IllegalArgumentException(); 
    super(age); 
    } 
} 

Aber wie ist das gleiche in idiomatischen, Best Practice Haskell erreicht?

Antwort

8

Make Name abstract und bieten einen intelligenten Konstruktor. Das bedeutet, dass Sie das Name Daten Konstruktor nicht exportieren, und bieten einen Maybe -returning Konstruktor statt:

module Data.Name 
(Name -- note: only export type, not data constructor 
, fromString 
, toString 
) where 

newtype Name = Name String 

fromString :: String -> Maybe Name 
fromString n | null n   = Nothing 
      | length n > 100 = Nothing 
      | otherwise  = Just (Name n) 

toString :: Name -> String 
toString (Name n) = n 

Es ist nun unmöglich, einen Name Wert der falschen Länge außerhalb des Moduls zu konstruieren.

Für Age, könnten Sie das gleiche tun, oder eine Art von Data.Word verwenden, oder verwenden Sie die folgende ineffizient, aber garantiert nicht negative Darstellung:

data Age = Zero | PlusOne Age 
+0

Würde es Ihnen etwas ausmachen, auf die Unterschiede zwischen der gleichen Sache, Typ aus Data.Word oder der ineffiziente Vorschlag zu erarbeiten? Vielleicht auch Liquid Haskell? – fred

+0

Einen intelligenten Konstruktor für 'Age' freizulegen ist etwas Arbeit; Sie müssen einen neuen Typ und zwei Funktionen erstellen. aber es ist effizient und einfach zu bedienen. 'Data.Word' ist nur eine vorzeichenlose Ganzzahl, wie' uint' in C#. Es hat wenig Vorteile, die "Zero"/"PlusOne" Darstellung wirklich zu verwenden. Ich weiß sehr wenig über Liquid Haskell, aber in F *, das auch Verfeinerungs-Typen enthält, würden Sie 'type Age = i: int {i> = 0}' schreiben und der Typ-Checker wird Ihr Programm ablehnen, wenn es nicht 100 ist % sicher, dass das Alter, das du passierst, größer oder gleich null ist. – rightfold

+0

Ich weiß, dass dies nur ein dummes Beispiel ist, aber viele Menschen machen den Fehler der realen Welt, eine Länge für Namen festzulegen, die zu kurz ist, um alle Namen zu verarbeiten, die die Leute tatsächlich haben. Einige Grenzen sind vernünftig, um DOS-Angriffe und dergleichen zu vermeiden, aber ich wäre sehr misstrauisch gegenüber einer Namenslänge von unter 256 Zeichen oder so, und wenn ich das System entwerfe, würde ich einen Experten konsultieren wollen, um sicherzustellen, dass es genug war . – dfeuer

2

Dieser Code Geruch in einigen Sprachen sein kann, aber In Haskell wird das normalerweise nicht berücksichtigt. Sie müssen eine konkrete Darstellung eines Namens und Geburtsdatums irgendwo wählen, und die Datentypen Erklärung von Person ist wahrscheinlich der beste Ort, um es zu tun. In Haskell würde der übliche Weg, um anderen Code von der Namensdarstellung abhängig zu machen, Person abstract sein. Anstatt den Person Konstruktor freizulegen, stellen Sie Funktionen zum Erstellen, Ändern und Überprüfen von Person Werten zur Verfügung.

+0

Meiner Erfahrung nach bevorzugen praktisch alle Entwickler und Codebasen in allen Sprachen Primitive für alles und verwenden anstelle von Klassen/Typen Variablennamen und Inline-Validierungen, um anzuzeigen und zu validieren, was die Dinge sind. Sie würden sagen, mein Beispiel sei Klassen-/Typ-Obsession und nenne das einen Code-Geruch. – fred

+0

@fred, es hängt wirklich vom Kontext ab und wie das Programm wächst. Vorzeitige Abstraktion und vorzeitige Verallgemeinerung können viel Zeit verschwenden, wenn Ihre ersten Vermutungen falsch sind. "Person" scheint eine ziemlich vernünftige Basisabstraktion zu sein, aber wiederum hängt das vom Programm ab. – dfeuer

+0

Lächerlich.Haskell hat Phantom-Typen, expressive Polymorphie, intelligente Konstruktoren, die Typen ohne Konstruktoren freilegen. All die Dinge, um mit primitiver Obsession effektiv umzugehen! Primitive Obsession - wenn im Speicher Darstellung in keiner Weise von legitimen Domain-Daten + Verhalten abweichen. (ZB absolute Größe und relative Größe werden durch die gleichen Werte im Computerspeicher dargestellt, aber verwirrend mit anderen wird schlimme Folgen haben. Phantom Typ hinzufügen. Hakell wird diese Fehler zeigen) –