Real World Haskell - Chapter 3. Defining Types, Streamlining Functions

    技术2024-08-19  64

     

     

     

     

    Chapter 3. Defining Types, Streamlining Functions

     

     

    使用“data”自定义新类型

     

    -- book.hs

    data BookInfo = Book Int String [String]

                  deriving (Show)

     

    mybook = Book 9780135072455 "Algebra of Programming"

                   ["Richard Bird", "Oege de Moor"]

    ---------------

    :load book

    mybook

    --> Book 9780135072455 "Algebra of Programming" ["Richard Bird","Oege de Moor"]

    :type mybook

    --> mybook :: BookInfo

     

    BookInfo”是类型构造器(type constructor),类型名必须以大写字母开头。

     

    Book”是值构造器(value constructor),用于创建类型为BookInfo 的值。值构造器也必须以大写字母开头。

     

    Int”,“String”,“[String]”是slot,值就装在这些slot 里面

     

    ghci 中构造Book

     

    ghci> Book 0 "The Book of Imaginary Beings" ["Jorge Luis Borges"]

    ghci> let cities = Book 173 "Use of Weapons" ["Iain M. Banks"]

     

    :info”命令显示详细类型信息

     

    :info BookInfo

     

    Naming Types and Values

     

    类型名和类型值是彼此独立的。

     

    类型构造器只用于类型声明或类型签名。值构造器只用于真实代码。

     

    类型构造器和值构造器可以有相同的名字

     

    data BookReview = BookReview BookInfo CustomerID String

     

    Type Synonyms

     

     

    定义类型别名的方法

     

    type B = Int

    data A = A BookInfo Int B

    type C = (A, B)

     

    这里C 的类型是tupe(A,B),这些类型别名没有值构造器

     

     

    Algebraic Data Types

     

    所有使用Data 关键字定义的类型都是代数类型

     

    Bool 是最简单的代数数据类型

     

    代数数据类型可以有多个值构造器

     

    data Bool = False | True

     

    Bool 类型有两个值构造器,Ture False

     

    代数类型的值构造器可以接受零个或多个参数。

     

    type CardHolder = String

    type CardNumber = String

    type Address = [String]

     

    data BillingInfo = CreditCard CardNumber CardHolder Address

    | CashOnDelivery

    | Invoice CustomerID

    deriving (Show)

     

    BillingInfo 有三个值构造器,而且各自接受的参数也不同。

     

    Tuples, Algebraic Data Types, and When to Use Each

     

    有相同类型签名的pairs tuple 具有相同的类型

     

    a = ("Porpoise", "Grey")

    b = ("Table", "Oak")

     

    ==”操作符要求两边有相同的类型

     

    Analogues to Algebraic Data Types in Other Languages

     

    The enumeration

     

    用代数类型实现枚举

     

    data Roygbiv = Red

    | Orange

    | Yellow

    | Green

    | Blue

    | Indigo

    | Violet

    deriving (Eq, Show)

     

    Red == Yellow --> False

    Green == Green --> True

     

    The discriminated union

     

    用代数类型实现联合(union)

     

    type Vector = (Double, Double)

    data Shape = Circle Vector Double

    | Poly [Vector]

     

    如果使用Circle 构造器,我们实际上创建了并存储了一个Circle 值,相反用Poly ,就存Poly 值。

     

    Pattern Matching

     

    假设我们得到某类型的一些值:

     

    1. 如果此类型有多个值构造器,我们应能够知道这个值是由哪个构造器构造出来的。

    2. 如果值构造器拥有数据域(slot),我们应能够取出这些数据值。

     

    Haskell 有一个简单但极有用的模式匹配机制,可以完成这两个任务。

     

    实现Not 函数

     

    myNot True = False

    myNot False = True

     

    myNot True -->Fale

    myNot True -->Ture

     

    看起来像是定义了两个名为myNot 的函数,但Haskell 允许将函数定义为一系列的等式。这样同一个函数对于不同的输入模式具有不同的行为。

     

    排在前面的函数等式优先匹配。

     

     

    对列表的所有元素求和

     

    sumList (x:xs) = x + sumList xs

    sumList [] = 0

     

    sumList [1,2]  --> 3

     

    (运算符“: 在表的前端添加一个元素,前面必须是一个元素,后面必须是一个列表)

    [1,2] (1:(2:[])) 的简略写法。但函数参数列表(x:xs) 中的“:”是表示匹配,结果是1 匹配x2:[] 匹配xs函数的参数匹配就把list 拆成两部分,part1 + 递归调用part2注意函数等式的排列顺序,排在前面的优先匹配。

     

    最后,标准函数sum 与上面我们定义的函数有同样的功能。

     

    Construction and Deconstruction

     

    不要被deconstruction 迷惑,模式匹配不解构任何东西,只是让我们“look inside”某个东西。

     

     

    Further Adventures

     

    返回3-tuple 的第三个值的函数

     

    third (a, b, c) = c

     

    一个稍复杂的参数匹配例子

     

    complicated (True, a, x:xs, 5) = (a, xs)

     

    complicated (True, 1, [1,2,3], 5)  -- > (1,[2,3])

    complicated (False, 1, [1,2,3], 5)  -- 参数匹配失败

     

     

    data BookInfo = Book Int String [String]

                  deriving (Show)

    bookID (Book id title authors) = id

     

    bookID (Book 1 “tt” [“dsa”]) --> 1

     

     

    m3list (3:xs) = 3 + m3list xs  -- 只匹配全3 为元素的list,不是全3 会引起运行时错误

    m3list [] = 0

    m3list [3,3,3]  --> 9

     

    The Wild Card Pattern

     

    _”称为wild card

     

    模式中的任意值“_

     

    data BookInfo = Book Int String [String]

                  deriving (Show)

    bookID (Book id _  _) = id

     

    bookID (Book 1 “tt” [“dsa”]) --> 1

     

    Exhaustive Patterns and Wild Cards

     

    默认匹配

     

    goodExample (x:xs) = x + goodExample xs

    goodExample _ = 0

    goodExample [1,2] -- > 3

     

    Record Syntax

     

    nicerID (Book id _ _ ) = id 这种代码称为boilerplate,既bulkyirksome

     

    取出记录里的字段值

     

    type CustomerID = Int

    type Address = [String]

     

    data Customer = Customer {

           customerID :: CustomerID,  -- customerID 是一个函数,输入Customer返回CustomerID

        customerName :: String,

        customerAddress :: Address

    } deriving (Show)

     

    let aa = Customer 1 “dfs” [“dfsa”]

    customerID aa --> 1

     

    记录函数(accessor functions)的另一种写法

     

    data Customer = Customer Int String [String]

                                deriving (Show)

    customerID :: Customer -> Int

    customerID (Customer id _ _) = id

     

    创建记录类型值的Detail 风格

     

    customer2 = Customer {

                         customerID = 271828,

                         customerAddress = ["1048576 Disk Drive",

                                                        "Milpitas, CA 95134",

                                                        "USA"],

                         customerName = "Jane Q. Citizen"

    }

     

    data CalendarTime = CalendarTime {

                                       ctYear :: Int,

                                       ctDay, ctHour, ctMin, ctSec :: Int

    }

     

    ctime = CalendarTime {

                  ctYear = 1,

                  ctDay=3, ctHour=4, ctMin=5, ctSec=6

    }

     

    使用记录的detail 创建风格可以改变记录的order。这里customerName 就和customerAddress 的顺序就对调了。

     

    使用记录的detail 创建风格打印的时侯也会显示更多信息。

     

    如果不使用记录语法,要从一个类型提取某个字段的值将是一件痛若的事。

     

    Parameterized Types

     

    Maybe a 类型

     

    data Maybe a = Something a

                          | Nonething

                          deriving (Eq, Show)

    Something 2 --> Something 2

    :type Something  “string"

    --> Something  “string" :: Main.Maybe [char]

    Something  (Something  2)  --> 嵌套定义要加括号

     

    Recursive Types

     

    list 类型是递归定义的

     

    List a 类型

     

    data List a = Cons a (List a)

                         | Nil

                           deriving (Show)

    fromList (x:xs) = Cons x (fromList xs)  -- list 来构造List a

    fromList [] = Nil

     

    Nil -> Nil

    Cons 0 Nil -- > Cons 0 Nil

    Cons 1 it -- > Cons 1 (Cons 0 Nil)

    fromList "durian"

    --> Cons 'd' (Cons 'u' (Cons 'r' (Cons 'i' (Cons 'a' (Cons 'n' Nil)))))

     

    Cons 构造器需要两个参数,一个a, 一个list a

     

    二叉树

     

    data Tree a = Node a (Tree a) (Tree a)

                         | Empty

                         deriving (Show)

    Node 1 (Node 2 Empty Empty) (Node 3 Empty Empty)

    simpleTree = Node "parent" (Node "left child" Empty Empty)

    (Node "right child" Empty Empty)

     

    Haskell 没有Null 类型,可以用Maybe 类型获得Null 类型的效果,但是这样做需要模式匹配;替代方法是用无参的值构造器(如NilEmpty

     

    Reporting Errors

     

    error 函数输出错误消息,并立既终止程序。

     

    error :: String -> a

     

    因为返回类型是a ,所以可以在任何地方调用,并返回正确的类型。

     

    返回List 列表的第二个元素,不够两个元素就报错(有问题版)

     

    mySecond :: [a] -> a  -- 不写好像也可以自动推断出参数类型因为tail 接受list

    mySecond xs = if null (tail xs)

                          then error "list too short"

                          else head (tail xs)

     

    mySecond "ab"               --> 'b'

    mySecond "a"                 --> Exception: list too short

    head (mySecond [[9]])     --> Exception: list too short

    mySecond []  --> 参数匹配失败,要求[a],这里是[]

     

    错误处理函数error 的缺点是不能区分对待可恢复错误和能终止程序的至命错误

     

    A More Controlled Approach

     

    返回List 列表的第二个元素,不够两个元素就报错(待改进)

     

    safeSecond :: [a] -> Maybe a

     

    safeSecond [] = Nothing

    safeSecond xs = if null (tail xs)

                                then Nothing

                                else Just (head (tail xs))

     

    safeSecond []              --> Nothing

    safeSecond [1]             --> Nothing

    safeSecond [1,2]   -->Just 2

    safeSecond [1,2,3]       -->Just 2

     

    返回List 列表的第二个元素,不够两个元素就报错

     

    tidySecond :: [a] -> Maybe a

    tidySecond (_:x:_) = Just x  -- 亮点!

    tidySecond _ = Nothing

     

    tidySecond [1,2]    -->Just 2

     

    模式“_:x:_”只匹配有两个元素以上的List 列表。

     

    Introducing Local Variables

     

    let 关键字开始一个变量声明块,并以关键字in 作为结束

     

    函数体内部使用let 定义局部变量

     

    lend amount balance = let reserve = 100  -- 如果ghci 报错就在行后加个分号“;

                              newBalance = balance - amount

                          in if balance < reserve

                             then Nothing

                             else Just newBalance

     

    let 的左边被捆绑到右边的表达式,注意let是与表达式而不是与值捆绑

     

    如果我们能使用一个名字,它就是“in scope”,否则就是“out of scope”;如果一个名字在整个源文件可见,我们就说它位于“top level

     

    Shadowing

     

    let 可以互相嵌套但不是聪明的做法

     

    foo = let a = 1

          in let b = 2

             in a + b

    print foo  -- >3

     

    shadowing 变量(影子变量)

     

    bar = let x = 1

          in ((let x = "foo" in x), x)

    -- >("foo",1)

     

    里面的x,是外面x shadowing shadowing 变量是一个同名变量,类型和值都可以不同。

     

    shadow 一个函数的参数

     

    quux a = let a = "foo";

             in  a ++ "eek!"

     

    type quux  -- t -> [Char]

     

    因为函数的参数永远不会被用到,所以参数可以是任意类型t

     

    使用GHC 的选项“-fwarn-name-shadowing”可以开启shadow 警告,以避免发生莫明其妙的问题。

     

    The where Clause

     

    我们可以使用另一种机制引入局部变量:where 子句

     

    使用where 子句推迟局部变量的定义

     

    lend2 amount balance = if amount < reserve * 0.5

                           then Just newBalance

                           else Nothing

        where reserve = 100

             newBalance = balance – amount  -- 注意和上面对齐,不然会出错

     

    where 子句有助于将读者的注意力集中在重要的位置,局部变量的值留到后面用where 来定义

     

    Local Functions, Global Variables

     

    定义局部函数和定义局部变量一样简单。

     

    定义局部函数

     

    pluralise :: String -> [Int] -> [String]

    pluralise word counts = map plural counts

        where plural 0 = "no " ++ word ++ "s"

              plural 1 = "one " ++ word

              plural n = show n ++ " " ++ word ++ "s"

     

    局部函数plural 含有多个等式,并使用了外部函数pluralise 的变量word

     

    定义全局变量

     

    itemName = "Weighted Companion Cube"

     

    在源文件的top level 定义既可

     

    The Offside Rule and Whitespace in an Expression

     

    Haskell 这种使用缩排的规则称为“offside rule”。

     

    第一个使用top-level 的声明或定义可以从任何列开始,然后Haskell 编译器或解释器就会记住那个缩排层级数;所有后面的top-level 声明都必须和第一个top-level 具有同样的缩进

     

    bar = let b = 2

               c = True

          in let a = b

             in (a, c)

    a 只对里面的let 可见,外面的let 不可见。

     

    foo = x

        where x = y

                 where y = 2  -- 注意这个where 的缩进

     

    A Note About Tabs Versus Spaces

     

    把编辑器调成用空格代替Tab

     

    The Offside Rule Is Not Mandatory

     

    offside rule”不是强制的

     

    可以用大括号“{}”将equations 括起来,并用分号“;”分隔里面的每项。

     

    用大括号代替缩排

     

    bar = let a = 1

    b = 2

    c = 3

    in a + b + c

     

    foo = let { a = 1; b = 2;

    c = 3 }

    in a + b + c

     

    The case Expression

     

    函数定义不是唯一可以使用模式匹配的地方。“case”语句也会进行模式匹配。

     

    case 语句

     

    fromMaybe defval wrapped =

        case wrapped of

             Nothing ->   defval

             Just value ->  value

     

    -> 左边是模式,如被匹配右边就得以evaluate

     

    case 关键字后接一个任意的表达式,并且用这个表达式的值去匹配“of”后面的表达式。匹配的须序是自上往下。

     

    of”后面的所有表达式必须具有相同的类型。

     

    wild card 表达式“_”可以放在case 语句的最后用作默认匹配

     

    Common Beginner Mistakes with Patterns

     

    以下是初学者对模式的常见误用

     

    Incorrectly Matching Against a Variable

     

    of”后面应该写case 对应的表达式的值

     

    错误的版本

     

    data Fruit = Apple | Orange

    apple = "apple"

    orange = "orange"

     

    whichFruit :: String -> Fruit

    whichFruit f = case f of

                     apple -> Apple

                     orange -> Orange

     

    正确的版本

     

    data Fruit = Apple | Orange  -- 亮点

    betterFruit f = case f of

                       "apple"   Apple

                       "orange"   Orange

     

    函数betterFruit 的类型签名可以自动推断出来,因为输入与String 型的"apple" 匹配,输出与Fruit 类型的"Apple" 匹配。

     

    Incorrectly Trying to Compare for Equality

     

    模式里的名字只能出现一次

     

    错误的例子

     

    bad_nodesAreSame (Node a _ _) (Node a _ _) = Just a

    bad_nodesAreSame _ _ = Nothing

     

    解决这个问题要用到guards

     

    Conditional Evaluation with Guards

     

    对函数的参数进行条件测试

     

    -- Node Tree 的值构造器;结点本身也是树,两者是一回事

    data Tree a = Node a (Tree a) (Tree a)

                         | Empty

                         deriving (Show)

     

    nodesAreSame (Node a _ _) (Node b _ _)

        | a == b = Just a

    nodesAreSame _ _ = Nothing

     

    | a == b 是参数模式的“guards”。一个模式可以有零个或多个“guards

    guards 就是类型为bool 的一个表达式

    guards”使用符号“| 引入

     

    使用guards 让代码变得更清晰

     

    myDrop n xs = if n <= 0 || null xs

                  then xs

                  else myDrop (n - 1) (tail xs)

                 

    niceDrop n xs | n <= 0 = xs

    niceDrop _ [] = []

    niceDrop n (_:xs) = niceDrop (n - 1) xs

     

    最新回复(0)