In [1]:
:opt no-pager
"Hello" ++ " World!"


"Hello World!"

In [2]:
thing :: String -> Int -> Int
thing "no" _ = 100
thing str int = int + length str

thing "no" 10
thing "ah" 10
:t thing


100

12

thing :: String -> Int -> Int


In [3]:
:t lines


lines :: String -> [String]


In [4]:
lines "the quick\nbrown fox\njumps"
concat (lines "the quick\nbrown fox\njumps")


["the quick","brown fox","jumps"]

"the quickbrown foxjumps"


In [5]:
:t readFile


readFile :: FilePath -> IO String


In [6]:
add a b = a + b

In [7]:
:t add


add :: forall a. Num a => a -> a -> a


In [8]:
:info Integral


<div style='background: rgb(247, 247, 247);'><form><textarea id='code'>class (Real a, Enum a) => Integral a where
  quot :: a -> a -> a
  rem :: a -> a -> a
  div :: a -> a -> a
  mod :: a -> a -> a
  quotRem :: a -> a -> (a, a)
  divMod :: a -> a -> (a, a)
  toInteger :: a -> Integer
  	-- Defined in ‘GHC.Real’
instance Integral Word -- Defined in ‘GHC.Real’
instance Integral Integer -- Defined in ‘GHC.Real’
instance Integral Int -- Defined in ‘GHC.Real’</textarea></form></div><script>CodeMirror.fromTextArea(document.getElementById('code'), {mode: 'haskell', readOnly: 'nocursor'});</script>

In [9]:
import Control.Monad
import Control.Concurrent

forM_ [1..5] $ \x -> do
  print x
  threadDelay $ 200 * 1000


1
2
3
4
5


In [10]:
:t forM_


forM_ :: forall (t :: * -> *) a (m :: * -> *) b. (Monad m, Foldable t) => t a -> (a -> m b) -> m ()


In [11]:
data Color = Red | Green | Blue

In [12]:
import IHaskell.Display

instance IHaskellDisplay Color where
  display color = return $ Display [html code]
    where
      code = concat ["<div style='font-weight: bold; color:"
                    , css color
                    , "'>Look!</div>"]
      css Red   = "red"
      css Blue  = "blue"
      css Green = "green"

In [13]:
Red
Green
Blue
:t Red
:t Green


Look!

Look!

Look!

Red :: Color

Green :: Color


In [14]:
myDrop n xs = if n <= 0 || null xs
              then xs
              else myDrop (n - 1) (tail xs)

In [15]:
myDrop 2 "foobar"
myDrop 3 []
:t myDrop


"obar"

[]

myDrop :: forall a a1. (Num a, Ord a) => a -> [a1] -> [a1]


In [16]:
isOdd n = mod n 2 == 1

In [17]:
:t isOdd
isOdd 2
isOdd 3


isOdd :: forall a. Integral a => a -> Bool

False

True


In [18]:
:t last
:t last [1,2,3]
:t last ['1','2','3']


last :: forall a. [a] -> a

last [1,2,3] :: forall a. Num a => a

last ['1','2','3'] :: Char


In [19]:
1 :: Int
1 :: Integer
:t 1


1

1

1 :: forall a. Num a => a


In [20]:
take 2 [1,2,3,4,5]
:t take 2
:t take


[1,2]

take 2 :: forall a. [a] -> [a]

take :: forall a. Int -> [a] -> [a]

Real World Haskell 第三章


In [21]:
-- Bookstore
type CustomerID = Int
type CardHolder = String
type CardNumber = String
type Address = [String]
data BillingInfo = CreditCard CardNumber CardHolder Address
                 | CashOnDelivery
                 | Invoice CustomerID
                   deriving (Show)

In [22]:
:t CreditCard
:t CashOnDelivery
:t Invoice


CreditCard :: CardNumber -> CardHolder -> Address -> BillingInfo

CashOnDelivery :: BillingInfo

Invoice :: CustomerID -> BillingInfo


In [23]:
CreditCard "2901650221064486" "Thomas Gradgrind" ["Dickens", "England"]
:type it


