Haskell入门笔记
最近在学习haskell,想要要深入的了解一下函数式编程(functional programming),朋友建议可以通过学习这一门语言来加深理解。所以就尝试一下。
learnyouahaskell.com是一个非常不错的入门教程,适合有一定的编程基础的人阅读。电子版可以免费在线阅读
String
'' 和 "的区别
List
字符串也是List,List的拼接可以使用 ++
1
|
ghci> [1,2,3,4] ++ [9,10,11,12]
|
高效率的拼接 使用操作符:
1
|
ghci> 'A':" SMALL CAT"
|
!! 根据索引值取List元素, 索引值从0开始
1
|
*Main> "hello world"!!0
|
取出大于List长度的会报错
1
|
*Main> [1,2,3,4,5,6]!!7
|
List的比较, 比较的方式是 lexicographical order
1
|
ghci> [2,1] > [1,2]
|
List有一系列的函数可以使用
taillastinitheadlength取List的长度length [5,4,3,2,1] == 5null检查List是否为空null [] == Truetake获取从头部开始的n个元素take 3 [1,2,3,4]获取到[1,2,3]reverse翻转数组minimum最小值maximum最大值sumelem检查一个元素是否在List中 4elem[3,4,5,6] == True
注意:这几个函数用于空List的时候会报错
1
|
ghci> tail []
|
集合的方式来理解List
1
|
ghci > [x*2 | x <- [1..10]]
|
元组 Tuples
元组的元素类型可以不一致, 而List是要求元素是同类型的。 例如
1
|
('Name', 50)
|
稍微负责一点的元组
1
|
ghci> [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2]
|
元组同样有一系列的函数,常用的有
fst, 取出元组首个元素
snd 取出元组第二个元素
zip 一个非常有用的函数,将两个List的对应元素合并为元组,并构成一个List
1
|
ghci> zip [1 .. 5] ["one", "two", "three", "four", "five"]
|
Type And TypeClasses
Type
Haskell是一个强类型的语言,有Char, Integer, Int, Float,Double,Bool等基本类型
Typeclasses
类限制 class constraint , => 这个符号代表类限制
1
|
ghci> :t (==)
|
the equality function takes any two values that are of the same type and returns a Bool. The type of those two values must be a member of the Eq class (this was the class constraint). 在标准的Haskell中,除了了io和function之外,其他类型都是属于Eq
Haskell的typeclass有 Eq, Ord, Show,Read,Enum, Bounded
Eq
Ord
Show
Read
1
|
hci> read 4
|
需要使用类型声明 (type annotations),Haskell才有办法正确的解析,或者是在表达式中,Haskell自己进行推断
1
|
ghci> read "4" :: Int
|
Syntax in Functions
Pattern Matching
1
|
lucky :: (Integral a) => a -> String
|
如果输入值是7,match到第一个pattern,不是7,那么就会match到第二个pattern。所以,在定义pattern的时候,次序是很重要的,先定义特例pattern,在定义普遍的pattern
再看下面这个例子,
1
|
factorial :: (Integral a) => a -> a
|
The x:xs pattern
1
|
head' :: [a] -> a
|
xs@(x:y:ys) @符号 表示 xs 代表的就是 (x:y:ys), 这样可以写起来更加简单一些。
1
|
capital :: String -> String
|
Gurard
看完下面这个例子,基本就可以理解Guard是怎么工作的。不需要介绍太多。需要的注意的是 where里面定义的变量只对于该函数有作用。
1
|
bmiTell :: (RealFloat a) => a -> a -> String
|
还可以类似于下面用3元组的方式
1
|
...
|
let <bindings> in <expression>
1
|
ghci> 4 * (let a = 9 in a + 1) + 2
|
introduce functions in a local scope:
1
|
ghci> [let square x = x * x in (square 5, square 3, square 2)]
|
多个表达式用分号隔开
1
|
ghci> (let a = 100; b = 200; c = 300 in a*b*c, let foo="Hey "; bar = "there!" in foo ++ bar)
|
使用let和List Comprehension来对bmi的例子进行改写, 注意,在List Comprehension中使用时,in是可以省略的,因为名字的可见性已经提前定义好了。
1
|
calcBmis :: (RealFloat a) => [(a, a)] -> [a]
|
如果不使用in来限定范围,则是全局可见的
1
|
ghci> let zoot x y z = x * y + z
|
case
1
|
case expression of pattern -> result
|
这一段表达式其实是case的一个语法糖
1
|
head' :: [a] -> a
|
如果不使用语法糖,则是
1
|
head' :: [a] -> a
|
Higher order functions
haskell,函数可以作为参数,也可以作为返回值
Curried functions
之前在js中就比较熟悉这个用法,所以接受起来很简单,不过haskell比javascript的定义方式优雅太多了。以haskell中的max函数为例.
1
|
ghci> max 4 5
|
max函数的定义的定义是 max :: (Ord a) => a -> a -> a
That can also be written as
max :: (Ord a) => a -> (a -> a). That could be read as:maxtakes anaand returns (that’s the->) a function that takes anaand returns ana. That’s why the return type and the parameters of functions are all simply separated with arrows.
举一个例子来看看这种定义方式的强大之处
1
|
multThree :: (Num a) => a -> a -> a -> a
|
运行一下看看
1
|
ghci> let multTwoWith9 = multThree 9
|
在举一个例子:
1
|
compareWithHundred :: (Num a, Ord a) => a -> Ordering
|
Compare has a type of
(Ord a) => a -> (a -> Ordering)and calling it with 100 returns a(Num a, Ord a) => a -> Ordering.
一个我第一次看稍难理解的一个例子
1
|
applyTwice :: (a -> a) -> a -> a
|
运行
1
|
ghci> applyTwice (+3) 10
|
zipwith
1
|
zipWith' :: (a -> b -> c) -> [a] -> [b] -> [c]
|
1
|
ghci> zipWith' (+) [4,2,5,6] [2,6,2,3]
|
filter
1
|
filter :: (a -> Bool) -> [a] -> [a]
|
1
|
ghci> filter (>3) [1,5,3,2,1,6,4,3,2,1]
|
1
|
ghci> sum (takeWhile (<10000) (filter odd (map (^2) [1..])))
|
list comprehesion vs filter
1
|
ghci> [n | n <- [1..10], odd n]
|
chain数列
1
|
chain :: (Integral a) => a -> [a]
|
Lambdas
使用符号\来表示,代表这是一个匿名函数,一次性使用。
将前面提及的numLongChains函数进行改写
1
|
numLongChains :: Int
|
其中 (\xs -> length xs > 15) 就是lambdas
下面我们看使用 partial 和 lambda的对比
1
|
ghci> map (+3) [1,6,3,2]
|
在这个例子中,partial会是一个更好的选择。可读性更高
更多例子
1
|
ghci> zipWith (\a b -> (a * 30 + 3) / b) [5,4,3,2,1] [1,2,3,4,5]
|
fold up list foldl
Folds can be used to implement any function where you traverse a list once, element by element, and then return something based on that. Whenever you want to traverse a list to return something, chances are you want a fold.
使用foldl对sum函数重新改写
1
|
sum' :: (Num a) => [a] -> a
|
1
|
ghci> sum' [3,5,2,1]
|
acc x
0 3
3 5
8 2
10 1
11
更简洁的写法
1
|
sum' :: (Num a) => [a] -> a
|
其中可以改写为这样子的形式有一个原因就是在haskell中 foo a = bar b a 可以简写为 foo = bar b
上例中就是 sum' xs = foldl (+) 0 xs 的简写
另外一个例子,使用 foldl来实现 elem 函数
1
|
elem' :: (Eq a) => a -> [a] -> Bool
|
acc的初始值是False,也就是不存在,如果x等于y就返回 True,不等于就保持acc
fold是非常强大的,书中重新实现了许多基础函数
1
|
maximum' :: (Ord a) => [a] -> a
|
head的实现最好还是之前用的pattern match,这里只是为了举例
scanl 和 scanr 与 foldl 和foldr是相似的,只是scan会输出每一个阶段的累计值
1
|
ghci> scanl (+) 0 [3,5,2,1]
|
需要多少个自然数才能使其开方的和大于1000
length (takeWhile (<1000) (scanl1 (+) (map sqrt [1..]))) + 1
如何理解呢?
执行takeWhile (<1000) (scanl1 (+) (map sqrt [1..]))得到的结果是
1
|
[1.0,2.414213562373095,4.146264369941973,6.146264369941973,8.382332347441762,10.83182209022494,13.47757340128953,16.30600052603572,19.30600052603572,22.4682781862041,25.7849029765595,29.249004591697254,32.854555867161245,36.59621325393519,40.46919660014261,44.46919660014261,48.59230222576027,52.834942912879555,57.19384185642023,61.66597781141981,66.24855350637564,70.93896926619907,75.73480078951178,80.63378027507814,85.63378027507814,90.73279978867092,95.92895221137755,101.22045483350674,106.60561964064124,112.0828452156929,117.65060957852292,123.3074638280153,129.05202647455332,134.88297836939861,140.79905815249822,146.79905815249822,152.88182068279644,159.04623468576543,165.29123268416382,171.6157880045006,178.01891224193344,184.4996529403413,191.0570914646433,197.6903410453541,204.39854497785348,211.18087496097874,218.03652956137978,224.96473279165528,231.96473279165528,239.03580060352076,246.1772290320636,253.38833158299158,260.6684414722721,268.0169107006216,275.43310918771726,282.91642396126514,290.46625839653586,298.0820315023998,305.76317725026837,313.5091439426832,321.31939361858986,329.19340149260165,337.1306554257954,345.1306554257954,353.192913174094,361.3169515787299,369.50230435060234,377.74851560183765,386.0551394647557,394.4217397300965,402.8478895032728,411.3331708775114,419.87717462282893,428.47949988987153,437.1397539277159,445.85755181479726,454.6325162021894,463.4642770685172,472.3524714858328,481.2967433958319,490.2967433958319,499.35212853396933,508.4625621131136,517.6277135030253,526.8472579603182,536.1208764558139,545.4482555089028,554.8290870285497,564.2630681606063,573.7499011411114,583.2892931552809,592.8809562019063,602.5246069628993,612.2199666777319,621.9667610225409,631.7647199936737,641.6135777954698,651.5130727320815,661.4629471031477,671.4629471031477,681.5128227242686,691.6123276626307,701.7612192277229,711.9592582549085,722.2062090208681,732.5018391618551,742.8459195946436,753.2382244400569,763.6785309489675,774.166619430669,784.7022731835218,795.2852784277802,805.9154242405149,816.5925024925461,827.3163077873097,838.0866374015787,848.9032912279707,859.7660717191709,870.6747838338066,881.6292349839099,892.6292349839099,903.6745960010971,914.7651325075066,925.9006612331666,937.0810011206655,948.3059732809874,959.575400950572,970.8891094495567,982.2469261411572,993.6486803921487]
|
取得 这个数组的长度 + 1之后就是解了。注意,这里不能使用filter,因为filter无法用于无限的数组
Function application with $
理解这个符号$更好的方式是看例子
sqrt (3 + 4 + 9) 与 sqrt 3 + 4 + 9 是不等价的,但是 可以使用 $ 来表示 符号右侧的表达式为左侧的输入值 sqrt $ 3 + 4 + 9
1
|
ghci> sqrt $ 3 + 4 + 9
|
再复杂一丢丢:sum (filter (> 10) (map (*2) [2..10])) 要怎么改写呢?
首先 filter (> 10) (map (*2) [2..10]) 可以改写为 filter (>10) $ map (*2) [2..10]
继而可以改写为 sum $ filter (> 10) $ map (*2) [2..10]
Function composition
使用符号.来表示函数的组合,f (g (x)) = (f.g) x直接从例子中进行理解 negate . (* 3) 表示的是 乘于 3 让后变为负数
1
|
ghci> (negate . (* 3)) 4
|
而组合不限于两个函数,可以多个组合 f (g (z x)) = (f . g . z) x
1
|
ghci> map (negate . sum . tail) [[1..5],[3..6],[1..7]]
|
上面举的例子都是只有一个参数的,如果是多个参数呢?答案就是 partial
例如 sum (replicate 5 (max 6.7 8.9))可以改写为 (sum . replicate 5 . max 6.7) 8.9 对与这个我目前不是很理解,不过还好作者指出了一个技巧,改写replicate 100 (product (map (*3) (zipWith max [1,2,3,4,5] [4,5,6,7,8]))) 看起来很复杂,但其实很简单 replicate 100 . product . map (*3) . zipWith max [1,2,3,4,5] $ [4,5,6,7,8]
那么这个函数组合到底对实际编程有什么用呢?看下面这个例子
1
|
oddSquareSum :: Integer
|
可以改写为
1
|
oddSquareSum :: Integer
|
如果为了更好的可读性, 可以使用let来进行可读性的改善
1
|
oddSquareSum :: Integer
|
modules
加载模块
加载模块的语法: import <module name>
1
|
import Data.List
|
在命令行模式下想要使用模块可以使用 :m + <module name>,如果有多个模块用空格隔开 例如 :m + Data. List
只需要 import 其中的几个函数
1
|
import Data.List (nub, sort)
|
import 的时候排除某个函数
1
|
import Data.List hiding (nub)
|
如果模块之间出现同名函数,可以通过 qualified来指定
1
|
import qualified Data.Map
|
如果想要使用 Data.Map的filter函数,就需要通过 Data.Map.filter 或者 M.filter 进行调用,而filter 则会使用我们熟悉的那个 filter,来自于 Prelude 模块,该模块会预加载。
想要查询函数可以使用 hoogle
Data.List
Data.List中有一些很有用的函数intersperse,intercalate, concat concatMap, takeWhile, dropWhile等等,下面列出一些对我有启发的函数:
and 和 or
and $ map (==4) [2,3,4,5,6,1]得到False 和 or $ map (==4) [2,3,4,5,6,1] 得到True
any 和 all
例如 any (==4) [1,2,3,4] 得到True, 而
1
|
all (`elem` ['A'..'Z']) "HEYGUYSwhatsup"
|
得到 False
transpose
transpose的用法对于我比较新奇 , 看书中的例子
1
|
ghci> transpose [[0,3,5,9],[10,0,0,9],[8,5,1,-1]]
|
iterate
group和sort
1
|
ghci> map (\l@(x:xs) -> (x,length l)) . group . sort $ [1,1,1,1,2,2,2,2,3,3,2,2,2,5,6,7]
|
记得@的用法吗? l@(x:xs) 表示 l 代表 (x:xs)
tails 和 inits
用tails实现字符串的search
1
|
search :: (Eq a) => [a] -> [a] -> Bool
|
首先 tails 执行得到 一个数组,例如 tails "abcde" 得到
1
|
["abcde","bcde","cde","de","e",""]
|
然后使用foldl进行累积。 上面的实现其实就是函数isInfixOf所做的事情, 类似的字符串函数还有 isPrefixOf 和 isSuffixOf
partition , span 和 break
1
|
hci> partition (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy"
|

浙公网安备 33010602011771号