Haskell学习笔记二:自定义类型

内容提要:

代数数据类型 - Algebraic Data Types;

自定义数据类型 - data关键字;值构造器;类型变量与类型构造器;

记录(Record)语法 - 简化自定义数据类型的一种语法糖;

一个完整的例子 - PurchaseOrder定义和简单计算、单元测试;

代数数据类型(Algebraic Data Types)

为什么Haskell的数据类型会有代数数据类型这个名字?回想我们初中时代,初次学习代数的情况,印象最深刻就是x,y,z代替了具体的数字,引入方程式的概念,对

解决问题进行了抽象,比如使用圆的面积计算公式:Area = πr2,其中r就是一个代表圆半径的字母符号。

Haskell就是借鉴代数理论来构建自身的类型体系的。如果构建的类型是由一些确定值组成的,那么就不需要类型变量,这类类型就是一个确定的类型;如果构建的类

型是由一些确定值加上类型变量组成的,那么这种类型就不是具体的类型,而是抽象的类型,在具体使用的时候,等到类型变量替换为具体的类型,才能够成为具体的

类型。空说无凭,马上进入实际的例子。

自定义数据类型

首先看看系统定义的Bool类型:

data Bool = False | True

详细解释一下:

  • 使用关键字data进行新类型的定义;
  • data后面跟新类型的名字,这个名字必须是大写字母开头;
  • 在等号后面,是新类型的可选值表达式,又称为值构造器(value constructors);
  • 如果有多个值构造器,之间使用“|”进行分割,表示或者、多种可能的含义;
  • 总结来说,Bool类型的定义可以这么理解:Haskell自定义的名字为“Bool”的类型,其取值或者为False,或者为True;

再看看自定义的“Shape”类型:

data Shape = Circle Float Float Float | Rectangle Float Float Float Float

和Bool类型定义略有不同的地方是,Shape有两个值构造器,其中每个值构造器的第一个字符串是其名字,后面是对应的具体类型;

可以这么理解自定义的Shape类型:

  • 自定义了名字为“Shape”的类型,其取值可能是一个Circle(圆),或者是一个Rectangle(长方形);
  • 如果是Circle,那么由三个Float值组成,分别代表Circle的圆心的横坐标、纵坐标,及其Circle的半径;
  • 如果是Rectangle,那么由四个Float值组成,前两个Float代表Rectangle的左上点的横坐标、纵坐标;后两个Float代表Rectangle的右下方的横坐标、纵坐标。

将上面关于Shape自定义类型的代码写入文件Shape.hs文件中,然后使用GHCI加载(编译),然后看看下面的一些交互结果:

-- 加载Shape.hs并编译
:l Shape

-- 首先看看True和False的类型是不是Bool
:t True
-- 结果为:True :: Bool
:t False
-- 结果为:False :: Bool

-- 然后看看Circle和Rectangle的类型是不是Shape:t Circle-- 结果为:Circle :: Float -> Float -> Float -> Shape:t Rectangle-- 结果为:Rectangle :: Float -> Float -> Float -> Float -> Shape

-- 可以看到,无论是Cirle还是Rectangle,都是值构造器,返回结果都是Shape

为什么Haskell自定义类型的值构造器是一个大写字符串,表示值构造器的名字呢?比如Bool类型的True,False,和Shape类型的Circle,Rectangle;因为从本质上来说

这个名字其实是一个函数名,通过这个函数名加上具体的参数(可能有,可能没有),就是构造出对应类型的具体值。这种构造具体类型不同值的实现方式,和其他语言有很

大的区别,比如C#,一个Shape类型,不可能有两个不同名字的构造函数。这点需要慢慢体会和适应,至少有一点好处,不同的构造器名字,可读性和表意性会更优。

Haskell自定义类型时,还可以带上类型变量进行抽象,比如Haskell自带的Maybe类型,其定义如下:

data Maybe a = Nothing | Just a

每次看到这个定义,我都由衷地觉得很酷:a是一个类型变量,在定义Maybe类型的时候,加上了这个类型变量,从而构建出一个新的类型,这个类型有两种可能的值:Nothing

表示空,什么都没有;Just a则通过值构建器Just,包装了具体的类型a。至于具体a是什么类型,不是Maybe类型定义时关注的,这极大地丰富了Maybe的内涵,抽象出Maybe的

本质——要么是空,要么就只是a这个东西。

Haskell可以推导中Maybe的一些具体类型,比如:

:t Just 1
-- 结果为:Just 1 :: Num a => Maybe a,表示兼容任何数字类型的类型
:t Just ‘a‘
-- 结果为:Just ‘a‘ :: Maybe Char
:t Nothing
-- 结果为:Nothing :: Maybe a,由于Nothing没有具体制定a的类型,所以
-- 这个值本身还是多态的

记录语法(Record)

在上面定义Shape的代码中,Circle后面跟了三个Float,Rectangle后面跟了四个Float,初次看到这种定义,肯定会很疑惑,这些Float都是什么含义?没有对应的名字么?