CreditCard "2901650221064486" "Thomas Gradgrind" ["Dickens","England"]

it :: BillingInfo


In [24]:
sumList :: Num a => [a] -> a
sumList (x:xs) = x + sumList xs
sumList []  = 0

sumList [1,2,3,4]
:t sumList
:t sum


10

sumList :: forall a. Num a => [a] -> a

sum :: forall (t :: * -> *) a. (Num a, Foldable t) => t a -> a


In [25]:
import System.Time
getClockTime
toCalendarTime it


Fri Nov  6 10:25:17 CST 2015

CalendarTime {ctYear = 2015, ctMonth = November, ctDay = 6, ctHour = 10, ctMin = 25, ctSec = 17, ctPicosec = 989259000000, ctWDay = Friday, ctYDay = 309, ctTZName = "CST", ctTZ = 28800, ctIsDST = False}


In [26]:
data List a = Cons a (List a)
            | Nil
              deriving (Show)

In [27]:
fromList (x:xs) = Cons x (fromList xs)
fromList []     = Nil

fromList [1,2,3]
:t it
:t fromList


Cons 1 (Cons 2 (Cons 3 Nil))

it :: forall a. Num a => List a

fromList :: forall a. [a] -> List a

Real World Haskell 第三章练习1:


In [28]:
fromCons (Cons a as) = (a:fromCons as)
fromCons Nil = []
:t fromCons
fromCons (Cons 1 (Cons 2 (Cons 3 Nil)))


fromCons :: forall a. List a -> [a]

[1,2,3]

Real World Haskell 第三章练习2:


In [29]:
data Tree a = Node a (Maybe (Tree a)) (Maybe (Tree a))
              deriving (Show)

simpleTree = Node "parent" (Just (Node "left tree" Nothing Nothing)) Nothing
simpleTree
:t Node


Node "parent" (Just (Node "left tree" Nothing Nothing)) Nothing

Node :: forall a. a -> Maybe (Tree a) -> Maybe (Tree a) -> Tree a


In [30]:
:t error


error :: forall a. [Char] -> a


In [31]:
-- file: ch03/MySecond.hs
mySecond :: [a] -> a

mySecond xs = if null (tail xs)
              then error "list too short"
              else head (tail xs)

In [32]:
mySecond "xi"
mySecond [2]


'i'

list too short


In [33]:
head (mySecond [[9]])


list too short


In [34]:
mySecond []


Prelude.tail: empty list


In [35]:
-- file: ch03/MySecond.hs
safeSecond :: [a] -> Maybe a

safeSecond [] = Nothing
safeSecond xs = if null (tail xs)
                then Nothing
                else Just (head (tail xs))

In [36]:
safeSecond []
safeSecond [1]
safeSecond [1,2]
safeSecond [1,2,3]


Nothing

Nothing

Just 2

Just 2


In [37]:
-- file: ch03/MySecond.hs
tidySecond :: [a] -> Maybe a

tidySecond (_:x:_) = Just x
tidySecond _       = Nothing
译注:(_:x:_) 相当于 (_:(x:_)),考虑到列表的元素只能是同一种类型
假想第一个 _ 是 a 类型,那么这个模式匹配的是 (a:(a:[a, a, ...])) 或 (a:(a:[]))
即元素是 a 类型的值的一个列表,并且至少有 2 个元素
那么如果第一个 _ 匹配到了 [],有没有可能使最终匹配到得列表只有一个元素呢?
([]:(x:_)) 说明 a 是列表类型,那么 x 也必须是列表类型,x 至少是 []
而 ([]:([]:[])) -> ([]:[[]]) -> [[], []],还是 2 个元素

第一个模式仅仅匹配那些至少有两个元素的列表(因为它有两个列表构造器),并将列表的第二个元素的值绑定给 变量 x。如果第一个模式匹配失败了,则匹配第二个模式。


In [38]:
tidySecond []
tidySecond [1]
tidySecond [1,2]
tidySecond [1,2,3]


Nothing

Nothing

Just 2

Just 2

引入局部变量

