当前位置: 首页 > 工具软件 > Haskell > 使用案例 >

haskell入门教程[持续更新]

贾飞鸿
2023-12-01

1开始

本文章绝大多数内容来自书籍Learn You A Haskell For Great Good

1.0 开启运行环境

在终端输入 g h c i ghci ghci开始运行haskell程序。

1.1 第一个文件

创建文件 b a b y . h s baby.hs baby.hs,包含以下函数:

doubleMe x = x + x

doubleUs x y = x*2 + y*2

doubleSmallNumber x = if x > 100
                        then x 
                        else x*2

输入 ``` :l baby ``` 编译文件。
然后输入 ```haskell ghci> doubleMe 2 4 ghci> doubleUs 1 2 6 ghci> doubleSmallNumber 1 2 ```

1.2 列表

列表,用[ , , ,]表示。

lostNumbers = [4,8,15,16,23,42]

++: 可以作为列表的连接符
++ 连接两个列表,而:连接一个列表和一个字符

ghci> [1,2,3,4] ++ [9,10,11,12]  
[1,2,3,4,9,10,11,12]  
ghci> "hello" ++ " " ++ "world"  
"hello world"  
ghci> ['w','o'] ++ ['o','t']  
"woot"  

如果要按索引从列表中获取元素,使用!!

ghci> [1,2,3,4,5] !! 1
2

head接受一个列表,并返回头部

ghci> head [5,4,3,2,1]  
5

tail接受一个列表,并返回除去第一个元素的列表

ghci> tail [5,4,3,2,1]  
[4,3,2,1] 

last接受一个列表,并返回最后一个元素

ghci> last [5,4,3,2,1]  
1

init接受一个列表,并返回除去最后一个元素的列表

ghci> init [5,4,3,2,1]  
[5,4,3,2]

length接受一个列表,并返回它的长度

ghci> length [5,4,3,2,1]  
5

null接受一个列表,如果为空,则返回True,否则返回False

ghci> null [1,2,3]  
False  
ghci> null []  
True

reverse接受一个列表,返回翻转后的列表

ghci> reverse [5,4,3,2,1]  
[1,2,3,4,5] 

take接受一个数x以及一个列表,返回该列表前x个数

ghci> take 3 [5,4,3,2,1]  
[5,4,3]  
ghci> take 1 [3,9,3]  
[3]  
ghci> take 5 [1,2]  
[1,2]  
ghci> take 0 [6,6,6]  
[]

drop接受一个数x以及一个列表,返回去除前x个数的列表

ghci> drop 3 [8,4,2,1,5,6]  
[1,5,6]  
ghci> drop 0 [1,2,3,4]  
[1,2,3,4]  
ghci> drop 100 [1,2,3,4]  
[]  

maximunminimum接受一个列表,返回最大值/最小值

ghci> minimum [8,4,2,1,5,6]  
1  
ghci> maximum [1,9,2,3,4]  
9   

sum接受一个数字列表并返回它们的总和。

elem接受一个事物和一个事物列表,并告诉我们该事物是否是列表中的一个元素。

ghci> 4 `elem` [3,4,5,6]  
True  
ghci> 10 `elem` [3,4,5,6]  
False 

1.3 Range

样例:

ghci> [1..20]  
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]  
ghci> ['a'..'z']  
"abcdefghijklmnopqrstuvwxyz"  
ghci> ['K'..'Z']  
"KLMNOPQRSTUVWXYZ"  

等差数列Range

ghci> [2,4..20]  
[2,4,6,8,10,12,14,16,18,20]  
ghci> [3,6..20]  
[3,6,9,12,15,18]  

cycle接受一个列表并返回一个无限循环列表

ghci> take 10 (cycle [1,2,3])  
[1,2,3,1,2,3,1,2,3,1]  
ghci> take 12 (cycle "LOL ")  
"LOL LOL LOL "

1.4 用集合语言表示列表

如果我们想要得到集合 S = { 2 ⋅ x ∣ x ∈ N , x ≤ 10 } S=\{2 \cdot x|x\in N,x\le 10\} S={2xxN,x10},我们可以用haskell这样表示:

