zoukankan      html  css  js  c++  java
  • 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 -> 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)
    }

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

    -- 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.

  • 相关阅读:
    并查集模板
    143. 最大异或对(Trie树存整数+二进制)
    Trie树模板
    835. 字符串统计(Trie树模板题)
    生兔兔
    汉诺塔问题
    一本通 1296:开餐馆
    一本通 1272:【例9.16】分组背包
    一本通 1292:宠物小精灵之收服
    一本通 1271:【例9.15】潜水员
  • 原文地址:https://www.cnblogs.com/seaman-h-zhang/p/4568957.html
Copyright © 2011-2022 走看看