In [1]:
:opt no-pager
"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
In [3]:
:t lines
In [4]:
lines "the quick\nbrown fox\njumps"
concat (lines "the quick\nbrown fox\njumps")
In [5]:
:t readFile
In [6]:
add a b = a + b
In [7]:
:t add
In [8]:
:info Integral
In [9]:
import Control.Monad
import Control.Concurrent
forM_ [1..5] $ \x -> do
print x
threadDelay $ 200 * 1000
In [10]:
:t forM_
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
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
In [16]:
isOdd n = mod n 2 == 1
In [17]:
:t isOdd
isOdd 2
isOdd 3
In [18]:
:t last
:t last [1,2,3]
:t last ['1','2','3']
In [19]:
1 :: Int
1 :: Integer
:t 1
In [20]:
take 2 [1,2,3,4,5]
:t take 2
:t take
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
In [23]:
CreditCard "2901650221064486" "Thomas Gradgrind" ["Dickens", "England"]
:type it
In [24]:
sumList :: Num a => [a] -> a
sumList (x:xs) = x + sumList xs
sumList [] = 0
sumList [1,2,3,4]
:t sumList
:t sum
In [25]:
import System.Time
getClockTime
toCalendarTime it
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
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)))
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
In [30]:
:t error
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]
In [33]:
head (mySecond [[9]])
In [34]:
mySecond []
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]
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]
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)。如果我们能使用,则说明在 作用域内,反之则说明在作用域外 。如果一个变量名在整个源代码的任意处都可以使用,则说明它位于最顶层的作用域。
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)
如上,内部的 x 隐藏了,或称作屏蔽(shadowing), 外部的 x。它们的变量名一样,但后者拥有完全不同的类型和值。
In [42]:
bar
我们同样也可以屏蔽一个函数的参数,并导致更加奇怪的结果。你认为下面这个函数的类型是什么?
In [43]:
-- file: ch03/NestedLets.hs
quux a = let a = "foo"
in a ++ "eek!"
:type quux
quux 1
在函数的内部,由于 let-绑定的变量名 a 屏蔽了函数的参数,使得参数 a 没有起到任何作用,因此该参数可以是任何类型的。
Tip
编译器警告是你的朋友
显然屏蔽会导致混乱和恶心的 bug,因此 GHC 设置了一个有用的选项 -fwarn-name-shadowing。如果你开启了这个功能,每当屏蔽某个变量名时,GHC 就会打印出一条警告。
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” 中我们会着重讲解其中的奥妙。
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]
我们定义了一个由多个等式构成的局部函数 plural。局部函数可以自由地使用其被封装在内的作用域内的任意变量:在本例中,我们使用了在外部函数 pluralise 中定义的变量 word。在 pluralise 的定义里,map 函数(我们将在下一章里再来讲解它的用法)将局部函数 plural 逐一应用于 counts 列表的每个元素。
我们也可以在代码的一开始就定义变量,语法和定义函数是一样的。
In [46]:
-- file: ch03/GlobalVariable.hs
itemName = "Weighted Companion Cube"
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
下面的例子演示了如何嵌套使用 let 和 where。
In [50]:
-- file: ch03/letwhere.hs
bar = let b = 2
c = True
in let a = b
in (a, c)
bar
变量 a 只在内部那个 let 表达式中可见。它对外部那个 let 是不可见的。如果我们在外部使用变量 a 就会得到一个编译错误。缩进为我们和编译器提供了视觉上的标识,让我们可以一眼就看出来作用域中包含哪些东西。
In [51]:
-- file: ch03/letwhere.hs
foo = x
where x = y
where y = 2
foo
于此类似,第一个 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
In [53]:
-- file: ch03/Guard.hs
fromMaybe defval wrapped =
case wrapped of
Nothing -> defval
Just value -> value
:t fromMaybe
case 关键字后面可以跟任意表达式,这个表达式的结果即是模式匹配的目标。of 关键字标识着表达式到此结束,以及匹配区块的开始,这个区块将用来定义每种模式及其对应的表达式。
该区块的每一项由三个部分组成:一个模式,接着一个箭头 ->,接着一个表达式;如果这个模式匹配中了,则计算并返回这个表达式的结果。这些表达式应当是同一类型的。第一个被匹配中的模式对应的表达式的计算结果即是 case 表达式的结果。匹配按自上而下的优先级进行。
如果使用通配符 _ 作为最后一个被匹配的模式,则意味着“如果上面的模式都没有被匹配中,就使用最后这个表达式”。如果所有模式匹配都没中,我们会看到前面章节里出现过的运行时错误。
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
In [57]:
-- file: ch03/BadTree.hs
bad_nodesAreSame (Node a _ _) (Node a _ _) = Just a
bad_nodesAreSame _ _ = Nothing
一个命名在一组模式绑定中只能使用一次。将同一个变量放在多个位置并不意味着“这些变量应该相等”。要解决这类问题,我们需要使用 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 表达式,我们的想法很容易就被藏进某个函数里,而代码则会变得不可读。