ghci> [x*2 | x <- [1..10]]  
[2,4,6,8,10,12,14,16,18,20] 

一个简易的计算列表长度的函数:

length' xs = sum[1 | _ <- xs]

1.5 元组

与列表不同,元组可以包含多种类型的组合。元组用 ( ) () ()表示,它的组成部分用逗号分隔。

fst接受一个pair(双元素元组)并返回其第一个元素。

ghci> fst (8,11)  
8  
ghci> fst ("Wow", False)  
"Wow" 

snd接受一个pair并返回它的第二个元素。

ghci> snd (8,11)  
11  
ghci> snd ("Wow", False)  
False  

zip需要两个列表,然后通过将匹配的元素成对连接,将它们压缩到一个列表中。

ghci> zip [1,2,3,4,5] [5,5,5,5,5]  
[(1,5),(2,5),(3,5),(4,5),(5,5)]  
ghci> zip [1 .. 5] ["one", "two", "three", "four", "five"]  
[(1,"one"),(2,"two"),(3,"three"),(4,"four"),(5,"five")]  

##1.6 一个简单的例子
我们想要找出边长为整数,周长为24的直角三角形。

ghci> let rightTriangles' = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2, a+b+c == 24]  
ghci> rightTriangles'  
[(6,8,10)] 

2 类型

2.1 类型检查

使用:t可以检查元素的类型。

ghci> :t 'a'  
'a' :: Char  
ghci> :t True  
True :: Bool  
ghci> :t "HELLO!"  
"HELLO!" :: [Char]  
ghci> :t (True, 'a')  
(True, 'a') :: (Bool, Char)  
ghci> :t 4 == 5  
4 == 5 :: Bool  

2.2函数类型

函数也有类型。在编写我们自己的函数时,我们可以选择给它们一个显式类型声明。

removeNonUppercase  ::  [ Char ]  ->  [ Char ]  
removeNonUppercase st  =  [ c  | c  <-  st, c  `elem`  [ 'A' .. 'Z' ]] 

removeNonUppercase函数获取Char列表,并除去非大写字母。

addThree  ::  Int  ->  Int  ->  Int  ->  Int  
addThree x y z  =  x  +  y  +  z  

addThree接收三个Int元素,并返回一个Int元素。

注意:IntInteger都表示整数,Integer的范围更大。此外还有FloatDoubleBoolChar等原始数据。

2.3 用:t获取函数的类型

ghci> :t (==)  
(==) :: (Eq a) => a -> a -> Bool  

这里的==是一个函数。
=>符号之前的所有内容都称为类约束。
Eq用于支持相等测试的类型
Ord用于具有排序的类型。
Show把元素转换成字符串。
这些都是函数类。

ghci> show 3  
"3"  
ghci> show 5.334  
"5.334"  
ghci> show True  
"True" 

Read把字符串转换为元素。

ghci> read "True" || False  
True  
ghci> read "8.2" + 3.8  
12.0  
ghci> read "5" - 2  
3  
ghci> read "[1,2,3,4]" ++ [3]  
[1,2,3,4,3] 

3 函数模式匹配

3.1 例子

sayMe :: (Integral a) => a -> String  
sayMe 1 = "One!"  
sayMe 2 = "Two!"  
sayMe 3 = "Three!"  
sayMe 4 = "Four!"  
sayMe 5 = "Five!"  
sayMe x = "Not between 1 and 5"

当我们输入sayMe 1的时候,就会输出One!。当我们输入sayMe 1的时候,就会输出Two!。以此类推。当我们输出的数不是1,2,3,4,5时,就会输出Not between 1 and 5


再来看下一个例子:

head' :: [a] -> a  
head' [] = error "Can't call head on an empty list, dummy!"  
head' (x:_) = x 

head'方法返回一个列表的首元素。_是通配符,表示匹配任意个任意元素。

length' :: (Num b) => [a] -> b  
length' [] = 0  
length' (_:xs) = 1 + length' xs 

该函数使用递归的写法,返回列表的长度。

sum' :: (Num a) => [a] -> a  
sum' [] = 0  
sum' (x:xs) = x + sum' xs  