在函数体内部,我们可以在任何地方使用 let 表达式引入新的局部变量。请看下面这个简单的函数,它用来检查我们是否可以向顾客出借现金。我们需要确保剩余的保证金不少于 100 元的情况下,才能出借现金,并返回减去出借金额后的余额。


In [39]:
-- file: ch03/Lending.hs
lend amount balance = let reserve    = 100
                          newBalance = balance - amount
                      in if balance < reserve
                         then Nothing
                         else Just newBalance

这段代码中使用了 let 关键字标识一个变量声明区块的开始,用 in 关键字标识这个区块的结束。每行引入了一个局部变量。变量名在 = 的左侧,右侧则是该变量所绑定的表达式。

特别提示

请特别注意我们的用词:在 let 区块中,变量名被绑定到了一个表达式而不是一个值。由于 Haskell 是一门惰性求值的语言,变量名所对应的表达式一直到被用到时才会求值。在上面的例子里,如果没有满足保证金的要求,就不会计算 newBalance 的值。 当我们在一个 let 区块中定义一个变量时,我们称之为let 范围内的变量。顾名思义即是:我们将这个变量限制在这个 let 区块内。 另外,上面这个例子中对空白和缩进的使用也值得特别注意。在下一节 “The offside rule and white space in an expression” 中我们会着重讲解其中的奥妙。

在 let 区块内定义的变量,既可以在定义区内使用,也可以在紧跟着 in 关键字的表达式中使用。

一般来说,我们将代码中可以使用一个变量名的地方称作这个变量名的作用域(scope)。如果我们能使用,则说明在 作用域内,反之则说明在作用域外 。如果一个变量名在整个源代码的任意处都可以使用,则说明它位于最顶层的作用域。

屏蔽

我们可以在表达式中使用嵌套的 let 区块。


In [40]:
-- file: ch03/NestedLets.hs
foo = let a = 1
      in let b = 2
         in a + b

上面的写法是完全合法的;但是在嵌套的 let 表达式里重复使用相同的变量名并不明智。


In [41]:
-- file: ch03/NestedLets.hs
bar = let x = 1
      in ((let x = "foo" in x), x)


Redundant bracket
Found:
((let x = "foo" in x), x)
Why Not:
(let x = "foo" in x, x)

如上,内部的 x 隐藏了,或称作屏蔽(shadowing), 外部的 x。它们的变量名一样,但后者拥有完全不同的类型和值。


In [42]:
bar


("foo",1)

我们同样也可以屏蔽一个函数的参数,并导致更加奇怪的结果。你认为下面这个函数的类型是什么?


In [43]:
-- file: ch03/NestedLets.hs
quux a = let a = "foo"
         in a ++ "eek!"

:type quux
quux 1


quux :: forall t. t -> [Char]

"fooeek!"

在函数的内部,由于 let-绑定的变量名 a 屏蔽了函数的参数,使得参数 a 没有起到任何作用,因此该参数可以是任何类型的。

Tip

编译器警告是你的朋友

显然屏蔽会导致混乱和恶心的 bug,因此 GHC 设置了一个有用的选项 -fwarn-name-shadowing。如果你开启了这个功能,每当屏蔽某个变量名时,GHC 就会打印出一条警告。

where 从句

还有另一种方法也可以用来引入局部变量:where 从句。where 从句中的定义在其所跟随的主句中有效。下面是和 lend 函数类似的一个例子,不同之处是使用了 where 而不是 let


In [44]:
-- file: ch03/Lending.hs
lend2 amount balance = if amount < reserve * 0.5
                       then Just newBalance
                       else Nothing
    where reserve    = 100
          newBalance = balance - amount

尽管刚开始使用 where 从句通常会有异样的感觉,但它对于提升可读性有着巨大的帮助。它使得读者的注意力首先能集中在表达式的一些重要的细节上,而之后再补上支持性的定义。经过一段时间以后,如果再用回那些没有 where 从句的语言,你就会怀念它的存在了。

与 let 表达式一样,where 从句中的空白和缩进也十分重要。 在下一节 “The offside rule and white space in an expression” 中我们会着重讲解其中的奥妙。

局部函数与全局变量

