{-
2017.02.21-22 《Haskell趣學指南 —— Learning You a Haskell for Great Good!》
學習了Haskell的基本語法,並實現了一些模塊和程序,學習的時候,應該注意GHCI模式和文本模式時的一些語法的差異
[官網](http://learnyouahaskell.com)
shell > ghc xxx.hs # 編譯文件
shell > runhaskell xxx.hs # 直接運行hs文件
shell > ghci # 進入Haskell解釋器
ghci > :l test.hs -- 載入test.hs文件
ghci > :r -- 重新載入文件
注意
1. ghci中定義變量需要使用let關鍵字,而hs文件中不需要
2. ghci中,冒號(:)開頭標示命令,類似於vi。如 :? 表示查找助,:l 表示加載文件。
-}
----------------------------------------------------------------------------------------
-- ch00 HelloHaskell
import System.IO
main = do
putStr $ "Hello," -- Hello,
putStrLn $ "Haskell" -- Haskell\n
print $ "Hello,Haskell" -- "Hello,Haskell"
----------------------------------------------------------------------------------------
-- ch01 Introduction
doubleMe x = x + x -- 前綴函數: doubleMe 3
doubleUs x y = x*2 + y*2 -- 中綴函數: doubleUs 3 4 或 3 `doubleUs` 4
-- Haskell中程序是一系列的函數的集合,函數取數據作為參數,並將它們轉為想要的結果。
-- 每個函數都會返回一個結果,可以作為其他函數的子結構。
-- Hasekill中if返回的是表達式(expression)不是語句(statement)。
operatedIfSmall x = (if x>=50 then x else x*2) + 1
-- 1.3 introduction to list
-- 列表list是一種單類型的數據結構,用來存儲同構元素
let vec = [1] ++ 2:[3,4,5] ++ 6:[7,8,9] ++ [10] --語法糖
print vec -- [1,2,3,4,5,6,7,8,9,10]
let mat = [[1,2,3],[4,5,6],[7,8,9]] --嵌套列表
mat!!2!!2 -- 使用雙感嘆號來索引列表中的元素,注意從0開始
let vec = [1,2,3,4,5]
vec!!0 -- 1,訪問列表中的元素
vec!!2 -- 3
head vec -- 1
tail vec -- [2,3,4,5]
init vec -- [1,2,3,4]
last vec -- 5
length vec -- 5
null vec -- False
null [] -- True
reserve vec -- [5,4,3,2,1]
take 3 vec -- [1,2,3]
drop 3 vec -- [4,5]
maximum vec -- 5
minimum vec -- 1
sum vec -- 15
product vec -- 120
3 `elem` vec -- True
13 `elem` vec -- False
-- 生成列表區間
print [1..10] -- [1,2,3,4,5,6,7,8,9,10]
print [2,4..10] -- [2,4,6,8,10]
-- 生成無限列表
take 10 (cycle [1,2,3,4])
take 10 (repeat 5)
replicate 10 5
-- 列表推導 list compression
print [x*2 | x<- [1..10]] --[2,4,6,8,10,12,14,16,18,20]
-- 增加謂詞(predicate),過濾(filter)
print [x*2 | x<- [1..10], x*2>10] --[12,14,16,18,20]
-- 判斷一個數是否為奇數,奇數返回True,偶數返回False。
let boomBangs xs = [ if x < 6 then "BOOM!" else "BANG!" | x <- xs, odd x]
boomBangs [1..10]
-- 對兩個列表組合,使用逗號隔開的多個謂詞
print [x*y| x<-[3,4,5],y<-[8,9,10]] -- [24,27,30,32,36,40,40,45,50]
print [x*y| x<-[3,4,5],y<-[8,9,10],x*y>28, x* y<45] -- [30,32,36,40,40]
-- 使用一組名詞和一組形容詞構成列表推導式子
let nouns = ["hobo","frog","pope"]
let adjectives = ["lazy","grouchy","scheming"]
[adjective ++ " " ++ noun | adjective <- adjectives, noun <- nouns]
-- ["lazy hobo","lazy frog","lazy pope","grouchy hobo","grouchy frog",
-- "grouchy pope","scheming hobo","scheming frog","scheming pope"]
-- 自定義length函數,下划線(_)表示無關緊要的臨時變量
let mylength xs = sum [1| _ <- xs]
mylength [2,4..10]
-- 去除字符串中非大寫字母
let removeNonUppercase xs = [c |c<-xs, c `elem` ['A'..'Z']]
removeNonUppercase "Hello,World!" -- "HW"
-- 嵌套處理嵌套列表(列表的列表)
let xxs = [[1,2,3,4],[5,6,7,8]]
let myevens xxs = [[x|x<-xs, even x] | xs <- xxs]
myevens xxs
-- 元組tuple,又稱有序對pair,允許存儲多個異構的值
let tt =(1,2.0,'3',"four")
-- 使用元組的列表存儲坐標
let pts = [(x,y)|x<-[1..5],y<-[5,4..1]]
pts!!3 -- (4,1)
fst (pts!!3) -- 4
snd (pts!!3) -- 1
-- zip交叉配對
let tbs = zip [1..] ["one","two","three","four","five"]
print tbs
-- 查找邊長是整數且周長為24的直角三角形
let rightTriangle24 = [(a,b,c)|c<-[1..(24 `div` 2)],b<-[1..c],a<-[1..b],a^2+b^2==c^2, a+b+c==24]
rightTriangle24
----------------------------------------------------------------------------------------
-- ch02 type reference
{-
Haskell常見類型有 Bool,Int,Float,Double,Num,Char,[],()等,凡是類型,首字母必須大寫。
使用小寫字母表示的類型變量,表示可以是任意類型。
:type True -- True :: Bool
:type fst -- fst :: (a, b) -> a
類型類定義行為的抽象接口(類似於純虛基類),如果某個類型是某個類型類的實例(instance),那么它必須實現該類型所描述的行為。
具體來說,類型類就是一組函數集合,如果某類型實現某類型類的實例,就需要為這一類類型提供這些函數的實現。
:type (==) -- (==) :: (Eq a) => a -> a -> Bool
常見類型類有Eq等性測試、Ord比較大小、Show轉化為字符串、Read從字符串讀取參數並轉為為種類型、Enum連續枚舉、Bounded上下界限、Num數值類、Floating浮點類、Integeral實數和整數。一個類型可以有多個類型類實例,一個類型類可以包含多個類型作為實例。有時候某些類型是另外類型的先決條件(prerequisite),如Eq是Ord實例的先決條件。
Eq:必須定義了==和/=兩個函數
Ord:包含了所有標准類型的比較函數,如<,>,<=,>=等。compare函數取兩個Ord中的相同類型的值作為參數,返回一個Ordering類型的值(有GT、LT、EQ三種值)。
Show:常見的是show函數
Read:常見的是read函數
Enum:連續枚舉,每個值都有前驅predecessor和后繼successer,可以分別通過pred和succ函數得到。
Bounded:反應的是實例類型的上下限,通過maxBound和minBound獲得,這兩個函數類型都是多態函數,原型為 (Bounded a)=>a。
-}
let tt =(True,1,2.0,'3',"four")
:type tt -- tt :: (Fractional t1, Num t) => (Bool, t, t1, Char, [Char])
let tf = (head,tail,init,last)
:type tf -- tf :: ([a] -> a, [a1] -> [a1], [a2] -> [a2], [a3] -> a3)
:t (==) -- (==) :: Eq a => a -> a -> Bool
5 == 5 -- True
5 /= 5 -- False
:t (>) -- (>) :: Ord a => a -> a -> Bool
5 > 5 -- False
5 `compare` 5 -- EQ
:t show -- show :: Show a => a -> String
show 3.14 -- "3.14"
:t read -- read :: Read a => String -> a
read "True" || False -- True
[read "True",False] -- [True, False]
(read "3.14"::Double) + 2.4 -- 5.54
read "[1,2,3,4,5]" ++ [6] -- [1,2,3,4,5,6]
(read "[1,2,3,4,5]"::[Int]) ++[6] -- [1,2,3,4,5,6]
[1..5] --[1,2,3,4,5]
[LT ..GT] --[LT,EQ,GT]
succ 'D' -- 'E'
pred 'D' -- 'C'
minBound::(Bool,Int,Char) -- (False,-9223372036854775808,'\NUL')
maxBound::(Bool,Int,Char) -- (True,9223372036854775807,'\1114111')
:t fromIntegral -- fromIntegral :: (Integral a, Num b) => a -> b
length [x|x<-[1..100],x `mod` 2 == 0, x `mod` 3 == 0] --16
fromIntegral (length [x|x<-[1..100],x `mod` 2 == 0, x `mod` 3 == 0]) + 3.14 --19.14
-- ch03 函數語法
-- 模式匹配(pattern matching)是通過檢查數據是否符合特定的結構來判斷是否匹配,並從模式中解析數據。
-- 在模式中給出小寫字母的名字而不是具體的值,那么就是一個萬能模式(catchal pattern),它總能匹配輸入參數,並將其綁定到模式中的名字供我們引用。
-- 模式用來檢查參數的結構是否匹配,而哨兵(guard)用來檢查參數的性質時候威震,類似於if條件判斷語句。
-- 萬能匹配模式,替代if-then-else決策樹
-- 辨別1-4並輸出相應的單詞,否則令做處理
sayMe :: Int -> String
sayMe 1 = "One"
sayMe 2 = "Two"
sayMe 3 = "Three"
sayMe 4 = "Four"
sayMe x = "Not between 1 and 4"
-- print [sayMe x| x<- [1..5]] -- ["One","Two","Three","Four","Not between 1 and 4"]
-- 階乘函數 product[1..n]
factorial :: Int -> Int
factorial 0 = 1
factorial n = factorial(n-1) * n
-- factorial 10 -- 3628800
-- 元組的模式匹配
addTuples :: (Double,Double) -> (Double,Double) -> (Double,Double)
addTuples (x1,y1) (x2,y2) = (x1+x2,y1+y2)
-- addTuples (1,2) (3,4) -- (4.0,6.0)
-- 獲取三元組第三個元素
thirdItem :: (a,b,c) -> c
thirdItem (_,_,z) = z
-- thirdItem (1,2,3) -- 3
-- 列表推導模式匹配,使用運行時錯誤來指明錯誤
myfirst :: [a] -> a
myfirst [] = error "Cannot call myfirst on an empty list, dummy!"
myfirst (x:_) = x
-- 列表匹配,綁定多個變量
tell :: (Show a) => [a] -> String
tell [] = "The list is empty"
tell (x:[]) = "The list has one element: " ++ show x
tell (x:y:[]) = "The list has two elements: " ++ show x ++ " and " ++ show y
tell (x:y:_) = "This list is long. The first two elements are: " ++ show x ++ " and " ++ show y
-- tell [1..5] -- "This list is long. The first two elements are: 1 and 2"
-- 使用as模式,將模式的值分割為多個項的同時保持對整體的引用。
firstLetter :: String -> String
firstLetter "" = "Empty string, whoops!"
firstLetter all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]
-- firstLetter "Hello" -- "The first letter of Hello is H"
-- 使用哨兵,哨兵是一個布爾表達式,跟在豎線(|)右邊,計算為真的時候就選擇對應的函數體,否則向后查找。
tellBMI :: (RealFloat a) => a -> String
tellBMI bmi
| bmi <= 18.5 = "You're underweight, you emo, you!"
| bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
-- tellBMI 21
-- 關於計算時數值的一些問題,參考https://hackage.haskell.org/package/base-4.9.1.0/docs/Numeric.html
calcBMI :: (RealFloat a, Show a) => a -> a -> String
calcBMI weight height
| bmi <= skinny = info++ "You're underweight, you emo, you!"
| bmi <= normal = info++ "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= fat = info++ "You're fat! Lose some weight, fatty!"
| otherwise = info++ "You're a whale, congratulations!"
where bmi = weight / height **2.5 *1.3
(skinny,normal,fat) = (18.5, 25.0, 30.0) -- man24,woman22
info= "You are " ++ show weight ++" kgs, " ++show height ++ " m, and your bmi is " ++ show bmi ++ "! \n"
-- tellBMI 63 172
--列表生成器generator,let語句,謂詞等構造列表推導式
calcBMI2 :: [(Double,Double)] ->[Double]
calcBMI2 xs = [bmi|(w,h)<-xs, let bmi = w/h**2.5 *1.3,bmi>15.0]
-- 使用case語句,其實模式匹配不過是case表達式的語法糖而已
describeList :: [a] -> String
describeList xs = "The list is " ++ case xs of [] -> "empty."
[x] -> "a singleton list."
xs -> "a longer list."
-- 使用where what語句替換case語句
describeList2 :: [a] -> String
describeList2 xs = "The list is " ++ what xs
where what [] = "empty."
what [x] = "a singleton list."
what xs = "a longer list."
----------------------------------------------------------------------------------------
-- ch4 遞歸
-- 按照遞歸的基本思想,遞歸傾向於將問題展開為同樣的子問題,並不斷對子問題進行展開、求解,直達到達問題的基准條件(base case)為止。基准條件中問題不必再進行分解,必須有程序員知名一個非遞歸結果。
-- 編寫函數的時候,首先確定基准條件,也就是對應特殊輸入的簡單非遞歸解(比如空列表排序結果仍然為空);然后將問題分解為一個或者多個子問題,並遞歸地調用自身,最后基於子問題里得到的結果,組合成為最終的解(比如快速排序中,將列表分解為兩個列表和一個主元,並對這兩個列表分別應用同一個函數進行排序,最后得到的結果重新組合起來,就是最終排序后的列表了。
-- 遞歸在Haskell中很重要,命令式語言中傾向於“告知如何計算”(how),而Haskell傾向於“聲明問題是什么”(what)。這也就是說Haskell編程是,重要的不是給出解題步驟,而是定義問題和解的描述。此時遞歸將大顯身手,一旦確定了正確的基准條件和正確的子問題,Haskell就會根據子問題的求解結果組合成最終的結果。
-- 遞歸計算最大值,注意什么時候用方括號,什么時候用圓括號
mymax :: (Ord a) => [a] -> a
mymax [] = error "empty list" -- 基准模式1,空列表拋出錯誤
mymax [x] = x -- 基准模式2,單元數列表返回元素本身
mymax (x:xs) = max x (mymax xs) -- 模式3,列表形式,遞歸調用本身
-- mymax [1,2,5,3,4] --5
-- 復制n份,取一個整數和一個元素,返回一個包含該整數個重復元素的列表。
-- 因為是布爾判斷,所以使用了哨兵而不是模式匹配。
myreplicate :: Int -> a -> [a]
myreplicate n x
| n<=0 = []
| otherwise = x:myreplicate (n-1) x
-- myreplicate 4 3 -- [3,3,3,3]
-- 從列表中選取前n個元素
mytake :: (Num i, Ord i) => i -> [a] ->[a]
mytake n _
| n<=0 = [] -- 基准模式1,如果要獲取不大於0個元素,則返回空列表;注意這里的哨兵沒有otherwise,可以轉到其他的模式。
mytake _ [] = [] -- 基准模式2,如果列表為空,返回空列表
mytake n (x:xs) = x: mytake (n-1) xs -- 迭代模式,將列表切片,並遞歸操作
-- mytake 3 [1..5] -- [1,2,3]
-- 翻轉列表
myreverse :: [a] -> [a]
myreverse [] = []
myreverse (x:xs) = myreverse xs ++ [x]
-- myreverse [1..5] -- [5,4,3,2,1]
-- 生成無限列表
myrepeat :: a -> [a]
myrepeat x = [x] ++ myrepeat x
-- mytake 4 (myrepeat 3) -- [3,3,3,3]
-- 打包
myzip :: [a]->[b]->[(a,b)]
myzip [] _ = []
myzip _ [] = []
myzip (x:xs) (y:ys) = (x,y):myzip xs ys
-- myzip [1..5] [5..1] -- []
-- myzip [1,2,3,4,5] [5,4,3,2,1] -- [(1,5),(2,4),(3,3),(4,2),(5,1)]
-- 查看是否子元素
myelem :: (Eq a) => a -> [a] -> Bool
myelem a [] = False
myelem a (x:xs)
| a == x = True
| otherwise = a `myelem` xs
-- 3 `myelem` [1..5] -- True
-- 13 `myelem` [1..5] -- False
-- 快速排序
-- 使用謂詞和列表推導
myqsort :: (Ord a) => [a] -> [a]
myqsort [] = [] -- 基准模式是空列表
myqsort (x:xs) = -- 迭代模式是,將小於當前元素的放在左邊,大於當前元素的放在右邊,分別對左右兩部分進行快排
myqsort [a|a<-xs, a<=x] ++ [x] ++ myqsort [a|a<-xs, a>x]
-- 使用filter函數
myqsort2 :: (Ord a) => [a] -> [a]
myqsort2 [] = [] -- 基准模式是空列表
myqsort2 (x:xs) = -- 迭代模式是,將小於當前元素的放在左邊,大於當前元素的放在右邊,分別對左右兩部分進行快排
let smallerOrEqual = filter (<=x) xs
larger = filter (>x) xs
in myqsort2 smallerOrEqual ++ [x] ++ myqsort2 larger
-- myqsort [1,5,3,2,7,6,4,9,8] -- [1,2,3,4,5,6,7,8,9]
----------------------------------------------------------------------------------------
-- ch05 高階函數
-- Haskell中可以把函數用做參數和返回值,這樣的特定成為高階函數。
-- C++14支持函數返回自動類型推導,可以返回一個函數內部的lambda,也就是支持高階函數,好BUG啊。
{-
// g++ -std=c++14 xxx.cpp
auto fun(){
auto myfun = [](int a, int b){return a>b?a:b;};
return myfun;
}
auto myfun = fun();
cout << myfun(3,4)<<endl;
-}
-- 只要在類型簽名中看到了箭頭符號(->),就意味着它是一個將箭頭左邊視為參數類型並將箭頭右側部分作為返回類型的函數。
-- 如果遇到a->(a->a)這樣的類型簽名,說明它是一個函數,取類型為a的值作為參數,返回一個函數同樣取類型a為參數,並且返回值類型是a。
-- 這樣做,可以以部分參數調用函數,得到一個部分應用(Partial application)函數,該函數所接受的參數是一個函數,和之前少傳入的參數類型一致。
multThree :: Int -> (Int -> (Int -> Int))
multThree x y z = x*y*z
-- multiThree 3 4 5 -- 60
-- 使用部分應用
multTwoWithThree :: Int -> Int -> Int
multTwoWithThree x y = multThree 3 x y
-- multiTwoWithThree 4 5 -- 60
-- 在ghc中使用部分應用
multTwoWithThree2 = multThree 3 -- hs文件中不用let,但是ghc中需要使用let
-- multiTwoWithThree2 4 5 -- 60
-- 對中綴函數使用截斷,除以10
dividedByTen :: (Floating a) => a -> a
dividedByTen = (/10)
-- dividedByTen 200 -- 20.0
-- 對中綴函數使用截斷
subtractFour :: Int -> Int
subtractFour = (subtract 4)
-- [subtractFour x | x<-[1..5]] -- [-3,-2,-1,0,1]
-- 對中綴函數使用截斷
isUpperCase :: Char -> Bool
isUpperCase = (`elem` ['A'..'Z'])
-- [isUpperCase x | x<-"Hello"] -- [True,False,False,False,False]
-- 取另一個函數作為參數,並返回函數
applyTwice :: (a->a) -> a -> a
applyTwice f x = f (f x)
-- applyTwice (+3) 10 -- 16
-- applyTwice (++[3]) [10] -- [10,3,3]
-- applyTwice ([3]++) [10] -- [3,3,10]
-- 實現zipWith,取一個函數和兩個列表作為輸入,使用函數調用從兩個列表中取出的相應元素
-- 在編寫高階函數時,如果拿不准類型,可以先省略類型聲明,然后在使用:t命令查看Haskell的類型推導結果
myzipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
myzipWith _ [] _ = []
myzipWith _ _ [] = []
myzipWith f (x:xs) (y:ys) = f x y : myzipWith f xs ys
-- myzipWith (+) [1,2,3,4,5] [11,12,13,14,15] -- [12,14,16,18,20]
-- myzipWith (*) (replicate 5 2) [1..] -- [2,4,6,8,10]
-- 實現flip函數,以一個函數為參數,返回參數順序顛倒的函數
myflip :: (a->b->c) -> (b -> a -> c)
myflip f x y = f y x
-- zip [1..5] "hello" -- [(1,'h'),(2,'e'),(3,'l'),(4,'l'),(5,'o')]
-- myflip zip [1..5] "hello" -- [('h',1),('e',2),('l',3),('l',4),('o',5)]
-- myzipWith div (repeat 2) [1..5] -- [2,1,0,0,0]
-- flip (myzipWith div) (repeat 2) [1..5] -- [0,1,1,2,2]
-- 函數式程序員工具箱
-- 身為函數式程序員,很少需要對單個值求解花費太多心思。更多情況下是,處理一系列的數值、字母或者其他類型的數據
-- 通過轉換了這一類集合來求取最后的結果。常用的工具函數有:map、filter
-- map接受一個取a返回b的函數和一組a值的列表為參數,返回一組b值的列表
mymap:: (a->b) -> [a] -> [b]
mymap _ [] = []
mymap f (x:xs) = f x : mymap f xs
-- filter 去一個謂詞(predicate)和一個列表作為參數 ,返回由列表所有符合該條件的元素組成的列表。
-- 謂詞特指判斷事物是True或者False的函數,也就是返回布爾值的函數
myfilter :: (a->Bool) -> [a] -> [a]
myfilter _ [] = []
myfilter f (x:xs)
| f x = x : myfilter f xs
| otherwise = myfilter f xs
-- mymap (+2) [1..10] -- [3,4,5,6,7,8,9,10,11,12]
-- myfilter (>5) [1..10] -- [6,7,8,9,10]
-- myfilter (`elem` ['a'..'z']) "Hi, my name is Ausk!" -- "imynameisusk"
-- myfilter (`elem` ['A'..'Z']) "Hi, my name is Ausk!" -- "HA"
-- myfilter (<15) (myfilter even [1..20]) -- [2,4,6,8,10,12,14]
-- [x|x<-[1..20], even x,x<15] -- [2,4,6,8,10,12,14]
-- 計算小於1000的所有奇數的平方的和,分別使用括號、列表生成器、函數組合和函數應用符
sum (takeWhile (<1000) (filter odd (map (^2) [1..]))) -- 5456,函數組合
sum (takeWhile (<1000) [x^2| x<-[1..],odd x]) -- 5456,列表生成器
sum . takeWhile (<1000) . filter odd $map (^2) $[1..] -- 5456,函數組合和函數應用符
-- 將自然數的平方根相加,會在何處超過1000?
sqrtSum :: Int
sqrtSum = length ( takeWhile(<1000) ( scanl1 (+) (map sqrt [1..]) ) ) + 1
-- sum(map sqrt [1..130]) -- 993.6486803921487
-- sum(map sqrt [1..131]) -- 1005.0942035344083
-- 克拉茲序列,定義如下
-- 從任意自然數開始,如果是1則停止;如果是偶數則除以2;如果是奇數則乘以3然后加上1。取得結果並重復過程。
chain :: Integer -> [Integer]
chain 1=[1] -- 基准條件
chain n -- 哨兵選擇
| even n = n:chain (n `div` 2)
| odd n = n:chain (n*3 + 1)
-- chain 1 -- [1]
-- chain 10 -- [10,5,16,8,4,2,1]
-- 映射帶有多個參數的函數
let listOfFuncs = map (*) [10..] -- (*10) (*11) (*12) (*13) (*14) (*15) ...
(listOfFuncs !!4) 4 -- (*14) 4 = 56
-- Haskell中的Lambda是一次性的匿名函數,以右斜杠開頭,后面緊跟着函數的參數列表,參數之間使用空格分割,->之后是函數體,通常使用圓括號括起來。
map (+3) [1..5] -- [4,5,6,7,8]
map (\x -> x+3) [1..5] -- [4,5,6,7,8]
map (\(a,b) -> a+b) (zip [1..5] [11..15]) -- [12,14,16,18,20]
zipWith (\a b -> a*10 + b) [1..] [5,4,3,2,1] -- [15,24,33,42,51]
-- 處理列表大多數具有篤定模式。通常會將基准條件設置為空列表,然后引入 (x:xs)模式,再對單個元素和余下的列表做一些事情。
-- 因為這一模式比較常見,Haskell中提供了折疊(fold)函數來使之簡化。通過折疊函數將一個數據結構(如列表)歸約為單個值。
-- 一個折疊可以從左邊或者右邊開始折疊,通常取一個二元函數、一個待折疊的列表(和一個初始值),得到歸約后的單值。
-- 左折疊(foldl、foldl1)從左側開始,右折疊(foldr、foldr1)從右側開始。
-- 使用lambda表達式和foldl進行折疊
suml :: (Num a) => [a] -> a
suml xs = foldl (\acc x-> acc + x) 0 xs
-- 使用foldl進行折疊
suml2 :: (Num a) => [a] -> a
suml2 xs = foldl (+) 0 xs
-- 使用lambda表達式和foldr進行折疊
sumr :: (Num a) => [a] -> a
sumr xs = foldr (\x acc-> acc + x) 0 xs
-- 使用foldr進行折疊
sumr2 :: (Num a) => [a] -> a
sumr2 xs = foldr (+) 0 xs
-- 使用左折疊實現map
mapl :: (a->b) -> [a] -> [b]
mapl f xs = foldl(\acc x -> acc ++ [f x]) [] xs
-- mapl (+1) [1..5] -- [2,3,4,5,6]
-- 使用右折疊實現map
mapr :: (a->b) -> [a] -> [b]
mapr f xs = foldr(\x acc -> f x : acc) [] xs
-- mapr (+1) [1..5] -- [2,3,4,5,6]
-- 使用折疊實現一些函數
mymax_foldr1 ::(Ord a) => [a] -> a
mymax_foldr1 = foldr1 max -- 使用foldr1函數和max組合,得到求最值的函數
-- mymax [2,4,10,7,3,8,6,1,5,9] -- 10
-- 掃描scan,類似於折疊,不過會將變動記錄到一個列表中
mymax_scanr1 :: (Ord a) => [a] ->[a]
mymax_scanr1 = scanr1 max
-- mymax_scanr1 [2,4,10,7,3,8,6,1,5,9] -- [10,10,10,9,9,9,9,9,9,9]
-- 掃描scan,類似於折疊,不過會將變動記錄到一個列表中
mymax_scanl1 :: (Ord a) => [a] ->[a]
mymax_scanl1 = scanl1 max
-- mymax_scanl1 [2,4,10,7,3,8,6,1,5,9] --[2,4,10,10,10,10,10,10,10,10]
-- 無限列表折疊,當二元函數不總是需要對第二個參數進行求值如邏輯操作(&& ||)的短路求值
-- 這時,可以利用Haskell的惰性求值操作無限列表
myand ::[Bool] -> Bool
myand xs = foldr (&&) True xs
myand (repeat False)
-- 使用空格的函數調用符是左結合的,如 f a b c 等價於 ((f a b) c)
-- 而函數應用符(function application operator)即$的調用是右結合的
-- 函數應用符號可以減少括號,也可以將函數應用轉化為函數
-- 即 f $ g $ x 等價於 f$(g$x), 如 sum $ a + b + c 等價於 (sum (a + b + c))
:t ($) -- ($) :: (a -> b) -> a -> b
sum $3+4+5 -- 12
map ($3) [ (4+), (5*),(6/),(^2),sqrt] -- [7.0,15.0,2.0,9.0,1.7320508075688772]
-- 函數組合function composition,定義是 (f.g)(x) = f(g(x))
-- 也即是先拿參數調用一個函數,然后取結果作為參數調用另一個函數
-- 進行函數組合的函數是 (.)
:t (.) -- (.) :: (b -> c) -> (a -> b) -> a -> c
map (\x -> negate $sqrt $ abs x) [-4,16,-9] -- [-2.0,-4.0,-3.0]
map (negate . sqrt . abs) [-4,16,-9] -- [-2.0,-4.0,-3.0]
sum (replicate 5 (max 3 4) ) -- 20
(sum . replicate 5) (max 3 4) -- 20
sum $ replicate 5 $ max 3 4 -- 20
-- 通過使用函數組合哈數來省略表達式中的大量括號,可以首先找到最里面的函數和參數,
-- 在它們前面加上一個$,接着省略其余函數的最后一個參數,通過 (.)組合在一起。
replicate 2 ( product ( map (*3) ( zipWith max [1,4,7,9] [3,2,6,10])))
replicate 2 . product . map (*3) $ zipWith max [1,4,7,9] [3,2,6,10]
-- 使用函數組合PointFree風格,即省略共同的參數,使得代碼更加簡潔易讀。
-- 讓程序員更加傾向於思考函數的組合方式,而不是數據的攢肚和變化。
mysum ::(Num a) => [a] -> a
mysum xs = foldr (+) 0 xs
-- 等價的point-free風格
mysum ::(Num a) => [a] -> a
mysum = foldr (+) 0
-- 原始函數
func :: (Floating a, Integral b, RealFrac a) => a -> b
func x = log(sqrt (max 50 x))
-- 等價的point-free風格
func :: (Floating a, Integral b, RealFrac a) => a -> b
func = log.sqrt.max 50
----------------------------------------------------------------------------------------
-- ch06 模塊
-- Haskell 中的模塊,類似與Python中的module,C++中的namespace,Java中的Package。
-- 模塊中可以包含類型和函數的定義,並且允許導出(export)其中的部分定義。
-- 將模塊代碼分解到模塊中,使得代碼相互之間較少過多的依賴,也就是使得模塊松耦合(loosely coupled),方便管理和重用。
-- 在任意函數定義之前導入模塊,import ModuleName (FunctionName) 或者 :m ModuleName
-- 要檢查函數的定義或者查找函數所在的模塊,使用[Hoogle](http://www.haskell.org/hoogle/)
-- 學習Haskell的最好方式是查找函數和模塊的文檔,也可以閱讀Haskell各個模塊的源代碼。
-- 注意當點號位於限定導入的模塊名和函數之間且沒有空格時標示對模塊中函數的引用;否則視為函數的組合。
import qualified Data.Map as M -- 限定導入 Data.Map,定義別名為M,必須使用M.xxx來引用名字
import Data.List hiding (nub) -- 避免導入模塊中的某些函數
import Data.List (sort,group,words) -- 導入模塊中的特定函數
import Data.List (sort,group,words)
wordNums :: String ->[(String,Int)]
wordNums = map (\ws ->(head ws, length ws)) . group . sort . words
-- wordNums xs = map (\ws ->(head ws, length ws)) (group(sort(words xs)
wordNums "it is a good idea, it is very good !" -- [("!",1),("a",1),("good",2),("idea,",1),("is",2),("it",2),("very",1)]
-- 干草堆中的縫紉針:查找一個列表(縫紉針,needle)是否在另一個列表(干草堆,haystack)中
-- 等價的函數是 Data.List.isInPrefixOf
import Data.List
isIn :: Eq a => [a] -> [a] -> Bool
needle `isIn` haystack = any (needle `isPrefixOf`) $tails haystack
-- isIn "how" "Hello,how are you ?" -- True
-- isIn "hei" "Hello,how are you ?" -- False
----------------------------------------------------------------------------------------
-- 凱撒加密與解密
-- 加密,把字符映射到另一個字符。首先把字符轉化為數字,增加/減少偏移量,再從數字映射到字母。
import Data.Char
-- 加密,map (\c -> chr $ ord c + offset) msg
myencode :: Int -> String -> String
myencode offset = map ( chr . (+offset) . ord )
-- 解密,map (\c -> chr $ ord c + offset) msg
mydecode :: Int -> String -> String
mydecode offset = map ( chr . (-offset) . ord )
let msg = "how are you"
let offset = 10
let encoded = myencode offset msg
let decoded = mydecode offset encoded
decoded == msg -- True
----------------------------------------------------------------------------------------
-- 實現嚴格左折疊,不使用惰性求值,防止大量的延遲計算占據內存而導致棧溢出
--myfoldl:: (b -> a -> b) -> b -> [a] -> b
myfoldl g initval [] = error "empty list"
myfoldl g initval [x] = g initval x
--myfoldl g initval all@(x:xs)= myfoldl g (g initval x) xs
myfoldl g initval all@(x:xs)= myfoldl g (g initval $head all) $tail all
-- myfoldl (+) 0 [1..100000] -- 5000050000
-- Maybe類型: Maybe a類型標示可以為空,可以只含有一個元素
:t Just -- Just :: a -> Maybe a
:t Just "Hi" -- Just "Hi" :: Maybe [Char]
----------------------------------------------------------------------------------------
import Data.Char
import Data.List
-- 計算數字的各位之和
digitSum :: Int -> Int
digitSum = sum . map digitToInt . show
-- digitSum 49999 -- 40
-- 查找一個數,這個數的各位數字之和等於n
findFirstTo :: Int -> Maybe Int
findFirstTo n = find (\x -> digitSum x == n ) [1..]
-- findFirstTo 40 -- Just 49999
-- 有些數據結構不關心數據的存儲順序,而是將數據按照鍵值對的形式進行存儲,然后按鍵索值,查找指定鍵的值。
-- 這樣的數據結構可以使用關聯列表(association list)或者映射map實現,於Python中的dict,C++中的map/unordered_map
idBook = [("jj","M72340"),("ff","M72341")]
-- 取一個鍵和一個鍵值列表作為參數,過濾列表,僅僅保留與鍵相匹配的元素,取首個鍵值對,返回其中的值。
-- 一個標准的處理列表的遞歸函數,基准條件、分隔條件、遞歸調用都齊全了,這也是典型的折疊模式。
findKey :: (Eq k) => k -> [(k,v)] -> Maybe v
findKey key [] = Nothing
findKey key ((k,v):xs)
| key == x = Just v
| otherwise findKey key xs
-- [findKey key idBook| key <- ["ff", "ff","ll"] ] -- [Just "M72340",Just "M72341",Nothing]
-- 基於折疊模式,替換遞歸函數。折疊模式簡潔明了,一目了然。
findKey :: (Eq k) => k -> [(k,v)] -> Maybe v
findKey key xs = foldr (\(k,v) acc -> if key == k then Just v else acc) Nothing xs
-- [findKey key idBook| key <- ["ff", "ff","ll"] ] -- [Just "M72340",Just "M72341",Nothing]
-- 使用Data.Map數據結構
import qualified Data.Map as M -- 限定導入 Data.Map,定義別名為M,必須使用M.xxx來引用名字
phoneBookToMap ::(Ord k) => [(k,String)] -> M.Map k String
phoneBookToMap xs = M.fromListWith add xs
where add number1 number2 = number1 ++ ", " ++ number2
phoneBookList = [("jj","M72340"),("jj","Q33361"),("ff","M72341")]
phoneBookMap = phoneBookToMap phoneBookList -- fromList [("ff","M72341"),("jj","Q33361, M72340")]
newPhoneBookMap = M.insert "ff" "XX-XXX" phoneBookMap -- fromList [("ff","XX-XXX"),("jj","Q33361, M72340")]
M.size phoneBookMap -- 2
M.lookup "jj" phoneBookMap --Just "Q33361, M72340"
M.lookup "jia" phoneBookMap --Nothing
-- Set
import qualified Data.Set as Set
let text1 = "Are they so different?"
let text2 = "Yeah, they are so different!"
let set1 = Set.fromList text1 -- fromList " ?Adefhinorsty"
let set2 = Set.fromList text2 -- fromList " !,Yadefhinorsty
Set.intersection set1 set2 -- fromList " defhinorsty"
Set.difference set1 set2 -- fromList "?A"
Set.difference set2 set1 -- fromList "!,Ya"
Set.union set1 set2 -- fromList " !,?AYadefhinorstyset1
Set.insert 4 $ Set.fromList [9,3,8,1] -- fromList [1,3,4,8,9]
Set.delete 4 $ Set.fromList [3,4,5,4,3,4,5] -- fromList [3,5]
Set.fromList [2,3,4] `Set.isSubsetOf` Set.fromList [1,2,3,4,5] -- True
----------------------------------------------------------------------------------------
-- 自定義數據類型和類型別名
-- 汽車Car,帶類型說明的數據結構,及相應的操作
data Car = Car{ company::String, model::String, year::Int } deriving (Show)
tellCar :: Car -> String
tellCar (Car {company = c, model = m, year = y}) = "This " ++ c ++ " " ++ m ++ " was made in " ++ show y
-- 一周七天,繼承類型
data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday
deriving (Eq, Ord, Show, Read, Bounded, Enum)
-- 遞歸數據結構
-- 樹的構造和操作
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
test = do
let stang = Car{company = "Ford", model = "Mustang", year=1967}
print $ tellCar stang
{-
show Wednesday
read "Saturday" :: Day
Monday `compare` Wednesday
Monday == Monday
[minBound .. maxBound] :: [Day]
succ Monday
pred Saturday
-}
let nums = [8,6,4,1,7,3,5]
let numsTree = foldr treeInsert EmptyTree nums
print $ numsTree
print $ 3 `treeElem` numsTree
print $ 13 `treeElem` numsTree
----------------------------------------------------------------------------------------
{-
-- hello.hs
-- 功能:學習輸入和輸出操作
shell > ghc --make ./hello.hs
shell > ./hello
-}
import System.IO
main = do
print "What's your name?"
name <- getLine
putStrLn $ "Hey " ++ name ++ ", you rock!"
sequence $ map print [3..5]
mapM print [3..5] -- mapM取一個函數和一個列表作為參數,然后將函數映射到列表上,最后將結果應用與sequence。
mapM_ print [3..5] -- mapM_與mapM類似,不過不保存結果。
-- 使用 openFile打開文件,hGetContents讀取文件,hClose關閉文件
infile_handle <- openFile "hello.hs" ReadMode
contents <- hGetContents infile_handle
putStr contents
hClose infile_handle
-- 使用withFile確保文件操作后,文件的句柄一定被關閉
withFile "hello.hs" ReadMode (\infile_handle -> do
contents <- hGetContents infile_handle
putStr contents)
----------------------------------------------------------------------------------------
-- ch10 函數式地解決問題
-- 常用的中綴表達式,需要用括號改變優先級別。而使用逆波蘭表達式(reverse polish natation, RPN),
-- 操作符在操作數的后面。解析表達式子的時候,遇到數字時將數字壓入棧,
-- 遇到符號時彈出兩個數字並使用符號操作,然后將結果入棧。表達式結尾時,棧里面僅剩下一個數字,也就是表達式的值。
import Data.List
solveRPN :: String -> Float
solveRPN = head . foldl foldingFunction [] . words
where foldingFunction (x:y:ys) "*" = (x * y):ys
foldingFunction (x:y:ys) "+" = (x + y):ys
foldingFunction (x:y:ys) "-" = (y - x):ys
foldingFunction (x:y:ys) "/" = (y / x):ys
foldingFunction (x:y:ys) "^" = (y ** x):ys
foldingFunction (x:xs) "ln" = log x:xs
foldingFunction xs "sum" = [sum xs]
foldingFunction xs numberString = read numberString:xs
{-
-- http://learnyouahaskell.com/for-a-few-monads-more
readMayBe ::(Read a) => String -> Maybe a
readMayBe stmt = case reads stmt of [(x,"")] -> Just x
_ -> Nothing
foldingFunction2 :: [Double] -> String -> Maybe [Double]
foldingFunction2 (x:y:ys) "+" = (x + y):ys
foldingFunction2 (x:y:ys) "-" = (y - x):ys
foldingFunction2 (x:y:ys) "/" = (y / x):ys
foldingFunction2 (x:y:ys) "^" = (y ** x):ys
foldingFunction2 (x:xs) "ln" = log x:xs
foldingFunction2 xs "sum" = [sum xs]
foldingFunction xs numberString = liftM (:xs) $ readMayBe numberString
solveRPN2 :: String -> Maybe Double
solveRPN2 stmt = do
[result] <- foldM foldingFunction2 [] $ words stmt
return result
-}
-----------------------------------------------------------------------------------
{-
- 計算最快的路線
# A === 50 == A1 === 5 === A2 === 40 === A3 === 10 === A4
# || || || ||
# 30 20 25 00
# || || || ||
# B === 10 == B1 === 90 === B2 === 2 === B3 === 8 === B4
求分別從A和B出發到達A4和B4的最短路徑
解決方案:
每次檢查三條路線的通行時間:A路上一段路程,B路上一段路程,以及連接兩個路口的C小路。
稱它們為一個地段section。因此將道路標示為4個地段:
> 50 30 10
> 5 20 90
> 40 25 2
> 10 0 8
定義道路數據類型
data Section = Section { getA :: Int, getB :: Int, getC :: Int } deriving (Show)
type RoadSystem = [Section]
引入標簽Label表示一個枚舉類型(A、B或者C),以及一個類型別名Path。
data Label = A | B | C deriving (Show)
type Path = [(Label, Int)]
在求解時,處理的列表同時維護抵達當前地段A的最佳路徑和抵達B的最佳路徑。遍歷的時候累加這兩條最佳路徑,是一個左折疊。
折疊的步驟是,根據抵達當前地段A路和B路的最佳路線和下一個地段的信息,分別計算到達下一個地段A和下一個地段B的最佳路線。
初始地段A、B路的最佳路線都是[],檢查地段 Section 50 10 30,推斷出到達A1和B1的最佳路線分別是[(B,10),(C,30)]和[(B,10)]。
重復這一計算步驟,直到處理完畢。寫成函數的形式,參數是一對路徑和一個地段,返回到達下一個地段的一條新的最佳路徑。
roadStep :: (Path, Path) -> Section -> (Path, Path)
-}
import Data.List --在任何函數開始之前道路模塊
import System.IO -- 導入 IO 模塊
-- 自定義數據類型和類型別名
data Section = Section { getA :: Int, getB :: Int, getC :: Int } deriving (Show)
type RoadSystem = [Section]
data Label = A | B | C deriving (Show)
type Path = [(Label, Int)]
-- 構建道路
heathrowToLondon :: RoadSystem
heathrowToLondon = [Section 50 10 30, Section 5 90 20, Section 40 2 25, Section 10 8 0]
-- 根據當前路段A和B路的最佳路線以及下一個Section的信息,求解得到下一個路段A路和B路的最佳路線。
roadStep :: (Path, Path) -> Section -> (Path, Path)
roadStep (pathA, pathB) (Section a b c) =
let priceA = sum $ map snd pathA
priceB = sum $ map snd pathB
forwardPriceToA = priceA + a
crossPriceToA = priceB + b + c
forwardPriceToB = priceB + b
crossPriceToB = priceA + a + c
newPathToA = if forwardPriceToA <= crossPriceToA
then (A,a):pathA
else (C,c):(B,b):pathB
newPathToB = if forwardPriceToB <= crossPriceToB
then (B,b):pathB
else (C,c):(A,a):pathA
in (newPathToA, newPathToB)
-- 求解最佳路徑
optimalPath :: RoadSystem -> Path
optimalPath roadSystem =
let (bestAPath, bestBPath) = foldl roadStep ([],[]) roadSystem
in if sum (map snd bestAPath) <= sum (map snd bestBPath)
then reverse bestAPath
else reverse bestBPath
-- 將一個列表分割成為一些等長度的列表
groupsOf :: Int -> [a] -> [[a]]
groupsOf 0 _ = undefined
groupsOf _ [] = []
groupsOf n xs = take n xs : groupsOf n (drop n xs)
-- 輸入和輸出操作
shell > ghc --make ./hello.hs
shell > ./hello
testCalcBestPath = do
print "The default patch:" ++ show heathrowToLondon
print "The optimal path:" ++ show (optimalPath heathrowToLondon)
{-
echo -e "50\n10\n30\n5\n90\n20\n40\n2\n25\n10\n8\n0" > path.txt
runhaskell haskell.hs <path.txt
ghc --make -O xxx.hs
shell > ./haskell < path.txt
print "Get path from file (runhaskell XXX.hs < path.txt):"
-}
-- 直接從文件中讀取路徑
withFile "path.txt" ReadMode (\infile_handle -> do
contents <- hGetContents infile_handle
putStr contents
let threeTupleList = groupsOf 3 (map read $ lines contents)
roadSystem = map (\[a,b,c] -> Section a b c) threeTupleList
path = optimalPath roadSystem
putStrLn $ "The best path to take is: " ++ show path
putStrLn $ "The best path to take is: " ++ (concat $ map (show . fst) path )
putStrLn $ "The price of the path is: " ++ show (sum $ map snd path)
)
main = do
-- -- 調用測試函數
-- testCalcBestPath
print $ "Hello, Haskell!"
{-
print "What's your name?"
name <- getLine
putStrLn $ "Hey " ++ name ++ ", you rock!"
sequence $ map print [3..5]
mapM print [3..5] -- mapM取一個函數和一個列表作為參數,然后將函數映射到列表上,最后將結果應用與sequence。
mapM_ print [3..5] -- mapM_與mapM類似,不過不保存結果。
-- 使用 openFile打開文件,hGetContents讀取文件,hClose關閉文件
infile_handle <- openFile "hello.hs" ReadMode
contents <- hGetContents infile_handle
putStr contents
hClose infile_handle
-- 使用withFile確保文件操作后,文件的句柄一定被關閉
withFile "hello.hs" ReadMode (\infile_handle -> do
contents <- hGetContents infile_handle
putStr contents)
-}