该函数使用递归的写法,返回列表元素的和。

3.2 条件分支语句

使用Guard可以代替if-else语句。

luckySum :: (Integral a) => a -> a -> a -> String

luckySum x y z
    | x+y+z == 21 = "lucky total 21"
    | x+y+z > 10 && x+y+z < 21 = "getting closer"
    | otherwise = "not that lucky"
--等同于if-elseif-else

Guard实现比较函数

myCompare :: (Ord a) => a -> a -> Ordering
myCompare a b
    | a > b     = GT
    | a == b    = EQ
    | otherwise = LT
ghci> 2 `myCompare` 3
LT

将函数用反引号``括起来可以使用中缀表达式。

使用case同样可以创造分支语句。

head' :: [a] -> a  
head' [] = error "No head for empty lists!"  
head' (x:_) = x 
-- 相同
head' :: [a] -> a  
head' xs = case xs of [] -> error "No head for empty lists!"  
                      (x:_) -> x 

case的表达式长这样:

case expression of pattern -> result  
                   pattern -> result  
                   pattern -> result  
                   ... 

case不同于Guard,可以局部使用。

describeList :: [a] -> String  
describeList xs = "The list is " ++ case xs of [] -> "empty."  
                                               [x] -> "a singleton list."   
                                               xs -> "a longer list." 

3.3 定义局部变量

使用where可以定义变量并给变量赋值。

luckySum :: Int -> Int -> Int -> String

luckySum x y z
    | sum == best = "lucky total " ++ show best
    | sum > lowLimit && sum < best = "getting closer"
    | otherwise = "not that lucky"
    where sum = x+y+z
          best = 21
          lowLimit = 10

使用let也可以定义变量,但是有作用范围。

cylinder :: (RealFloat a) => a -> a -> a  
cylinder r h = 
    let sideArea = 2 * pi * r * h  
        topArea = pi * r ^2  
    in  sideArea + 2 * topArea

let的使用方式是let <bindings> in <expression>

let还可以使用类似c语言的宏定义,where也可。

ghci> [let square x = x * x in (square 5, square 3, square 2)]  
[(25,9,4)]  

4 递归

一个用递归实现的查找列表最大值函数。

maximum' :: (Ord a) => [a] -> a  
maximum' [] = error "maximum of empty list"  
maximum' [x] = x  
maximum' (x:xs) = max x (maximum' xs)  

快速排序的haskell实现。

quicksort :: (Ord a) => [a] -> [a]  
quicksort [] = []  
quicksort (x:xs) =   
    let smallerSorted = quicksort [a | a <- xs, a <= x]  
        biggerSorted = quicksort [a | a <- xs, a > x]  
    in  smallerSorted ++ [x] ++ biggerSorted  

是不是非常简洁呢~
使用递归函数时,先定义终止条件,再定义子结构。
由于递归在之前学习算法的时候被用到过很多次,所以这里就不再赘述了。

5 高阶函数

5.1 Curried 函数

让我们首先看一个例子。

ghci> max 4 5  
5  
ghci> (max 4) 5  
5 

这里,max a b返回两者中的最大值。我们也可以把(max 4) 看做一个函数,它接受一个数,并返回该数和4的最大值。

函数max :: (Ord a) => a -> a -> a可以看做一个函数,它接受一个数,并返回一个函数f。这个函数f接受一个数,并返回一个数。所以这个表达式意义和max :: (Ord a) => a -> (a -> a)相同。

某些中缀函数可以写成部分形式。

divideByTen :: (Floating a) => a -> a
divideByTen = (/10)  
isUpperAlphanum :: Char -> Bool  
isUpperAlphanum = (`elem` ['A'..'Z'])

5.2 将函数作为参数

函数可以将函数作为参数,也可以返回函数

applyTwice :: (a -> a) -> a -> a  
applyTwice f x = f (f x) 

该函数第一个参数是一个函数,第二个参数是相同的a。它能将函数f应用两次。

ghci> applyTwice (+3) 10
16
ghci> applyTwice (++ " HAHA") "HEY"
"HEY HAHA HAHA"
ghci> applyTwice (3:) [1]  
[3,3,1] 