你可能已经注意到了,在 Haskell 的语法里,定义变量和定义函数的方式非常相似。这种相似性也存在于 let 和 where 区块里:定义局部函数就像定义局部变量那样简单。


In [45]:
-- file: ch03/LocalFunction.hs
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"

pluralise "apple" [0,1,2,3,4,5]


["no apples","one apple","2 apples","3 apples","4 apples","5 apples"]

我们定义了一个由多个等式构成的局部函数 plural。局部函数可以自由地使用其被封装在内的作用域内的任意变量:在本例中,我们使用了在外部函数 pluralise 中定义的变量 word。在 pluralise 的定义里,map 函数(我们将在下一章里再来讲解它的用法)将局部函数 plural 逐一应用于 counts 列表的每个元素。

我们也可以在代码的一开始就定义变量,语法和定义函数是一样的。


In [46]:
-- file: ch03/GlobalVariable.hs
itemName = "Weighted Companion Cube"

表达式里的缩进规则和空白字符

请看 lend 和 lend2 的定义表达式,左侧空出了一大块。这并不是随随便便写的,这些空白字符是有意义的。

Haskell 依据缩进来解析代码块。这种用排版来表达逻辑结构的方式通常被称作缩进规则。在源码文件开始的那一行,首个顶级声明或者定义可以从该行的任意一列开始,Haskell 编译器或解释器将记住这个缩进级别,并且随后出现的所有顶级声明也必须使用相同的缩进。

以下是一个顶级缩进规则的例子。第一个文件 GoodIndent.hs 执行正常。


In [47]:
-- file: ch03/GoodIndent.hs
-- 这里是最左侧一列

    -- 顶级声明可以从任一列开始
    firstGoodIndentation = 1

    -- 只要所有后续声明也这么做!
    secondGoodIndentation = 2

第二个文件 BadIndent.hs 没有遵守规则,因此也不能正常执行。


In [48]:
-- file: ch03/BadIndent.hs
-- 这里是最左侧一列

    -- 第一个声明从第 4 列开始
    firstBadIndentation = 1

-- 第二个声明从第 1 列开始,这样是非法的!
secondBadIndentation = 2

如果我们尝试在 ghci 里加载这两个文件,结果如下。

ghci> :load GoodIndent.hs
[1 of 1] Compiling Main             ( GoodIndent.hs, interpreted )
Ok, modules loaded: Main.
ghci> :load BadIndent.hs
[1 of 1] Compiling Main             ( BadIndent.hs, interpreted )

