haskell学习

2017-10-24  本文已影响0人  咣咣当

工具

haskell platform,直接百度安装.

打开控制台输入ghci即进入交互模式。

假如定义了myfunction.hs,在ghci中输入:l myfunction.hs便会进行加载。‘

细节

初学者第一个函数

doubleMe x = x * x.
创建test.hs,键入以上函数,加载方式为::l test.hs,之后就可使用。
也可以在test.hs键入两行函数:

doubleMe x y = x*y + x*y
doubleUs x y = doubleMe x y + doubleMe x y

之后再重新加载,两个函数都可以使用。

在haskell中if then else是一种表达式。如

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

可以看到,then esle是不可省略的,必须有一个确定的最终值。

首字母大写的函数是不允许的。

类似下面的没有参数的函数,其实就是定义了一个常量字符串:

someName = "hahahhahahahahahah..."

list

在ghci下使用let定义一个常量

let a = 1

range

list comprehension

定义集合的操作

tuple

tuple是元组。

type

typeclass

(一)

函数

succ 6 输出7 表示某个值的后继
min 4 5,max 4 5.

模式匹配

其实就是类似switch
如下代码:

lucky::(Integral a)=> a -> String
lucky 7 = "its 7"
lucky x = "its not 7"

如果匹配到7,则后续不执行。如果未匹配到,则所传参数绑定到x上。但是如果是下面的代码:

lucky::(Integral a)=> a -> String
lucky x = "its not 7"
lucky 7 = "its 7"

则报错,因lucky 7 已经被加载,lucky x已经包含了lucky 7。

可以通过这种方式实现递归:

factorial :: (Integral a) => a -> a
factorial 0 = 1
factorial n = n * factorial (n - 1)

_符号表示不关心值,如:

first :: (a,b,c) -> a
first (x,_,_) = x

可以通过:来匹配List,因为[a,b,c]本来就是a:b:c:[]的语法糖
a:b会将[1,2,3]匹配成1:[2,3],而如果匹配单元素List,可以写为(x:[]),匹配双元素List:(x:y:[]),也可以不加括号,写为[x][x,y],但是(x:y:_)必须加括号,注意此处加上括号并不是表示tuple.因为([1,2,3])还是一个数组,单元素的tuple其实毫无意义。
实现head:

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

error是一个函数,会导致程序崩溃。

实现length:

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

xs@(x:y:ys)类似这种形式的模式,xs就表示整体,如:

capital :: String -> String
capital "" = "Empty string, whoops!"
capital all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]

guard

类似if语句,模式匹配是匹配值,而guard则匹配bool

bmiTell :: (RealFloat a) => a -> String
bmiTell 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!"

与模式匹配不同的是,guard模式是通过判断表达式的真假来运作的。直到遇到一个为真的表达式,并且|必须与前边有缩进。

在定义函数的时候如func a b也可以这么定义a `func` b
guard也可以和模式匹配进行配合,如果guard没有匹配到结果,后续没有代码则报错,但是如果后续还有模式匹配的代码则继续执行,比如实现take:

myTake :: (Num b, Ord b) => [a] -> b -> [a]
myTake _ b
        | b <= 0 = []
myTake [] _ = []
myTake (x:xs) b = x : myTake xs (b-1)

guard后边跟着模式匹配,代码不会报错。

where

在guard模式中,可以通过where来引用某个复杂的变量值,这样就不用重复出现某个复杂的表达式了。如:

bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
    | 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!"
    where bmi = weight / height ^ 2

where也支持模式匹配,如:

where bmi = weight / height ^ 2
    (skinny, normal, fat) = (18.5, 25.0, 30.0)

所以下面的代码不难理解:

initials :: String -> String -> String
initials firstname lastname = [f] ++ ". " ++ [l] ++ "."
    where (f:_) = firstname
        (l:_) = lastname

where也可以定义函数:

calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi w h | (w, h) <- xs]
    where bmi weight height = weight / height ^ 2

let