我们将实现另一个已经在标准库中的函数,称为flipflip 简单地接受一个函数并返回一个函数。

flip' :: (a -> b -> c) -> (b -> a -> c)  
flip' f = g  
    where g x y = f y x 
-- (a -> b -> c) -> (b -> a -> c)等价于 (a -> b -> c) -> (b -> a -> c),所以这样写也可以:
flip' :: (a -> b -> c) -> b -> a -> c  
flip' f y x = f x y
ghci> flip' zip [1,2,3,4,5] "hello"  
[('h',1),('e',2),('l',3),('l',4),('o',5)]  

map接受一个函数和一个列表,并将该函数应用于列表中的每个元素,生成一个新列表。

map :: (a -> b) -> [a] -> [b]  
map _ [] = []  
map f (x:xs) = f x : map f xs 

filter 接受一个返回bool的函数和一个列表,并将该函数应用于列表中每个元素,当值为False则被筛出。

filter :: (a -> Bool) -> [a] -> [a]  
filter _ [] = []  
filter p (x:xs)   
    | p x       = x : filter p xs  
    | otherwise = filter p xs 
ghci> filter (>3) [1,5,3,2,1,6,4,3,2,1]  
[5,6,4]  
ghci> filter (==3) [1,2,3,4,5]  
[3] 

利用filter可以重写quicksort函数。

quicksort :: (Ord a) => [a] -> [a]    
quicksort [] = []    
quicksort (x:xs) =     
    let smallerSorted = quicksort (filter (<=x) xs)  
        biggerSorted = quicksort (filter (>x) xs)   
    in  smallerSorted ++ [x] ++ biggerSorted  

利用filter找到1~100000的整数中被3829整除的最大的数。

largestDivisible :: (Integral a) => a
largestDivisible = head (filter p [100000,99999..])  
    where p x = x `mod` 3829 == 0 

takeWhile函数接受一个bool函数以及一个列表。takeWhile对列表中每个元素进行函数操作,当返回True时加入列表,当返回False时停止。
eg:计算小于10000的奇数的平方和

ghci> sum (takewhile (<10000) (filter odd (map(^2) [1..])))

用集合语言表示,即为

ghci> sum( takeWhile (<10000) [n^2 | n <- [1..], odd (n^2)])

5.3 Lambda函数

我们使用\表示匿名函数,后面些参数,用空格分隔。之后是->和函数体。
一个简单的Lambda函数例子:

ghci> map (\(a,b) -> a + b) [(1,2),(3,5),(6,3),(2,6),(2,5)]  
[3,8,9,8,7]   

这里,(\(a,b) -> a + b)就是一个匿名函数。它把一个Pair的两个值相加,然后返回。

使用匿名函数同样可以实现flip

flip' :: (a -> b -> c) -> b -> a -> c  
flip' f = \x y -> f y x  

这里的\x y -> f y x是一个Lambda函数。它接受两个参数,并返回一个函数。

foldl函数接受一个[二元函数]、一个[初始值]、一个[要折叠的列表]。[二元函数]本身有两个参数。使用[累加器]和第一个(或最后一个)元素调用[二元函数],并生成一个新的[累加器]。然后,使用新的[累加器]和现在新的第一个(或最后一个)元素再次调用[二元函数],依此类推。一旦我们遍历了整个列表,就只剩下[累加器]了,这就是我们将列表简化为的内容。

sum' :: ( Num a) => [a] -> a
sum' xs = foldl(\acc x -> acc + x) 0 xs
ghci> sum' [3, 5, 2, 1]
11

foldr函数和foldl函数相似。只不过是从列表的最右边开始,并且累加器放在右边。

map' :: (a -> b) -> [a] -> [b]
map' f xs = foldr (\x acc -> f x: acc) [] xs

这里空列表[]作为初始值。每次将得到的x连接到累加器acc左边。

5.4 一些特殊符号

符号$具有[右结合性],相当于括号。这个符号优先级很低,能够减少工作量。

sqrt (3 + 4 + 9)
--equals
sqrt $ 3 + 4 + 9

