理解 Haskell 类型

我最近从 Perl 工作中休息了一下,去 Recurse Center 学习。我正在学习 Haskell,到目前为止这是一次有趣的冒险。我听说过 Haskell 类型系统的优点,于是开始从一本入门书籍《Learn You a Haskell for Great Good!》开始学习。这本书充满了卡通式的幽默——“这能有多难?”我自问。答案却是“很难”。我发现 Haskell 的类型系统并不直观,因此本文概述了我对 Haskell 类型的理解。如果你是一个有命令式编程背景的程序员,可能会觉得这篇文章很有用。

你的直觉是错误的

对于命令式语言的程序员来说,Haskell 的关键字可能会误导。以 type 关键字为例。它并不是为了创建新的类型本身,而是 类型同义词,类似于现有类型的别名。我可能会这样使用它

type FirstName = String
type LastName = String
type Age = Int

声明新类型的一种方法是用 data 关键字(很自然!)。如果我想创建一个表示人的类型,可以使用 data

data Person = Person String String Int

这声明了一个名为 Person 的新类型,它有 2 个字符串和 1 个整型属性。但我也可以使用前面示例中的类型同义词,并澄清我的意图

data Person = Person FirstName LastName Age

函数和类型

Haskell 中函数签名可以由类型限制。我可以创建一个函数来告诉两个人中谁年纪更大

eldest :: Person -> Person -> String
eldest (Person x1 y1 z1) (Person x2 y2 z2)
  | z1 > z2   = x1 ++ " " ++ y1 ++ " is older"
  | z1 < z2   = x2 ++ " " ++ y2 ++ " is older"
  | otherwise = "They're the same age!"

这里有很多新的语法,所以请耐心一点。第一行声明了一个名为 eldest 的函数,它接受两个人的参数并返回一个字符串。第二行将每个人的属性分配给变量。函数的其余部分测试哪个人年纪更大,并返回相应的信息。我将所有的代码保存到一个名为“person.hs”的文件中,这样我就可以在 Haskell REPL(ghci)中测试这个函数。

ghci> :l person.hs
[1 of 1] Compiling Main             ( person.hs, interpreted )
Ok, modules loaded: Main.
ghci> let a = Person "Bart" "Simpson" 10
ghci> let b = Person "Lisa" "Simpson" 7
ghci> eldest a b
"Bart Simpson is older"

有时我们不需要在函数中访问类型的所有属性。在这种情况下,Haskell 允许你使用 _ 作为占位符,它不会被分配给变量。例如,为了打印一个人的首字母缩写,我只需要知道他们的名字和姓氏

initials :: Person -> String
initials (Person x y _) = [head x,'.',head y, '.']

代码的第二行将一个人的名字分配给 x,姓氏分配给 y。然后使用 head 获取每个名字的首字母,并返回一个新的字符列表,每个字符后面跟着一个点。我可以通过重新加载“person.hs”来测试这个函数。

ghci> :l person.hs
[1 of 1] Compiling Main             ( person.hs, interpreted )
Ok, modules loaded: Main.
ghci> let a = Person "Maggie" "Simpson" 1
ghci> initials a
"M.S."

类型类

类型类类似于 Perl 中的 traits(角色)。例如,整数是类型类 Ord 的实例,因为它们可以排序,Num 的实例,因为它们是数字,等等。每个类型类定义了在特定上下文中处理类型的函数。Eq 类型类添加了使用运算符如 == 比较类型的能力。

通过使用类型类泛化类型的属性,Haskell 可以支持泛型函数,这些函数在类型类上操作,而不是被限制在一种类型上。从 Learn You a Haskellquicksort 函数的签名是一个很好的例子。

quicksort :: (Ord a) => [a] -> [a]
quicksort [] = []
quicksort (x:xs) =·
    let smallerSorted = quicksort [a | a <- xs, a <= x]
        biggerSorted = quicksort [a | a <- xs, a > x]
    in  smallerSorted ++ [x] ++ biggerSorted

