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

内容提要:

代数数据类型 - Algebraic Data Types;编程

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

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

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

 

代数数据类型(Algebraic Data Types)单元测试

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

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

 

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

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

类型。空说无凭,立刻进入实际的例子。spa

 

自定义数据类型

首先看看系统定义的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 -> Float
calculatePOAmount' po = foldl (\acc x -> acc + x) 0 [ extPrice i || i <- itemList po]

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

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

-- Test_PurchaseOrder.hs
module 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",即全部检查的逻辑都是正确的。

 

补充一段摘自“book.realworldhaskell.org”中关于Haskell类型变量和C++模板,Java/C#泛型的对比文字:

To once again extend an analoty to more familiar languages, patameterised types bear some resemblance to templates in C++, and to generics in

Java. Just be aware that this is shallow analogy. Templates and generics were added to their respective languages long after the languages were

initially defined, and have an awkward feel. Haskell's parameterised types are simpler and easier to use, as the language was designed with them

from the begining.

相关文章
相关标签/搜索