sum( filter (>10) (map (*2) [2..10]))
--equals
sum $ filter (>10) $ map (*2) [2..10]

读这类式子时,只需要记住$的优先级最低,最后读即可。

复合函数
符号.相当于复合函数的连接符。
f(g(z x))相当于(f.g.z) x

6 模组

Haskell中的模组就是java中的包,c++中的头文件。我们用import <module name>导包。

import Data.List  
  
numUniques :: (Eq a) => [a] -> Int  
numUniques = length . nub 

我们可以通过在包后面加括号()实现导入部分功能。

import Data.List (nub, sort)

我们可以利用hiding选择不导入某些功能。

import Data.List hiding (nub)
--不导入nub

当我们使用一个有[命名冲突]的函数时(比如我们导入了Data.Map包,然后想使用filter函数。由于有多个filter函数,故产生了冲突。)这个时候我们可以用qualified来制定我们用哪个包。

import qualified Data.Map as M 

as可以给包起别名,从而简化操作

6.1 Data包

本词条省略。书上有。懒得写了/

6.2 制作自己的模组

module Geometry  
( sphereVolume  
, sphereArea  
, cubeVolume  
, cubeArea  
, cuboidArea  
, cuboidVolume  
) where  
  
sphereVolume :: Float -> Float  
sphereVolume radius = (4.0 / 3.0) * pi * (radius ^ 3)  
  
sphereArea :: Float -> Float  
sphereArea radius = 4 * pi * (radius ^ 2)  
  
cubeVolume :: Float -> Float  
cubeVolume side = cuboidVolume side side side  
  
cubeArea :: Float -> Float  
cubeArea side = cuboidArea side side side  
  
cuboidVolume :: Float -> Float -> Float -> Float  
cuboidVolume a b c = rectangleArea a b * c  
  
cuboidArea :: Float -> Float -> Float -> Float  
cuboidArea a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2  
  
rectangleArea :: Float -> Float -> Float  
rectangleArea a b = a * b 

构造一个集合模组

在程序中通过import Geometry,以及再控制台中通过:m Geometry导入。

7 数据

7.1 自定义数据类型

通过data关键字,我们可以自定义数据类。

data Bool = False | True
data Int = -2147483648 | -2147483647 | ... | -1 | 0 | 1 | 2 | ... | 2147483647   -

|代表或。用于声明数据的范围,或者枚举类型。

data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday
data Shape = Circle Float Float Float | Rectangle Float Float Float Float

这种方式声明了两个类:CircleRectangle。它们的类型如下:

ghci> :t Circle  
Circle :: Float -> Float -> Float -> Shape  
ghci> :t Rectangle  
Rectangle :: Float -> Float -> Float -> Float -> Shape 

利用Shape类及其子类,我们可以写出计算其面积的函数。

surface :: Shape -> Float  
surface (Circle _ _ r) = pi * r ^ 2  
surface (Rectangle x1 y1 x2 y2) = (abs $ x2 - x1) * (abs $ y2 - y1) 

(Circle _ _ r)Circle的构造函数。

ghci> let circle1 = Circle 1 1 3
ghci> let rec1 = Rectangle 1 1 3 3
ghci> surface circle1
28.274334

我们可以在类定义后面加上deriving (XXX),表示作为XXX的子类。

data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show) 

我们可以定义Point类,来简化Shape类。

data Point = Point Float Float deriving (Show)  
data Shape = Circle Point Float | Rectangle Point Point deriving (Show)

这时,它们的构造函数应该这样写:

ghci> (Circle (Point 1 1) 1)

我们可以在模组中加入自定义数据类型。

module Shapes
(Point(..)
,Shape(..)
,surface    
)where
--

(..)表示其为一个数据类型。换句话说,就是结构体。

7.2 Record语法

data Person = Person String String Int Float String String deriving (Show)  
--equals
data Person = Person { firstName :: String  
                     , lastName :: String  
                     , age :: Int  
                     , height :: Float  
                     , phoneNumber :: String  
                     , flavor :: String  
                     } deriving (Show) 

这样,我们就能通过成员变量名访问成员变量的值。