格式:let [bindings] in [expressions] let in是一个表达式,其值是expressions表示的值,bindings中进行局部变量的定义。与where不同的是,let in是一个表达式,所以可以随处安放,同if else then,而where是一个语法结构,一般只用在guard后缀。

maax x = let y = 1 in y

let也可以定义局部函数:

[let square x = x * x in (square 5, square 3, square 2)]

定义多个名字,使用;隔开

(let a = 100; b = 200; c = 300 in a*b*c, let foo="Hey "; bar = "there!" in foo ++ bar)

使用模式匹配:

(let (a,b,c) = (1,2,3) in a+b+c) * 100

用在list中:

calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2]

(w, h) <- xs 这里无法使用 bmi 这名字,因为它在 let 绑定的前面。

case

case 是一个表达式,与switch相似:
格式为:

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

如:

head xs = case xs of [] -> error "error"
                      (x:_) -> x

模式匹配本质上就是case的语法糖。
上述代码写成模式匹配则为:

head [] = error "error"
head (x:_) = x

递归

haskell中实现while和for的方案就是递归。

实现取列表中最大值:

maximum' :: (Ord a) => [a] -> a
maximum' [] = error "maximum of empty list"
maximum' [x] = x
maximum' (x:xs)
    | x > maxTail = x
    | otherwise = maxTail
    where maxTail = maximum' xs

递归实现的快排,真是特妹的优雅!!

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

如实现的reverse函数:

myReverse :: [a] -> [a]
myReverse [] = []
myReverse (x:xs) = myReverse xs ++ [x]

一定要注意,最后一行为什么不写成:myReverse xs : x呢,原因是没有[1,2,3]:3这种写法,但是有[1,2,3] ++ [3]这种写法或者1:[1,2,3]

递归的固定模式可以描述成这样:先定义一个边界条件,再定义函数,让它从一堆元素中取一个并做点事情后,把剩余的元素重新交给该函数。

高端函数

指可以接受函数作为参数,也可以返回函数作为结果。

curried functions

原则上haskell的所有函数都只有一个参数,定义的函数传多个参数是怎么来的?
所有多个参数的函数都是curried function,如func a b传入两个参数,实际上是func a回传了一个函数,并将b传给该函数。
如:max::(Ord a)=> a->a->a可以看作max::(Ord a) => a -> (a -> a)
max a表示返回一个a->a类型的函数。那么max a b可以理解为 (max a) b

所以如果想要构造一个和7比较大小的函数,直接调用max 7即可,因为max 7会返回一个(a->a)的函数。 如下代码:

max7 :: (Ord a, Num a) => a -> a
max7 = max 7

所以此时max7为(a->a)的函数。
查看以下代码:

ghci> let multWithEighteen = multTwoWithNine 2
ghci> multWithEighteen 10
180

以上代码可以看出,一个参数没有传入全的函数会返回另一个函数,等待剩余的参数传递完毕。

中级函数也可以返回函数:

divideByTen :: (Floating a) => a -> a
divideByTen = (/10)

这个例子就可知道形如(/10) (+3) (++ "abc") (3:) (3+)都是函数。
同样的(*) (+) (++)也都是函数,不过这样函数的参数为两个。
(/10) 200200/10是等价的。而(200/) 10200/10也是等价的。

以下代码调用某个函数两次:

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

从代码中可以看出 第一个参数是(a->a)类型,也就是该类型的函数,在这括号是必须的,表示第一参数必须是一个函数,第二个参数可以是任意元素,最后一个参数返回某个元素。
那么调用:applyTwice (+3) 10 其实就是((+3) ((+3) 10))

map和filter

map:

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

map是结合高端函数和递归来实现的。

filter:

filter :: (a -> Bool) -> [a] -> [a]
filter _ [] = []
filter p (x:xs)
    | p x = x : filter p xs
    | otherwise = filter p xs

lambda

