为清晰起见,制作新类型/数据是否不好?

时间:2019-07-16 16:48:48

标签: haskell type-safety type-alias newtype

我想知道做这样的事情是否不好:

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来说还比较陌生,所以我不知道对于这种类型的东西有什么好的做法。如果这不是一个好主意,或者有一些可以提高清晰度的方法是首选,那会是什么?

5 个答案:

答案 0 :(得分:18)

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

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用于特殊化多态类型。

答案 1 :(得分:13)

我不会考虑这种糟糕的形式,但是很明显,我并不代表整个Haskell社区。据我所知,语言功能是为特定目的而存在的:使代码更易于阅读。

可以在各种“核心”库中找到使用类型别名的示例。例如,Read类定义了此方法:

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 {{1}),这通常会很有用}实例转换为其他实例所没有的类型。

答案 2 :(得分:8)

使用newtype(创建一个新类型,该新类型与基础类型具有相同的表示形式,但不能用其替代)被认为是 good 形式。这是避免primitive obsession的廉价方法,并且对于Haskell尤其有用,因为在Haskell中,函数参数的名称在签名中不可见。

新类型也可以是悬挂有用的类型类实例的地方。

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

  • Coercible一个“魔术”类型类,当newtype构造函数在作用域内时,它可以简化新类型及其基础类型之间的转换。通常在避免函数实现中的样板化很有用。

    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。基于基础类型中可用实例的自动派生新类型实例的扩展。

  • DerivingVia是一个更通用的扩展,用于基于其他新类型中具有相同基础类型的实例为新类型自动派生实例。

    < / li>

答案 3 :(得分:5)

要注意的重要一件事是AlignmentChar的关系不仅是清晰度,而且是正确性之一。您的Alignment类型表示这样一个事实,即与Char有多少居民相比,只有三个有效的对齐方式。通过使用它,您可以避免使用无效的值和运算带来麻烦,并且还可以使GHC在打开警告的情况下,告知您有关模式匹配不完整的信息。

对于同义词,意见不一。就我个人而言,我觉得type这类小型Int的同义词可以使您跟踪严格意义相同的事物的不同名称,从而增加认知负担。就是说leftaroundabout makes a great point,因为这种同义词在解决方案原型设计的早期阶段很有用,当您不必担心要为您的域采用的具体表示形式的细节时对象。

(值得一提的是,此处关于type的评论在很大程度上不适用于newtype。用例却有所不同:尽管type只是为newtype引入了不同的名称同一件事, let cell = self.tableView.dequeueReusableCell(withIdentifier: "commentCell", for: IndexPath(row: 0, section: 0)) as! commentTableViewCell 引入了不同的命令。这可能是一个令人惊讶的强大举动-进一步讨论,请参见danidiaz's answer。)

答案 4 :(得分:2)

绝对是件好事,这是另一个示例,假设您将此数据类型与某些操作结合在一起:

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,则必须在功能上进行一些调整,但这就是另一个问题。