ghci> let guy = Person "Buddy" "Finklestein" 43 184.2 "526-2928" "Chocolate"  
ghci> firstName guy  
"Buddy"  
ghci> height guy  
184.2  
ghci> flavor guy  
"Chocolate"  

并且通过成员变量名赋值。

data Car = Car {company :: String, model :: String, year :: Int} deriving (Show) 
ghci> Car {company="Ford", model="Mustang", year=1967}  
Car {company = "Ford", model = "Mustang", year = 1967} 

7.3 派生类

我们可以使用deriving(a,b,c,...)继承多个类。

data Person = Person { firstName :: String  
                     , lastName :: String  
                     , age :: Int  
                     } deriving (Eq, Show, Read)  

这样,我们便能对Person实例进行readshow操作。注意,进行read操作时需要指明转换类型。通过::表示。

ghci> let mikeD = Person {firstName = "Michael", lastName = "Diamond", age = 43}  
ghci> mikeD  
Person {firstName = "Michael", lastName = "Diamond", age = 43}

ghci> "mikeD is: " ++ show mikeD  
"mikeD is: Person {firstName = \"Michael\", lastName = \"Diamond\", age = 43}" 
  
ghci> read "Person {firstName =\"Michael\", lastName =\"Diamond\", age = 43}" :: Person  
Person {firstName = "Michael", lastName = "Diamond", age = 43}  

7.4 别名

我们可以通过type为类型起别名,就像c++中的typedef

type AssocList k v = [(k,v)]
type PhoneBook = [(String,String)]
type PhoneNumber = String  
type Name = String  
type PhoneBook = [(Name,PhoneNumber)]

7.5 递归数据结构

简单二叉搜索树递归定义与插入、查找:

-- 定义
data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)
-- 创建实例
singleton :: a -> Tree a  
singleton x = Node x EmptyTree EmptyTree 
-- 插入
treeInsert :: (Ord a) => a -> Tree a -> Tree a  
treeInsert x EmptyTree = singleton x  
treeInsert x (Node a left right)   
    | x == a = Node x left right  
    | x < a  = Node a (treeInsert x left) right  
    | x > a  = Node a left (treeInsert x right)
-- 查找
treeElem :: (Ord a) => a -> Tree a -> Bool  
treeElem x EmptyTree = False  
treeElem x (Node a left right)  
    | x == a = True  
    | x < a  = treeElem x left  
    | x > a  = treeElem x right  

EmptyTree是一个Tree类。它只有一个名字,没有任何成员函数和成员变量。

ghci> :t EmptyTree
Tree a

7.6 创造特定类的实例

我们可以通过instance创造一个类的实例。这个实例是一个函数。

data TrafficLight = Red | Yellow | Green
instance Eq TrafficLight where  
    Red == Red = True  
    Green == Green = True  
    Yellow == Yellow = True  
    _ == _ = False 

上述代码相当于重写TrafficLight中的equals
类似的,我们能够重写toString(即Show)

instance Show TrafficLight where  
    show Red = "Red light"  
    show Yellow = "Yellow light"  
    show Green = "Green light"
ghci> Red == Red  
True  
ghci> Red == Yellow  
False  
ghci> Red `elem` [Red, Yellow, Green]  
True

如果要重写该数据的==,我们可以这样写:

data BicycleType = ElectriBicycle | NormalBicycle deriving(Show, Eq, Read)
type BicycleNumber = Integer
type BicycleName = String
data Bicycle =  Bicycle {
    bicycleName :: BicycleName,
    bicycleNumber :: BicycleNumber,
    bicycleType :: BicycleType
} deriving(Show)

instance Eq Bicycle where
    x == y = ((bicycleName x) == (bicycleName y) && (bicycleType x) == (bicycleType y))

对于Bicycle类,两个实例相等只需要bicycleNamebicycleType相等即可。

7.7 函数类

我们利用class创造一个函数类,然后用instance实现实例函数。

-- 将一个元素转换为bool
class YesNo a where  
    yesno :: a -> Bool 
-- 数值
instance YesNo Int where  
yesno 0 = False  
yesno _ = True
-- 列表
instance YesNo [a] where  
    yesno [] = False  
    yesno _ = True 