匿名函数,样式是\ 参数 -> 函数体,通常会用括号将lambda函数括起来,否则会引起歧义。
\x -> x + 3表示输入x输出x+3。同样lambda可以取多个参数:\a b -> a + b。再看个复杂点的例子:

addThree :: (Num a) => a -> a -> a -> a
addThree = \x -> \y -> \z -> x + y + z

这种样式也是可以。

其他高级函数

foldl其实就是Java stream中的reduce
foldl是折叠,将一个数组从前折到后,传入的第一个参数是函数,第二个参数是初始值,第三个参数为List,
foldl 是fold left, 而foldr则是fold right,即从右边开始折叠。
foldl1foldr1则与foldlfoldr相似,不过他们的初始值为数组的第一个元素(首个或末尾),只不过计算空List则会报错。

foldl (\x y -> x+y) 0 [1,2,3]
foldl (+) 0 [1,2,3]
let sum = foldl (+) 0 [1,2,3] in sum [1,2,3]

因为fold函数的特殊性,传入List传入Item,所以可以用来实现一些遍历的库函数,如max,min等。只要满足返回的结果不为List,都可以想办法完成。

scan函数与fold函数不同的是,scan会将每步的计算结果保存在List中,如:

scanl (+) 0 [1,2,3,4] 输出:[0,1,3,6]

其同样有scanl,scanr, scanl1,scanr1等函数

$操作符

$操作符的优先级最低,所以其可以充当(),如sqrt 3 + 4 + 5表示根3 + 4 + 5的值,如果我想取sqrt(3 + 4 +5)那么也可以写成sqrt $ 3+4+5其首先会计算符号右边的值。 同样的,如果有一些函数的括号特别多,就可以使用符号来简化代码:sum (map sqrt [1..130])可以写成sum $ map sqrt [1..130]

函数组合:

数学中的函数组合为:

image.png

在haskell中可以使用.号来表示,如:map (\x -> negate (abs x)) [5,-3,-6,7,-3,2,-19,24]表示将所有的x先获取绝对值,然后通过negate取负,那么使用函数组合则表示为:map (negate . abs) [5,-3,-6,7,-3,2,-19,24],这样表示的函数更为方便,但是注意函数组合的顺序。
函数组合可以构造更多的函数,形如f1 . f2 a的函数实际上和f1(f2 a)等价的。

模块

类比Java中的类,装在模块的方式是通过import
在某个.hs文件中使用import Data.List可以将Data.List模块装入,这个模块有很多操作List的方法。
如果使用ghci交互界面来装载模块,可以使用:m Data.List,使用:m Data.List Data.Map Data.Set装载多个函数。其实ghci初始的时候会装载Prelude模块,所以你能使用到filtermap等常用函数
import Data.List (nub,sort)只装载nub和sort函数
import Data.List hiding (nub)除了nub函数,其他都装载
import qualified Data.Map 关键字qualified表示,如果调用该模块中的某个与外部函数同名的函数,就必须加上Data.Map前缀。
import qualified Data.Map as M同名函数加M前缀即可:M.filter

Data.List

Data.List有很多方便处理List的函数,如map,filter等,为了方便,将Data.List中的一些函数直接加入到haskell中,所以调用的时候,就不用再写Data.List前缀。
以几个罕见函数举例:

有个函数叫on函数,其定义如下:

on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
f `on` g  \x y -> f (g x) (g y)

即把g函数的结果传递给f函数,所以(==) `on` (>0)就表示函数\x y -> x>0 == y >0 ,同理 compare `on` length就表示\x y -> length x `compare` length y

Data.Char

处理字符串的模块

import Data.Char
encode :: Int -> String -> String
encode salt msg = let msgs = map ord msg
        ¦       ¦     digs = map (+ salt) msgs
        ¦       ¦     in map chr digs
decode :: Int -> String -> String
decode salt msg = encode (-salt) msg

则输入encode 1 "abc"输出"bcd",同样decode 1 "bcd"输出"abc"

Data.Map

Map是key不重复一种KV键值对集合(List)。

Data.Set

