haskell Types 和 Typeclasses
Algebraic Data Types 入门
在前面的章节中,我们谈了一些 Haskell 内置的类型和 Typeclass。而在本章中,我们将学习构造类型和 Typeclass 的方法。
我们已经见识过许多类型,如 Bool
、Int
、Char
、Maybe
等等,不过在 Haskell 中该如何构造自己的类型呢?好问题,一种方法是使用 data 关键字。首先我们来看看 Bool
在标准函式库中的定义:
data Bool = False | True
data 表示我们要定义一个新的类型。=
的左端标明类型的名称即 Bool
,=
的右端就是值构造子 (Value Constructor),它们明确了该类型可能的值。|
读作"或",所以可以这样阅读该声明:Bool
类型的值可以是 True
或False
。类型名和值构造子的首字母必大写。
相似,我们可以假想 Int
类型的声明:
data Int = -2147483648 | -2147483647 | ... | -1 | 0 | 1 | 2 | ... | 2147483647
头尾两个值构造子分别表示了 Int
类型的最小值和最大值,注意到真正的类型宣告不是长这个样子的,这样写只是为了便于理解。我们用省略号表示中间省略的一大段数字。
我们想想 Haskell 中图形的表示方法。表示圆可以用一个 Tuple,如(43.1,55.0,10.4)
,前两项表示圆心的位置,末项表示半径。听着不错,不过三维矢量或其它什么东西也可能是这种形式!更好的方法就是自己构造一个表示图形的类型。假定图形可以是圆 (Circle) 或长方形 (Rectangle):
data Shape = Circle Float Float Float | Rectangle Float Float Float Float
这是啥,想想?Circle
的值构造子有三个项,都是 Float。可见我们在定义值构造子时,可以在后面跟几个类型表示它包含值的类型。在这里,前两项表示圆心的坐标,尾项表示半径。Rectangle
的值构造子取四个 Float
项,前两项表示其左上角的坐标,后两项表示右下角的坐标。
谈到「项」 (field),其实应为「参数」 (parameters)。值构造子的本质是个函数,可以返回一个类型的值。我们看下这两个值构造子的类型声明:
ghci> :t Circle
Circle :: Float -> Float -> Float -> Shape
ghci> :t Rectangle
Rectangle :: Float -> Float -> Float -> Float -> Shape
Cool,这么说值构造子就跟普通函数并无二致啰,谁想得到?我们写个函数计算图形面积:
surface :: Shape -> Float
surface (Circle _ _ r) = pi * r ^ 2
surface (Rectangle x1 y1 x2 y2) = (abs $ x2 - x1) * (abs $ y2 - y1)
值得一提的是,它的类型声明表示了该函数取一个 Shape
值并返回一个 Float
值。写 Circle -> Float
是不可以的,因为 Circle
并非类型,真正的类型应该是 Shape
。这与不能写True->False
的道理是一样的。再就是,我们使用的模式匹配针对的都是值构造子。之前我们匹配过 []
、False
或 5
,它们都是不包含参数的值构造子。
我们只关心圆的半径,因此不需理会表示坐标的前两项:
ghci> surface $ Circle 10 20 10
314.15927
ghci> surface $ Rectangle 0 0 100 100
10000.0
Yay,it works!不过我们若尝试输出 Circle 10 20
到控制台,就会得到一个错误。这是因为 Haskell 还不知道该类型的字符串表示方法。想想,当我们往控制台输出值的时候,Haskell 会先调用 show
函数得到这个值的字符串表示才会输出。因此要让我们的 Shape
类型成为 Show 类型类的成员。可以这样修改:
data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)
先不去深究 deriving(派生),可以先这样理解:若在 data
声明的后面加上 deriving (Show)
,那 Haskell 就会自动将该类型至于 Show
类型类之中。好了,由于值构造子是个函数,因此我们可以拿它交给 map
,拿它不全调用,以及普通函数能做的一切。
ghci> Circle 10 20 5
Circle 10.0 20.0 5.0
ghci> Rectangle 50 230 60 90
Rectangle 50.0 230.0 60.0 90.0
我们若要取一组不同半径的同心圆,可以这样:
ghci> map (Circle 10 20) [4,5,6,6]
[Circle 10.0 20.0 4.0,Circle 10.0 20.0 5.0,Circle 10.0 20.0 6.0,Circle 10.0 20.0 6.0]
我们的类型还可以更好。增加加一个表示二维空间中点的类型,可以让我们的 Shape
更加容易理解:
data Point = Point Float Float deriving (Show)
data Shape = Circle Point Float | Rectangle Point Point deriving (Show)
注意下 Point
的定义,它的类型与值构造子用了相同的名字。没啥特殊含义,实际上,在一个类型含有唯一值构造子时这种重名是很常见的。好的,如今我们的 Circle
含有两个项,一个是 Point
类型,一个是 Float
类型,好作区分。Rectangle
也是同样,我们得修改 surface
函数以适应类型定义的变动。
surface :: Shape -> Float
surface (Circle _ r) = pi * r ^ 2
surface (Rectangle (Point x1 y1) (Point x2 y2)) = (abs $ x2 - x1) * (abs $ y2 - y1)
唯一需要修改的地方就是模式。在 Circle
的模式中,我们无视了整个 Point
。而在 Rectangle
的模式中,我们用了一个嵌套的模式来取得 Point
中的项。若出于某原因而需要整个 Point
,那么直接匹配就是了。
ghci> surface (Rectangle (Point 0 0) (Point 100 100))
10000.0
ghci> surface (Circle (Point 0 0) 24)
1809.5574
表示移动一个图形的函数该怎么写?它应当取一个 Shape
和表示位移的两个数,返回一个位于新位置的图形。
nudge :: Shape -> Float -> Float -> Shape
nudge (Circle (Point x y) r) a b = Circle (Point (x+a) (y+b)) r
nudge (Rectangle (Point x1 y1) (Point x2 y2)) a b = Rectangle (Point (x1+a) (y1+b)) (Point (x2+a) (y2+b))
简洁明了。我们再给这一 Shape
的点加上位移的量。
ghci> nudge (Circle (Point 34 34) 10) 5 10
Circle (Point 39.0 44.0) 10.0
如果不想直接处理 Point
,我们可以搞个辅助函数 (auxilliary function),初始从原点创建图形,再移动它们。
baseCircle :: Float -> Shape
baseCircle r = Circle (Point 0 0) r
baseRect :: Float -> Float -> Shape
baseRect width height = Rectangle (Point 0 0) (Point width height)
ghci> nudge (baseRect 40 100) 60 23
Rectangle (Point 60.0 23.0) (Point 100.0 123.0)
毫无疑问,你可以把你的数据类型导出到模块中。只要把你的类型与要导出的函数写到一起就是了。再在后面跟个括号,列出要导出的值构造子,用逗号隔开。如要导出所有的值构造子,那就写个..。
若要将这里定义的所有函数和类型都导出到一个模块中,可以这样:
module Shapes
( Point(..)
, Shape(..)
, surface
, nudge
, baseCircle
, baseRect
) where
一个 Shape
(..),我们就导出了 Shape
的所有值构造子。这一来无论谁导入我们的模块,都可以用 Rectangle
和Circle
值构造子来构造 Shape
了。这与写 Shape(Rectangle,Circle)
等价。
我们可以选择不导出任何 Shape
的值构造子,这一来使用我们模块的人就只能用辅助函数 baseCircle
和 baseRect
来得到 Shape
了。Data.Map
就是这一套,没有 Map.Map [(1,2),(3,4)]
,因为它没有导出任何一个值构造子。但你可以用,像 Map.fromList
这样的辅助函数得到 map
。应该记住,值构造子只是函数而已,如果不导出它们,就拒绝了使用我们模块的人调用它们。但可以使用其他返回该类型的函数,来取得这一类型的值。
不导出数据类型的值构造子隐藏了他们的内部实现,令类型的抽象度更高。同时,我们模块的用户也就无法使用该值构造子进行模式匹配了。
haskell Types 和 Typeclasses的更多相关文章
- Haskell Types与Typeclasses
可使用 :t 命令检测表达式类型. 明确的类型首字母必大写. 一.Types Char Bool Int(有界,与Integer类型对比效率高) Integer(无界,与Int类型对比效率低) Flo ...
- Haskell手撸Softmax回归实现MNIST手写识别
Haskell手撸Softmax回归实现MNIST手写识别 前言 初学Haskell,看的书是Learn You a Haskell for Great Good, 才刚看到Making Our Ow ...
- Typeclassopedia
https://wiki.haskell.org/wikiupload/8/85/TMR-Issue13.pdf By Brent Yorgey, byorgey@gmail.com Original ...
- Type class-Typeclass-泛型基础上的二次抽象---随意多态
对泛型的类型添加约束,从而使泛型类型的变量具有某种通用操作. 再使用这些操作,参与到其它操作中. In computer science, a type class is a type system ...
- [Real World Haskell翻译]第20章 Haskell系统编程
第20章 Haskell系统编程 到目前为止,我们已经讨论了大多数的高层次的概念.Haskell也可以用于较低级别的系统编程.很可能是用haskell编写出底层的与操作系统接口的程序. 在本章中,我们 ...
- haskell笔记1
haskell platform下载:https://www.haskell.org/platform/ 进入haskell控制台,终端输入 $ ghci 编译文件 :l file.hs 数组操作 & ...
- Introduction to Haskell
"I know why you're here. ...why you hardly sleep, why night after night, you sit by your comput ...
- Haskell 笔记(三)类型系统
类型 (Type) Haskell的类型系统式静态类型系统,在编译的时候就知道数据类型,所以不同类型的值运算在编译的时候就会报错,比如用布尔值和整数运算,在C语言中这种运算就不会报错. Haskell ...
- Haskell复习笔记(一)
Haskell笔记这是第三次总结,前两次都因为各种原因丢失了,对于Haskell我算不上什么大神,只不过在大学时为了学习算法时选择了Haskell. 当时的入门书籍选择的是<Learn You ...
随机推荐
- 20151216JqueryUI学习笔记---按钮
按钮(button) , 可以给生硬的原生按钮或者文本提供更多丰富多彩的外观. 它不单单可以设置按钮或文本,还可以设置单选按钮和多选按钮.一. 使用 button 按钮使用 button 按钮 UI ...
- ACM——线性表操作
线性表操作 时间限制(普通/Java):1000MS/3000MS 运行内存限制:65536KByte总提交:2795 测试通过:589 描述 线性表是n个元素 ...
- zoj 1649 Rescue (BFS)(转载)
又是类似骑士拯救公主,不过这个是朋友拯救天使的故事... 不同的是,天使有多个朋友,而骑士一般单枪匹马比较帅~ 求到达天使的最短时间,杀死一个护卫1 units time , 走一个格子 1 unit ...
- nodejs 实现机制
最近在学习nodejs,作为一个从未学过javascript的程序员,把自己学习的过程贴出来,当做记录和总结吧. 1. nodejs的原理: nodejs完全是基于事件轮询机制的一个javascrip ...
- 九度OJ 1452 搬寝室 -- 动态规划
题目地址:http://ac.jobdu.com/problem.php?pid=1452 题目描述: 搬寝室是很累的,xhd深有体会.时间追述2006年7月9号,那天xhd迫于无奈要从27号楼搬到3 ...
- spark-shell 执行脚本并传入参数
使用方法: ./spark-script.sh your_file.scala first_arg second_arg third_arg 脚本: scala_file=$ arguments=$@ ...
- Python之实现一个简易计算器
自己动手写计算器 一.功能分析 用户输入一个类似这样 3*( 4+ 50 )-(( 100 + 40 )*5/2- 3*2* 2/4+9)*((( 3 + 4)-4)-4) 这样的表达式,假设表达式里 ...
- shipyard docker 管理平台
终于把shipyard弄好了. 我也是根据shipyard的官方文档,做的.在刚开始的时候觉得好难,也遇到了困难,查看了好多文档 但做完之后发现,只需要几步就能简单的配置成功,就能运行了. 修改tcp ...
- 23种设计模式全解析 (java版本)
转自:http://blog.csdn.net/longyulu/article/details/9159589 其中PHP常用的五种设计模式分别为:工厂模式,单例模式,观察者模式,策略模式,命令模式 ...
- H5小内容(四)
SVG 基本内容 SVG并不属于HTML5专有内容 HTML5提供有关SVG原生的内容 在HTML5出现之前,就有SVG内容 SVG,简单来说就是矢量图 ...