class的语法为class <classname> <parameter> where ...

Functor类可以对map进行改进,实现多态。它的抽象类接受一个函数,并把它应用于元素。

class Functor f where  
    fmap :: (a -> b) -> f a -> f b
instance Functor Tree where  
    fmap f EmptyTree = EmptyTree  
    fmap f (Node x leftsub rightsub) = Node (f x) (fmap f leftsub) (fmap f rightsub)

这里的[]Tree是构造函数。

ghci> fmap (*2) EmptyTree  
EmptyTree  
ghci> fmap (*4) (foldr treeInsert EmptyTree [5,7,3,2,1,7])  
Node 28 (Node 4 EmptyTree (Node 8 EmptyTree (Node 12 EmptyTree (Node 20 EmptyTree EmptyTree))))

如果直接对EmptyTree使用map (*2),将会报错。而fmap将不会。

8 IO

8.1 main

对于初学者来说,最简单的程序应该是"Hello, world"。

main = putStrLn "hello, world"

putStrLn用于在控制台输出字符串。

main = do  
    putStrLn "Hello, what's your name?"  
    name <- getLine  
    putStrLn ("Hey " ++ name ++ ", you rock!") 

getLine用于在控制台读取一行字符串。其值被复制到name中。由于getLine是一个IO操作,故要用<-赋值。
在有d多个操作的地方(这些操作应包括IO操作),应该用do加以修饰。
在haskell中,main函数可以递归调用。
下述为重复读取一行字符串,直到字符串为空,返回一个空元组()

main = do   
    line <- getLine  
    if null line  
        then return ()  
        else do  
            putStrLn $ reverseWords line  
            main  
  
reverseWords :: String -> String  
reverseWords = unwords . map reverse . words 

在haskell中,return并不会中断整个程序,而是继续运行下去。

类似c语言,haskell也有printgetChar。一个用于输出到终端,一个用于从终端读入。.

8.2 文件和流

通过getContents可以读取文件中所有内容。

建立一个文本文件,并把它输出。
建立文本文件haiku.txt

I'm a lil' teapot  
What's with that airplane food, huh?  
It's so small, tasteless 

建立读取文件capslocker.hs

import Control.Monad  
import Data.Char  
  
main = forever $ do  
    putStr "Give me some input: "  
    l <- getLine  
    putStrLn $ map toUpper l 

在控制台编译并输出

$ ghc --make capslocker   
[1 of 1] Compiling Main             ( capslocker.hs, capslocker.o )  
Linking capslocker ...  
$ cat haiku.txt  
I'm a lil' teapot  
What's with that airplane food, huh?  
It's so small, tasteless  
$ cat haiku.txt | ./capslocker  
I'M A LIL' TEAPOT  
WHAT'S WITH THAT AIRPLANE FOOD, HUH?  
IT'S SO SMALL, TASTELESS  
capslocker <stdin>: hGetLine: end of file 

通过管道运算符|可以就应用该应用程序。

通过调用openFile函数可以直接在应用程序中打开文件。运用这个函数需要导入System.IO包。

import System.IO  
  
main = do  
    handle <- openFile "girlfriend.txt" ReadMode  
    contents <- hGetContents handle  
    putStr contents  
    hClose handle  

openFile的函数签名:openFile :: FilePath -> IOMode -> IO Handle
IOMode是一个枚举类型,它包含ReadMode | WriteMode | AppendMode | ReadWriteMode
利用函数hGetContents,我们可以从openFile中得到的IO handle来获取文件的内容。它返回一个字符串。
类似的,还有hGetLinehPutStrhPutStrLnhGetChar等操作。

readFile也可以用来读取文件。

import System.IO  
  
main = do  
    contents <- readFile "girlfriend.txt"  
    putStr contents  

haskell提供了writeFile :: FilePath -> String -> IO ()来写入文件。

import System.IO     
import Data.Char  
    
main = do     
    contents <- readFile "girlfriend.txt"     
    writeFile "girlfriendcaps.txt" (map toUpper contents) 
 类似资料: