我想知道做这样的事情是否不好:
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来说还比较陌生,所以我不知道对于这种类型的东西有什么好的做法。如果这不是一个好主意,或者有一些可以提高清晰度的方法是首选,那会是什么?
答案 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
是一个更通用的扩展,用于基于其他新类型中具有相同基础类型的实例为新类型自动派生实例。
答案 3 :(得分:5)
要注意的重要一件事是Alignment
与Char
的关系不仅是清晰度,而且是正确性之一。您的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,则必须在功能上进行一些调整,但这就是另一个问题。