如果是有其他语言背景,特别是面向对象的一些语言,比如C#,Java,我们都熟悉类中属性都是有名字的,这样表意性和可读性才更好。其实一些函数式编程语言,比如Erlang、

Haskell,定义复杂或者组合类型时,都缺乏描述性的支持。好在Record语法,从间接层面可以解决这个问题。

比如如果使用记录语法再次定义Shape类型:

data Shape_Record = Circle { hAxis :: Float, cAxis :: Float, radius :: Float}
        | Rectangle { leftTopX :: Float, leftTopY :: Float, rightDownX :: Float, rightDownY :: Float} deriving (Show)

在上面使用记录语法定义新类型的例子中,值构造器名字后面,大括号包含的内容,就是记录语法:给定一个小写字母开头的名字,然后是对应的类型说明。有了类型中相关

字段的名字说明,就比较类似C#或者Java中的属性定义了,可读性和易用性得到了提升。

其实从本质上来说,记录语法不过是语法糖,因为类型中每个值对应的名字,其实是一个方法,可以从具体构建的类型实例中,或者对应字段的值。比如:

-- 根据定义的名字,或者对应的值
hAxis Circle { hAxis = 10.0, cAxis = 12.0, radius = 5.5}
-- 结果为:10.0

一个实际的例子

假设一家电子商务公司需要向供应商进货,通过生成采购订单和供应商进行采购动作。其中采购订单的主要内容包括:一个订单号、供应商的信息、采购商品的信息等,假设

采购订单本身有一个逻辑检查,即采购订单的总价值等于所有采购商品的价值之和(忽略运费之类的实际情况)。下面的代码展示了采购订单的定义,一些单元测试确保逻辑

正确。

采购订单(PurchaseOrder)定义,及其计算订单总价值(POAmount)的函数定义:

-- PurchaseOrder.hs 文件

module PurchaseOrder where

import Data.List -- 导入Data.List模块,需要使用其中定义的函数

-- 首先定义商品,即采购的具体商品,使用记录语法定义
-- Item信息包括:编号、描述、采购数量、单价、总价
data Item = Item { itemNumber :: String , itemDescription :: String, ordQty :: Int, unitPrice :: Float, extPrice :: Float } deriving (Show)

-- 给商品的List定义一个别名,便于阅读type ItemList = [Item]

-- 定义采购订单,使用记录语法-- 采购订单信息包括:订单编号、供应商编号、收货地址、订单总价、采购商品明细(是一个List)data PurchaseOrder = PurchaseOrder { poNumber :: String, vendorNumber :: String, shipToAddress :: String                    , poAmount :: Float, itemList :: ItemList } deriving (Show)

-- 定义计算采购订单总价的两个函数:逻辑很简单,即采购订单总价,等于其中每个商品的总价之和calculatePOAmount‘ :: PurchaseOrder -> FloatcalculatePOAmount‘ po = foldl (\acc x -> acc + x) 0 [ extPrice i || i <- itemList po]

calculatePOAmount :: PurchaseOrder -> PurchaseOrdercalculatePOAmount po = PurchaseOrder { poNumber = (poNumber po)                      , vendorNumber = (vendorNumber po)                      , shipToAddress = (shipToAddress po)                      , poAmount = (calculatePOAmount‘ po)                      , itemList = (itemList po)}

接下来对上面的代码进行单元测试,主要测试两个逻辑:第一、商品的总价等于单价乘以数量;第二、采购订单的总价等于每个商品的总价之和:

-- Test_PurchaseOrder.hsmodule Test_PurchaseOrder where

import PurchaseOrder
import Data.List

-- build test data
buildDefaultTestItem :: Item
buildDefaultTestItem = Item {itemNumber = "26-106-016", itemDescription = "this is a test item", ordQty = 100, unitPrice = 10.12, extPrice = 1012}

 buildTestItemList :: ItemList
 buildTestItemList = [ buildDefaultTestItem | x <- [1..10] ]

 -- test methods
 checkItemExtPrice :: Item -> Bool
 checkItemExtPrice item = (fromIntegral $ ordQty item) * (unitPrice item) == (extPrice item)

 checkSingleItem :: Bool
 checkSingleItem = checkItemExtPrice $ buildDefaultTestItem

 checkItemListExtPrice :: ItemList -> Bool
 checkItemListExtPrice itemList = and $ map checkItemExtPrice itemList

 checkItemList :: Bool
 checkItemList = checkItemListExtPrice $ buildTestItemList

 buildPO :: PurchaseOrder
 buildPO = PurchaseOrder {poNumber = "1926543", vendorNumber = "28483", shipToAddress = "test address here", itemList = buildTestItemList, poAmount = 0.00}

 checkPOAmount :: Bool
 checkPOAmount = (fromIntegral $ 1012 * 10) == (poAmount $ calculatePOAmount buildPO)

 all_methods_test :: String
 all_methods_test = if (and [checkSingleItem, checkItemList, checkPOAmount])
            then "All Pass."
            else "Failed."

最后将Test_PurchaseOrder.hs装载到GHCI中,通过编译,然后运行其中的all_methods_test方法,结果显示"All Pass",即所有检查的逻辑都是正确的。

