在 Haskell 中避免原始痴迷

Avoiding primitive obsession in Haskell

来自http://learnyouahaskell.com/making-our-own-types-and-typeclasses

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

在实际应用程序中,使用像 String 和 Int 这样的原语来表示姓名和年龄会构成对原语的痴迷,一种代码味道。 (同样显然 Date born 比 Int age 更可取,但让我们忽略它)相反,人们更喜欢

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

在 OO 语言中,这看起来像

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);
  }
}

但是如何在惯用的最佳实践中实现同样的效果 Haskell?

使Name抽象并提供智能构造函数。这意味着您不导出 Name 数据构造函数,而是提供一个 Maybe-返回构造函数:

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

现在无法在该模块外构造 Name 长度错误的值。

对于 Age,您可以做同样的事情,或者使用 Data.Word 中的类型,或者使用以下低效但保证非负的表示形式:

data Age = Zero | PlusOne Age

这在某些语言中可能是代码异味,但在 Haskell 中通常不被认为是一种代码异味。您必须在 某处 选择名称和出生日期的具体表示,而 Person 的数据类型声明可能是最好的选择。在 Haskell 中,防止其他代码依赖名称表示的通常方法是使 Person 抽象。不要公开 Person 构造函数,而是公开用于创建、修改和检查 Person 值的函数。