Set中的数据是唯一的,元素是必须可排序的。注意其因为和Data.List很多的函数重复,所以导入的时候使用import qulified Data.Set as Set的方式。创建一个Set是通过fromList函数,其他函数就不列举了。

创建自己的模块

在根目录创建geometry.hs。

module Geometry
( sphereVolume,
sphereArea
) where

sphereVolume :: Float -> Float
sphereVolume radius = (4.0 / 3.0) * pi * (radius ^ 3)

sphereArea :: Float -> Float
sphereArea radius = 4 * pi * (radius ^ 2)

分级模块创建方式,创建geometry/sphere.hs
则在sphere.hs中的内容为:

module Geometry.Sphere
( volume,
area
) where

volume :: Float -> Float
volume radius = (4.0 / 3.0) * pi * (radius ^ 3)

area :: Float -> Float
area radius = 4 * pi * (radius ^ 2)

构造自己的Types和Typeclasses

Bool在标准函数库的定义为:

data Bool = False | True

构造自己的类型,一种方法就是使用data关键字。
如构造一个图形,该图形可以是Circle也可以是Rectangle,定义为:

data Shape = Circle Float Float Float | Rectangle Float Float Float Float

上述Circle表示圆形,后边的跟着的三个Float表示一个Circle由三个Float组成。
创建完Shape后会自动生成Circle及Rectangle类型,此时查看其类型声明为:

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

所以Circle 50 50就是返回一个Shape类型,到这就明白了,并不是先有Shape后有Circle,而是现有Circle后有了Shape
同样类型:data Maybe a = Nothing | Just aNothing并不是事先定义的,并且Nothing是一个值构造子,也不是类型。要创建个某个类型的值,必须使用后边的值构造子,即你不能使用Shape关键字来创建一个Shape类型。
构造的方式为:

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

注意看传入的Circle和Rectangele的方式不一样。
那么调用方式为:

ghci> surface $ Circle 10 20 30
ghci> surface $ Rectange 0 0 100 100

但是下面的调用时错误的

ghci>Circle 10 20 30 

因为Circle 10 20 30并不是Show类型,只有Show类型的数据才能被显示。
那么修改构造的方式为:

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

即在定义的后边加上deriving (Show).然后再调用就可以显示出来了。

还可以这么定义:

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

这样在定义函数的时候就应该是:

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

那么调用就变为:

surface (Rectangle (Point 0 0) (Point 100 200))

前边知道了如何导出函数的模块,那么这种数据定义的模块应该怎么导入呢,方式如下:

module Shapes
( Point(..)
, Shape(..)
, surface
, nudge
, baseCircle
, baseRect
) where

即数据定义使用Shape(..)的方式。..表示将Shape中的CircleRectangle类型都导出,这样外部就可以使用到这两个构造子,如果只导出Circle则需要写为Shape(Circle)

Record Syntax

定义一个人的名字,并且生成各种函数:

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

这样就生成了Person类型以及firstName``lastName..等等的函数.
创建的时候则为

> Person {firstName="q",lastName="xg",age=10,phoneNumber="123456"}

并且打印Person的时候,会将firstName等等显示出来。

Type parameters

类型参数,类似泛型,如:

data Maybe a = Nothing | Just a

如果传给Maybe的是Char,他就是Maybe Char类型。如:Maybe 'a'就是Maybe Char类型的。
Nothing也是Maybe a类型,所以可以是Maybe Int,也可以是Maybe String
再看一个例子:

data Vector a = Vector a a a deriving (Show)
vplus :: (Num t) => Vector t -> Vector t -> Vector t
...

Derived instances

上边有涉及到Eq,Ord,Num的typeclass等等类似Java interface的东东,比如Int属于Num,但是如何实现这些interface呢,用java表达就是如何implement,方法就是通过派生deriving,使用data创建类型的时候,后续跟上deriving Num就表明该类型属于Num,并自动加上对应的行为。
但是注意data创建的是instance,而不是typeclass,且等号左边是类型构造子,等号右边是值构造子,一般在创建某种类型的值时,使用的是值构造子,而类型构造子则用于函数声明等位置处,类型构造子无法来创建某一个值,一定要注意,比如下边创建了一个Person的类型,那么要创建一个Person类型的值,就不能使用Person创建,而是要使用等号右边的关键字(值构造子)创建.
只要派生为Eq类,那么定义的数据类型就有可比性:

data Person = Person{firstName :: String,lastName :: String,age::Int} deriving Eq

haskell会检查值构造子是否一致,再用==检查其中的所有数据(必须都是Eq的成员)是否一致。
同样也可以指定为多个类型:

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

若将一个类型指定派生为某个A类型,则该类型的所有参数必须都属于A类型,才可以进行派生。
举一个经常会遇到的例子:

data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday 
        deriving (Eq, Ord, Show, Read, Bounded, Enum)

每个值构造子都没有参数,因为派生了Eq,所以具有可比性,派生了Ord,所以可以比较大小,有Bounded,可以使用minBound获取下界,有了Enum,可以使用succ等函数进行枚举。

在data声明中,=左边是类型构造子,=右边用|分割的是值构造子,要注意区分。因为函数生命中只能填写类型,如果分不清楚,可能就将值构造子填入,导致死都不知道怎么死的。
注:不要在data中添加类型约束,即便看起来没问题。

Type synonyms

给某个类型提供别名:

type String = [Char]

又如:

type PhoneNumber = Stirng
type Name = String
type PhoneBook = [(String,String)]

那么定义某个函数则为:

inPhoneBook::Name -> PhoneNumber -> PhoneBook -> Bool

而如果不定义别名,则该函数为:

inPhoneBook::String -> String -> [(String,String)] -> Bool

定义别名也可以有参数:

type AssocList k v = [(k,v)]

类型别名也可以定义不全的类型构造子.
类型别名一般可以用于函数类型声明或类型注释上,如果要创建一个新类型,不能用类型名+参数的方式去创建,一定要明白类型构造子和值构造子的区别。
类似函数在定义的时候进行声明,变量在定义的时候也可以进行声明,所以将来::看成通用的一类看待会更好理解这门语言。

a::Int
a=1

是可以工作的,同样函数在定义的时候也是这种格式:

test::a->a
test a = a+1

只不过这个地方用到了泛型,你也可以这样声明

test :: Int -> Int
test a=a+1

递归地定义数据结构

如List [1]1:[]的语法糖,[1,2]1:2:[][1,2,3]1:2:3:[]
可以看到List的类型类似这样x:listx其中x是匹配到的第一个元素,而listx是一个匹配的新的list,每个List类型都可以匹配为x:listx类型,并且每个listx都可以匹配为x:listx类型,除了基础类型[],所以在定义List的时候就可以通过递归来进行定义,如将[]替换为empty,:替换为Cons,那么定义List就可以是这样:
data List a = Empty | Cons a List a deriving (Show ,Read,Eq,Ord)
那么在创建List的时候就可以这样,1 `Cons` Empty注意最后的Empty就已经表明其是一个List,所以创建的递归数据类型一般都是需要最基础的类型,比如Empty,比如原始List的[]等。
Cons也可以替换为一个符号,如

data List a = Empty | a :-: (List a) deriving (Show, Read,Eq, Ord)

那么如果要定义该符号的优先级,需要加上infixr关键字,其后的数字表示优先级:

infixr 5 :-:
data List a = Empty | a :-: List a deriving (Show, Read,Eq, Ord)

那么如果要做模式匹配的话,是可以这样做的:

a:-:b = xxxx

原因就是:-:是构造子,而模式匹配就是通过构造子来进行匹配的,同理[]也是构造子,:也是构造子,所以你可以通过x:xs来进行匹配,注意模式匹配中匹配的都是值构造子,一定不要写类型。
二叉树

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
treeInesrt 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 left
    | x > a = treeElem right
上一篇 下一篇

猜你喜欢

热点阅读