时间: 2024-09-29 08:35:53

Haskell学习笔记二:自定义类型的相关文章

Swift学习笔记(二)参数类型

关于参数类型,在以前的编程过程中,很多时间都忽视了形参与实参的区别.通过这两天的学习,算是捡回了漏掉的知识. 在swift中,参数有形参和实参之分,形参即只能在函数内部调用的参数,默认是不能修改的,如果想要修改就需要在参数前添加var声明. 但这样的声明过后,仍旧不会改变实参的值,这样就要用到inout了,传递给inout的参数类型必须是var类型的,不能是let类型或者字面类型,(字面类型是在swift中常提的一个术语,个人认为就是赋值语句,也不能修改)而且在传递过程中,要用传值符号"&

C#学习笔记二: C#类型详解

前言 这次分享的主要内容有五个, 分别是值类型和引用类型, 装箱与拆箱,常量与变量,运算符重载,static字段和static构造函数. 后期的分享会针对于C#2.0 3.0 4.0 等新特性进行. 再会有三篇博客  这个系列的就会结束了. 也算是自己对园子中@Learning Hard出版的<<C#学习笔记>>的一个总结了. 博客内容基本上都是白天抽空在公司写好的了, 但是由于公司内部网络不能登录博客园所以只能够夜晚拿回来修改,  写的不好或者不对的地方也请各位大神指出. 在下感

Haskell学习笔记一:类型和类型类相关内容

内容提要: 静态类型系统: 编译时确定类型错误: 类型推导机制: 基础类型:Int,Integer,Float,Double,Bool,Char: 类型变量: 基础类型类:Eq,Ord,Show,Read,Enum,Bounded,Num,Integral,Floating: Haskell是一门函数式编程语言,被称为最为纯粹的函数式编程语言.Haskell的类型系统非常强大,其中包含了很多有趣.抽象.某种程度上充满学术气息的特质. Haskell属于静态类型语言,这意味着: 每个值或者表达式,

Hadoop学习笔记—5.自定义类型处理手机上网日志

一.测试数据:手机上网日志 1.1 关于这个日志 假设我们如下一个日志文件,这个文件的内容是来自某个电信运营商的手机上网日志,文件的内容已经经过了优化,格式比较规整,便于学习研究. 该文件的内容如下(这里我只截取了三行): 1363157993044 18211575961 94-71-AC-CD-E6-18:CMCC-EASY 120.196.100.99 iface.qiyi.com 视频网站 15 12 1527 2106 200 1363157995033 15920133257 5C-

linux shell学习笔记二---自定义函数(定义、返回值、变量作用域)介绍

linux shell 可以用户定义函数,然后在shell脚本中可以随便调用.下面说说它的定义方法,以及调用需要注意那些事项. 一.定义shell函数(define function) 语法: [ function ] funname [()] { action; [return int;] } 说明: 1.可以带function fun() 定义,也可以直接fun() 定义,不带任何参数. 2.参数返回,可以显示加:return 返回,如果不加,将以最后一条命令运行结果,作为返回值. retu

Go语言学习笔记(二) [变量、类型、关键字]

日期:2014年7月19日 1.Go 在语法上有着类 C 的感觉.如果你希望将两个(或更多)语句放在一行书写,它们 必须用分号分隔.一般情况下,你不需要分号. 2.Go 同其他语言不同的地方在于变量的类型在变量名的后面.例如:不是,int a,而是 a int.当定义了一个变量,它默认赋值为其类型的 null 值.这意味着,在 var a int后,a 的 值为 0.而 var s string,意味着 s 被赋值为零长度字符串,也就是 "". 3.Go语言的变量声明和赋值 在Go中使

Caliburn.Micro学习笔记(二)----Actions

Caliburn.Micro学习笔记(二)----Actions 上一篇已经简单说了一下引导类和简单的控件绑定 我的上一个例子里的button自动匹配到ViewModel事件你一定感觉很好玩吧 今天说一下它的Actions,看一下Caliburn.Micro给我们提供了多强大的支持 我们还是从做例子开始 demo的源码下载在文章的最后 例子1.无参数方法调用 点击button把textBox输入的文本弹出来 如果textbox里没有文本button不可点,看一下效果图 看一下前台代码 <Stac

angular学习笔记(二十八)-$http(6)-使用ngResource模块构建RESTful架构

ngResource模块是angular专门为RESTful架构而设计的一个模块,它提供了'$resource'模块,$resource模块是基于$http的一个封装.下面来看看它的详细用法 1.引入angular-resource.min.js文件 2.在模块中依赖ngResourece,在服务中注入$resource var HttpREST = angular.module('HttpREST',['ngResource']); HttpREST.factory('cardResource

Android学习笔记二

17. 在ContentProvider中定义的getType()方法是定义URI的内容类型. 18. SQLiteDatabase类中的insert/delete/update/query方法其实也挺好用的,我在EquipmentProvider类中做了实现 19. Android专门有个单元测试项目(Android Test Project),在这个项目中,可以新建一个继承AndroidTestCase类的具体测试类来单元测试某个功能.我新建了一个AndroidTestProject项目,在