这声明了一个新的函数 quicksort,它限制为可排序类型的列表。忽略代码的主体,只需关注代码的第一行,函数签名。代码 (Ord a) 定义了函数的类型类约束。这个函数可以用来排序任何可排序的项,如数字列表。字符串不是字符的列表吗?我想我们也可以用 quicksort 对它们进行排序。

实例和类

如果在某些 Haskell 代码中看到了 instance 关键字,你可能会想“哇,这是一个单例构造函数!”但实际上 instance 是用来将类型转换为类型类实例的。当你考虑到每个类型都是一个类型类的 实例 时,这就有意义了。回顾一下我之前提到的 Person 类型,如果我想让它可以排序怎么办?通常在讲英语的世界里,人们是按姓氏排序的,所以我将按这种方式实现它。

data Person = Person FirstName LastName Age deriving (Eq, Show)

instance Ord Person where
  compare (Person _ a _) (Person _ b _) = compare a b

我首先通过添加 deriving (Eq, Show) 更新了 Person 的类型声明。这些操作在整个类型上(所有属性一起)。Eq 将允许 Haskell 比较Person是否相等,而 Show 只允许 Haskell 将类型序列化为字符串。第二行代码使用 instance 使 Person 可排序。最后一行实现了使用 Person 的姓氏属性的比较函数。我可以使用上面声明的 quicksort 函数来测试代码。

ghci> :l person.hs
[1 of 1] Compiling Main             ( person.hs, interpreted )
Ok, modules loaded: Main.
ghci> let a = Person "Jason" "Bourne" 37
ghci> let b = Person "James" "Bond" 42
ghci> quicksort [a,b]
[Person "James" "Bond" 43,Person "Jason" "Bourne" 37]

这使我们的人名列表按姓氏排序,由于 PersonShow 的实例,Haskell 能够将详细信息打印到命令行。不错!

需要关注的最后一个关键字是 class。现在你可能不会感到惊讶地发现,class 不是用来声明类,就像在命令式编程中一样,而是用来创建新的类型类。当你开始使用 Haskell 时,你可能不会经常使用它,但考虑到减少重复代码,记住这一点是有用的。如果你有多个代码集,它们对不同的类型执行非常相似的操作,考虑创建一个新的类型类并将函数合并到新的类型类中,以保持 DRY

代码完成

这是最终的代码

--person.hs
type FirstName = String
type LastName  = String
type Age = Int 

data Person = Person FirstName LastName Age deriving (Eq, Show)

eldest :: Person -> Person -> String
eldest (Person x1 y1 z1) (Person x2 y2 z2)
  | z1 > z2   = x1 ++ " " ++ y1 ++ " is older"
  | z1 < z2   = x2 ++ " " ++ y2 ++ " is older"
  | otherwise = "They're the same age!"

initials :: Person -> String
initials (Person x y _) = [head x,'.',head y, '.']

quicksort [] = []
quicksort (x:xs) =·
    let smallerSorted = quicksort [a | a <- xs, a <= x]
        biggerSorted = quicksort [a | a <- xs, a > x]
    in  smallerSorted ++ [x] ++ biggerSorted

instance Ord Person where
  compare (Person _ a _) (Person _ b _) = compare a b

通过艰难的方式学习 Haskell

尽管它的行为有点幼稚,但 Learn You a Haskell 深入探讨了 Haskell 的类型系统,有时可能会有些冗长。我的当前学习方法包括阅读这本书,逐字逐句地输入每个代码示例,并研究宾夕法尼亚州立大学的 cis194 课程。两者都是免费的。O'Reilly 的 Real World Haskell 也免费在网上提供,并强调 Haskell 的直接实际应用。当你厌倦了编码二叉搜索树和排序算法时,它是个不错的选择。如果你需要查找 Haskell 术语,DuckDuckGo 有 !h 撞击,它会自动搜索 Hoogle


这篇文章最初发布在 PerlTricks.com 上。

标签

David Farrell

David 是一位职业程序员,他经常在 推特博客 上分享关于代码和编程艺术的见解。

浏览他们的文章

反馈

这篇文章有什么问题吗?通过在 GitHub 上打开一个问题或拉取请求来帮助我们。