为了清楚起见,制作 new types/datas 是不是不好的形式?

Is it bad form to make new types/datas for clarity?

我想知道做这样的事情是否是错误的形式:

data Alignment = LeftAl | CenterAl | RightAl
type Delimiter = Char
type Width     = Int

setW :: Width -> Alignment -> Delimiter -> String -> String

而不是像这样:

setW :: Int -> Char -> Char -> String -> String

我知道有效地重新制作这些类型只会占用几行代码以换取更清晰的代码。但是,如果我将类型 Delimiter 用于多个函数,这对于导入此模块或稍后阅读代码的人来说会更清楚。

我对 Haskell 比较陌生,所以我不知道对这类东西有什么好的做法。如果这不是一个好主意,或者有什么可以提高清晰度的首选,那会是什么?

您正在使用类型别名,它们对代码可读性的帮助很小。但是,最好使用 newtype 而不是 type 以获得更好的 type-safety。像这样:

data Alignment = LeftAl | CenterAl | RightAl
newtype Delimiter = Delimiter { unDelimiter :: Char }
newtype Width     = Width { unWidth :: Int }

setW :: Width -> Alignment -> Delimiter -> String -> String

您将处理 newtype 的额外包装和展开。但是代码将更加健壮以应对进一步的重构。 This style guide 建议仅将 type 用于特化多态类型。

我不认为这种形式不好,但显然,我不代表整个 Haskell 社区。据我所知,语言功能的存在是为了那个特定的目的:使代码更易于阅读。

可以在各种 'core' 库中找到使用类型别名的示例。例如,Read class 定义了这个方法:

readList :: ReadS [a]

ReadS 类型只是一个类型别名

type ReadS a = String -> [(a, String)]

另一个例子是 Forest type in Data.Tree:

type Forest a = [Tree a]

正如 Shersh 指出的那样,您还可以将新类型包装在 newtype 声明中。如果您需要以某种方式以某种方式限制原始类型(例如使用 smart constructors),或者如果您想要在不创建孤立实例的情况下向类型添加功能(典型示例是定义 QuickCheck Arbitrary 实例到类型,否则不会随此类实例一起提供)。

使用 newtype——它创建了一个与底层类型具有相同表示但不可替代的新类型——被认为是 good 形式。这是避免 primitive obsession 的一种廉价方法,它对 Haskell 特别有用,因为在 Haskell 中,函数参数的名称在签名中不可见。

Newtypes 也可以是挂起有用类型类实例的地方。

鉴于新类型在 Haskell 中无处不在,随着时间的推移,该语言已经获得了一些工具和习语来操纵它们:

  • Coercible 一个 "magical" 类型类,当新类型构造函数在范围内时,它简化了新类型与其基础类型之间的转换。通常有助于避免函数实现中的样板文件。

    ghci> coerce (Sum (5::Int)) :: Int

    ghci> coerce [Sum (5::Int)] :: [Int]

    ghci> coerce ((+) :: Int -> Int -> Int) :: Identity Int -> Identity Int -> Identity Int

  • ala。一种习惯用法(在各种包中实现),它简化了我们可能希望与 foldMap.

    等函数一起使用的新类型的选择

    ala Sum foldMap [1,2,3,4 :: Int] :: Int

  • GeneralizedNewtypeDeriving。根据基础类型中可用的实例,为您的新类型扩展 auto-deriving 个实例。

  • DerivingVia 一个更通用的扩展,基于 一些其他 新类型中可用的实例,为您的新类型提供 auto-deriving 个实例相同的底层类型。

需要注意的一件重要事情是,AlignmentChar 不仅是清晰度问题,而且是正确性问题。您的 Alignment 类型表达了这样一个事实,即只有三个有效对齐方式,而不是 Char 拥有的许多居民。通过使用它,您可以避免无效值和操作的麻烦,并且还可以让 GHC 在警告打开时以信息方式告诉您不完整的模式匹配。

至于同义词,众说纷纭。就我个人而言,我觉得 typeInt 这样的小类型的同义词会增加认知负担,因为它会让你跟踪完全相同的东西的不同名称。也就是说, 因为这种同义词在解决方案原型设计的早期阶段很有用,此时您不必担心要为您的域采用的具体表示的细节对象。

(值得一提的是,这里关于 type 的评论在很大程度上不适用于 newtype。但是,用例不同:而 type 只是引入了一个不同的同一事物的名称,newtype 通过命令引入了不同的事物。这可能是一个非常强大的举动——请参阅 了解更多讨论。)

绝对好,这里是另一个例子,假设你有这个数据类型和一些操作:

data Form = Square Int | Rectangle Int Int | EqTriangle Int

perimeter :: Form -> Int
perimeter (Square s)      = s * 4
perimeter (Rectangle b h) = (b * h) * 2
perimeter (EqTriangle s)  = s * 3

area :: Form -> Int
area (Square s)      = s ^ 2
area (Rectangle b h) = (b * h)
area (EqTriangle s)  = (s ^ 2) `div` 2 

现在假设您添加圆圈:

data Form = Square Int | Rectangle Int Int | EqTriangle Int | Cicle Int

添加其操作:

perimeter (Cicle r )      = pi * 2 * r

area (Cicle r)       = pi * r ^ 2

不是很好吗?现在我想使用 Float...我必须将每个 Int 更改为 Float

data Form = Square Double | Rectangle Double Double | EqTriangle Double | Cicle Double


area :: Form -> Double

perimeter :: Form -> Double

但是,如果为了清楚甚至为了重用,我使用类型怎么办?

data Form = Square Side | Rectangle Side Side | EqTriangle Side | Cicle Radius

type Distance = Int
type Side = Distance
type Radius = Distance
type Area = Distance

perimeter :: Form -> Distance
perimeter (Square s)      = s * 4
perimeter (Rectangle b h) = (b * h) * 2
perimeter (EqTriangle s)  = s * 3
perimeter (Cicle r )      = pi * 2 * r

area :: Form -> Area
area (Square s)      = s * s
area (Rectangle b h) = (b * h)
area (EqTriangle s)  = (s * 2) / 2
area (Cicle r)       = pi * r * r

这允许我只更改代码中的一行来更改类型,假设我希望 Distance 为 Int,我将只更改它

perimeter :: Form -> Distance
...

totalDistance :: [Form] -> Distance
totalDistance = foldr (\x rs -> perimeter x + rs) 0

我希望距离为浮点数,所以我只更改:

type Distance = Float

如果我想把它改成Int,我必须在功能上做一些调整,但那是另一个问题。