BadIndent.hs:8:2: parse error on input `secondBadIndentation'
Failed, modules loaded: none.

紧跟着的(译注:一个或多个)空白行将被视作当前行的延续,比当前行缩进更深的紧跟着的行也是如此。

let 表达式和 where 从句的规则与此类似。一旦 Haskell 编译器或解释器遇到一个 let 或 where 关键字,就会记住接下来第一个标记(token)的缩进位置。然后如果紧跟着的行是空白行或向右缩进更深,则被视作是前一行的延续。而如果其缩进和前一行相同,则被视作是同一区块内的新的一行。


In [49]:
-- file: ch03/Indentation.hs
foo = let firstDefinition = blah blah
          -- 只有注释的行被视作空白行
                  continuation blah

          -- 减少缩进,于是下面这行就变成了一行新定义
          secondDefinition = yada yada

                  continuation yada
      in whatever


Not in scope: ‘blah’


Not in scope: ‘blah’


Not in scope: ‘continuation’


Not in scope: ‘blah’


Not in scope: ‘yada’


Not in scope: ‘yada’


Not in scope: ‘continuation’


Not in scope: ‘yada’


Not in scope: ‘whatever’

下面的例子演示了如何嵌套使用 let 和 where。


In [50]:
-- file: ch03/letwhere.hs
bar = let b = 2
          c = True
      in let a = b
         in (a, c)
bar


(2,True)

变量 a 只在内部那个 let 表达式中可见。它对外部那个 let 是不可见的。如果我们在外部使用变量 a 就会得到一个编译错误。缩进为我们和编译器提供了视觉上的标识,让我们可以一眼就看出来作用域中包含哪些东西。


In [51]:
-- file: ch03/letwhere.hs
foo = x
    where x = y
            where y = 2
foo


2

于此类似,第一个 where 从句的作用域即是定义 foo 的表达式,而第二个 where 从句的作用域则是第一个 where 从句。

在 let 和 where 从句中妥善地使用缩进能够更好地展现代码的意图。

对制表符和空格说两句

在使用诸如 Emacs 这种能够识别 Haskell 代码的编辑器时,通常的默认配置是使用空格来表示缩进。如果你的编辑器不能识别 Haskell,建议您手工设置使用空格来表示缩进。

这么做的好处是可移植性。在一个使用等宽字体的编辑器里,类 Unix 系统和 Windows 系统对制表符的默认显示宽度并不一样,前者相当于 8 个字符宽度,而后者则相当于 4 个。这就意味着无论你自己认为制表符应该显示为多宽,你并不能保证别人的编辑器设置会尊重你的看法。使用制表符来表示缩进一定会在某些人的设置那里看起来一团糟。甚至还有可能导致编译错误,因为 Haskell 语言标准的实现是按照 Unix 风格制表符宽度来的。而使用空格则完全不用担心这个问题。

缩进规则并不是必需

我们也可以使用显式的语法结构来代替排版,从而表达代码的意图。例如,我们先写一个左大括号,然后写几个赋值等式,每个等式之间用分号分隔,最后加上一个右大括号。下面这个两个例子所表达的意图是一模一样的。


In [52]:
-- file: ch03/Braces.hs
bar = let a = 1
          b = 2
          c = 3
      in a + b + c

foo = let { a = 1;  b = 2;
        c = 3 }
      in a + b + c

当我们使用显式语法结构时,普通的排版不再起作用。就像在第二个例子中那样,我们可以随意的使用缩进而仍然保持正确。

显式语法结构可以用在任何地方替换普通排版。它对 where 从句乃至顶级声明也是有效的。但是请记住,尽管你可以这样用,但在 Hasekll 编程中几乎没有人会使用显式语法结构。

Case 表达式

函数定义并不是唯一我们能使用模式匹配的地方。case 结构使得我们还能在一个表达式内部使用模式匹配。如下面的例子所示,这个函数(定义在 Data.Maybe 里)能解构一个 Maybe 值,如果该值为 Nothing 还可以返回默认值。


In [53]:
-- file: ch03/Guard.hs
fromMaybe defval wrapped =
    case wrapped of
      Nothing     -> defval
      Just value  -> value
:t fromMaybe


fromMaybe :: forall t. t -> Maybe t -> t

case 关键字后面可以跟任意表达式,这个表达式的结果即是模式匹配的目标。of 关键字标识着表达式到此结束,以及匹配区块的开始,这个区块将用来定义每种模式及其对应的表达式。

该区块的每一项由三个部分组成:一个模式,接着一个箭头 ->,接着一个表达式;如果这个模式匹配中了,则计算并返回这个表达式的结果。这些表达式应当是同一类型的。第一个被匹配中的模式对应的表达式的计算结果即是 case 表达式的结果。匹配按自上而下的优先级进行。

如果使用通配符 _ 作为最后一个被匹配的模式,则意味着“如果上面的模式都没有被匹配中,就使用最后这个表达式”。如果所有模式匹配都没中,我们会看到前面章节里出现过的运行时错误。

新手在使用模式时常见的问题

在某些情况下,Haskell 新手会误解或误用模式。下面就是一些错误地使用模式匹配的例子。建议阅读时先自己想一想期望的结果是什么,然后看看实际的结果是否出乎你的意料。

错误地对变量进行匹配


In [54]:
-- file: ch03/BogusPattern.hs
data Fruit = Apple | Orange

apple = "apple"

orange = "orange"

whichFruit :: String -> Fruit

whichFruit f = case f of
                 apple  -> Apple
                 orange -> Orange

随意一瞥,这段代码的意思似乎是要检查 f 的值是 apple 还是 orange。

换一种写法可以让错误更加显而易见。(译注:原文的例子太晦涩,换了评论中一个较清楚的例子)


In [55]:
-- file: ch03/BogusPattern.hs
whichFruit2 :: String -> Fruit
whichFruit2 apple = Apple
whichFruit2 orange = Orange

这样写明白了吗?就是这里,显然 apple 并不是指在上层定义的那个变量名为 apple 的值,而是当前作用域的一个模式变量。

Note

不可拒绝的模式

我们把这种永远会成功匹配的模式称作不可拒绝。普通的变量名和通配符 _ 都属于不可拒绝的模式。

上面那个函数的正确写法应该如此:


In [56]:
-- file: ch03/BogusPattern.hs
betterFruit f = case f of
                  "apple"  -> Apple
                  "orange" -> Orange

我们针对字符串值 "apple" 和 "orange" 进行模式匹配,于是问题得到了解决。

进行了错误的相等比较

如果我们想比较一个 Tree 类型的两个节点值,如果相等就返回其中一个应该怎么做?请看下面这个尝试。


In [57]:
-- file: ch03/BadTree.hs
bad_nodesAreSame (Node a _ _) (Node a _ _) = Just a
bad_nodesAreSame _            _            = Nothing


Conflicting definitions for ‘a’
Bound at: :1:24
:1:37
In an equation for ‘IHaskell1135.bad_nodesAreSame’

一个命名在一组模式绑定中只能使用一次。将同一个变量放在多个位置并不意味着“这些变量应该相等”。要解决这类问题,我们需要使用 Haskell 的另一个重要的特性,守卫。

使用守卫实现条件求值

模式匹配针对的是值长成什么样子,因此有着诸多局限。除此之外,我们常常需要在对函数体求值之前进行各种各样的检查。Haskell 也为此提供了守卫这个特性。我们将通过改写上面这个用来比较树的两个节点是否相等的这段函数来讲解它的使用方法。


In [58]:
-- file: ch03/BadTree.hs
nodesAreSame (Node a _ _) (Node b _ _)
    | a == b     = Just a
nodesAreSame _ _ = Nothing

在上面的例子中,我们首先使用了模式匹配已确保值的形式是对的,然后使用了一个守卫来比较其中的一个部分。

一个模式后面可以跟着 0 个或多个 Bool 类型的守卫。我们引入了 | 这个符号来标识守卫的使用。即在它后面写一个守卫表达式,再写一个 = 符号(如果是 case 表达式则应该用 ->),然后写上在这个守卫表达式的值为真的情况下将被使用的函数体。如果某个模式匹配成功,那么它后面的守卫将按照顺序依次被求值。如果其中的某个守卫值为真,那么使用它所对应的函数体作为结果。如果所有的守卫值都为假,那么模式匹配继续进行,即尝试匹配下一个模式。

当对守卫表达式求值时,它所对应的模式中提到的所有变量都会被绑定,因此能够使用。

下面是一个用守卫重构过的 lend 方法。


In [59]:
-- file: ch03/Lending.hs
lend3 amount balance
     | amount <= 0            = Nothing
     | amount > reserve * 0.5 = Nothing
     | otherwise              = Just newBalance
    where reserve    = 100
          newBalance = balance - amount

otherwise 看上去像是守卫表达式的某种特殊语法,但实际上它只是一个被绑定为值 True 的普通变量,这样写是位了提高可读性。

任何使用模式的地方都可以使用守卫。用模式匹配和守卫把方法写成一组等式会大大提高可读性。还记得我们在条件求值<>_一节中定义的 myDrop 方法吗?


In [60]:
-- file: ch02/myDrop.hs
myDrop n xs = if n <= 0 || null xs
              then xs
              else myDrop (n - 1) (tail xs)

下面是用模式和守卫重构过的例子。


In [61]:
-- file: ch02/myDrop.hs
niceDrop n xs | n <= 0 = xs
niceDrop _ []          = []
niceDrop n (_:xs)      = niceDrop (n - 1) xs

这种格式的代码清楚的依次列举了我们对函数在不同情况下的不同用法的期望。如果使用 if 表达式,我们的想法很容易就被藏进某个函数里,而代码则会变得不可读。