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.

相關文章
相關標籤/搜索