haskell学习总结(一)::初级篇

Raven's Blog

Home Page View on GitHub Send Email

haskell学习总结(一)::初级篇

前段时间看完了haskell趣学指南,了解了一些haskell的基本语法和函数式编程的皮毛。不过说实话,haskell的学习曲线确实比较陡,加上我大学学的数学理论基本都还给老师了,所以一些概念理解起来着实困难。

俗话说好记性不如烂笔头,现在就将已经学到的haskell知识整理总结一下

搭建开发环境

可以从haskell官网中找到haskell platform的下载地址。 haskell的开发环境主要包括:

Hello World


{-
    helloworld.hs
-}
main::IO ()
main = do
    -- display hello world
    putStrLn "hello world"

学习语言的第一件事当然就是写出HelloWorld了,将上面一段代码保存在helloworld.hs文件中,然后执行


ghc -o helloworld helloworld.hs
./helloworld

就会在屏幕中显示出"hello world"

基本语法

注释

单行注释: --

多行注释 {- -}

多行注释还可以声明一些GHCi扩展,比如:

{-#LANGUAGE OverloadedStrings#-}

{-#LANGUAGE QuasiQuotes#-}

{-#LANGUAGE TemplateHaskell#-}

{-#LANGUAGE TypeFamilies#-}

以下是另一种写法

{-# OPTIONS_GHC -XTypeFamilies -XTemplateHaskell -XQuasiQuotes #-}

在GHCI控制台情况下要使用扩展,可以使用:set命令:


Prelude>:set -XOverloadedStrings
Prelude>:set -XQuasiQuotes
Prelude>:set -XTemplateHaskell
Prelude>:set -XTypeFamilies

或者在启动GHCi的时候加上-X


ghci -XOverloadedStrings -X TemplateHaskell -ddump-splices
# If you want to see the expansion of splices, 
# use the flag -ddump-splices when starting GHCi

关于扩展的资料详见24 Days of GHC Extensions

类型

基本类型

Haskell是静态类型语言,但是有非常强大的类型推导,所以不需要向java或者C语言那样,必须写明声明变量的类型。 Haskell 编译器可以自动推断出程序中几乎所有表达式的类型[注:有时候要提供一些信息,帮助编译器理解程序代码]。这个过程被称为类型推导(type inference)。 虽然 Haskell 允许我们显式地为任何值指定类型,但类型推导使得这种工作通常是可选的,而不是非做不可的事。。 haskell中的基本类型主要有:

单个 Unicode 字符。

表示一个布尔逻辑值。这个类型只有两个值: True 和 False 。

带符号的定长(fixed-width)整数。这个值的准确范围由机器决定:在 32 位机器里, Int 为 32 位宽,在 64 位机器里, Int 为 64 位宽。Haskell 保证 Int 的宽度不少于 28 位。

不限长度的带符号整数。 Integer 并不像 Int 那么常用,因为它们需要更多的内存和更大的计算量。另一方面,对 Integer 的计算不会造成溢出,因此使用 Integer 的计算结果更可靠。

用于表示浮点数。长度由机器决定,通常是 64 位。(Haskell 也有 Float 类型,但是并不推荐使用,因为编译器都是针对 Double 来进行优化的,而 Float 类型值的计算要慢得多。)

列表

列表容器要求内部的元素类型完全一致[Int] 表示Int列表类型 [Char]表示字符列表也就是String类型,其他类型的列表以此类推

类型别名

haskell中可以用上C语言的typeof类似的功能,给现有的类型起个别名,比如:


type Age = Int
type String = [Char]
type IntList = [Int]

上面第二行,其实haskell内部已经帮我们做了

自定义新的数据类型

使用data关键字可以定义新的数据类型


-- 定义了一种新的类型Test
-- 但是他没有构造函数,只能用作类型参数
data Test

--定义了一种新的类型Person,构造函数也是Person
data Person = Person

以上代码定义了一个Person类型的数据,然后这个类型中没有保存任何数据


data Person = Person String Int

这样定义Person类型,可以保存两种数据:String类型和Int类型 注意:上面代码等号左边的Person是类型,右边Person叫做构造函数(constructor),左右并不一定要名字一致,而且构造函数可以有多个,之间用"|"隔开


data Person = Student String Int | Teacher String Int Double

Person是类型,Student是一个构造函数(Student::String->Int->Person),Teacher也是一个构造函数(Teacher::String->Int->Double->Person)


let person1 = Student "XiaoMing" 12
let person2 = Teacher "LaoZhang" 30  5000

这样person1和person2就都是Person类型数据了 若要取Person类型中的名字,我们可以定义这样的函数


getName::Person->String
getName (Student name _) = name
getName (Teacher name _  _) = name

getName (Teacher "LaoZhang" 30 5000)

为了自定义类型用起来方便,haskell还提供了另外一种构造函数(haskell的语法糖)


-- 注意字段之间的逗号
data Person = Student{name::String,age::Int}
            |Teacher{name::String,age::Int,wage::Double}
let person1 = Student{name="XiaoMing", age=12}
let person2 = Teacher{name="LaoZhang", age=30, wage=5000}
name person2

用这种构造函数,haskell会自动生成相应的取值函数,比如name::Person->String age::Person->Int

类型也可以带参数(不是构造函数带参数哦~),就像这样:


data Entry a b = Entry{key::a,value::b}
let p1 = Entry 1 "Foo"
let p2 = Entry "Bar" 3.5
let p3 = Entry{key="A",value="HHHH"}
let p4 = Entry (1::Double)  2
let p5 = Entry{key=1::Double, value=2}

上面的Entry中可以保存任何类型的a和b

除了上面标准的data用法外,还可以使用Generalised algebraic datatype(GADT)


{-# LANGUAGE GADTs #-}
data Person where
    Student::String->Int->Person
    Teacher::String->Int->Double->Person


data Entry a b where
    --前面的Entry是构造函数名 后面的Entry是定义的new type名称
    Entry::a->b->Entry 

--使用GADTs还可以添加更多的类型限定
data E a where
    A::Eq b=>b->E b

Record Wildcards扩展,可以让代码更简洁


{-# LANGUAGE RecordWildCards    #-}
data Person = Person{
        name::String,
        age::Int
}deriving(Show)

getPerson::IO Person
getPerson = return $ Person "Duyang" 13

main::IO ()
main = do
        -- 不使用扩展
        person <- getPerson
        putStrLn $ show $ name person
        putStrLn $ show $ age person
        -- 使用扩展,其中person2代表匹配到的Person类型整体
        person2@Person {..} <- getPerson
        putStrLn $ show name
        putStrLn $ show age
        putStrLn $ show person2                 

类型类

haskell中用data关键字可以像c语言中定义struct一样,同时也提供一种类似java接口的类型类,使用class关键字

一般情况


--跟在class之后的Animal是这个类型类的名字,之后的a是这个类型类的实例类型(instance type)
-- 注意 默认情况下(无扩展)haskell的类型类必须有一个类型变量
-- 也就是在Prelude中:k Animal结果必须是 Animal :: *->Constraint
class Animal a where
    eat::a->String->String
data Pet = Cat | Dog

--相当于java中Pet类实现Animal接口
instance Animail Pet where
    eat Cat food = "Cat eat " ++ food
    eat Dog food = "Dog eat " ++ food

let pet1 = Cat
eat pet1 "fish"
let pet2 = Dog
eat pet2 "meat"

扩展:Nullary Type Classes


{-# LANGUAGE NullaryTypeClasses  #-}
-- 使用上面这个扩展之后,没有类型变量的类型类变得合法了

-- 这里的类型类是没有类型参数的
-- :k Animal的结果是:Animal :: Constraint
class Animal where
    eat::String->String

-- 在需要的时候实现Animal
instance Animal where
    eat food = "eat " ++ food

main::IO ()
main = do
    putStrLn $ eat "Apple"
    -- It will display:  eat Apple

扩展:Multi-parameter Type Classes


{-# LANGUAGE MultiParamTypeClasses #-}
-- 使用上面这个扩展之后,有多个类型变量的类型类变得合法了

-- 类型类可以有多个类型变量(这里是3个)
-- :k Animal的结果是 Animal :: * -> * -> * -> Constraint
class Animal a b c where
        act::a->b->c->String

data Pet = Dog | Cat

data Food = Meat | Fish

data Action = Eat | Sleep

instance Animal Pet Action Food where
        act Dog Eat Meat = "Dog eat meat..."
        act Cat Eat Fish = "Cat eat fish..."
        act _ _ _ = "Invalidate!"

main::IO ()
main = do
        putStrLn $ act Dog Eat Meat
        -- Dog eat meat...
        putStrLn $ act Cat Eat Fish
        -- Cat eat fish...
        putStrLn $ act Dog Eat Fish
        -- Invalidate!
        putStrLn $ act Cat Eat Meat
        -- Invalidate!
        putStrLn $ act Cat Sleep Fish
        -- Invalidate!

函数

函数声明

比如上面helloworld程序中:


main::IO ()

表示main函数是没有参数,并且返回IO ()类型的函数 再来看看下面的函数


add::Int->Int->Int

这条语句声明了一个add函数,它接收两个Int类型的参数,返回一个Int类型的结果

当然以上是一种理解方式,对于函数式编程思想,应该是这样理解的:

add函数,接收一个Int类型的参数,返回一个偏函数(返回的函数类型是接收Int类型,产生Int结果) 可以这样认为:Haskell中的函数都是只有一个参数,并且只产生一个结果

另外注意一点:haskell的函数声明不是必须的,函数类型可以推导出来,也就是说main::IO ()这句可以省略:)

函数的实现

如上面helloworld程序


main = do
    putStrLn "hello world"

这就是函数的实现 函数名 [参数列表]= 函数体 再来看看add的实现:


add x y = x + y

可以看到add接受两个参数 x y 然后将x+y的结果作为结果 当然,一个函数可以有多个实现,比如这样:


add 0 0 = 100
add x y = x + y

对于上面add函数

add 0 0的结果是 100

add 0 1的结果是 1

这就叫函数的模式匹配(Pattern matching)

模式匹配一般也可以用Case expressions来实现:


add x y = case  x y of 
            0 0 -> 100
            otherwise -> x+y    

再看如下代码


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

guard 由跟在函数名及参数后面的竖线标志,通常他们都是靠右一个缩进排成一列。一个 guard 就是一个布尔表达式,如果为真,就使用其对应的函数体。如果为假,就送去见下一个 guard,如之继续。如果我们用 24.3 呼叫这个函数,它就会先检查它是否小于等于 18.5,显然不是,于是见下一个 guard。24.3 小于 25.0,因此通过了第二个 guard 的检查,就返回第二个字串。

分支控制

上面的case of 与Guard可以看做其他语言的case,那么haskell有没有if呢? haskell中也有if,但是与java 或者c等其他面向对象、面向流程的语言来说,haskell的if else是表达式(expression不是statement),主要表现在else是不可以省略的。

if的完整形式是: if ... then ... else ...


add x y = if x==0 && y==0
            then 100
            else  x + y

Lambda

haskell中也支持lambda语法(可以认为是匿名函数啦)


-- 下面的语法定义了一个lambda表达式,
-- 最终结果将是101
(\x->x+1) 100

-- 下面定义了一个接受两个参数的lambda表达式,
-- 两个参数之间以空格分隔, 
-- 最终结果是3
(\x y->x+y) 1 2

-- 接受两个参数的lambda的另外一种写法,
-- 注意\x-> \y-> 之间的空格, 
-- 最终结果是3
(\x-> \y->x+y) 1 2


-- lambda 的应用
-- :t map
-- map::(a->b)->[a]->[b]
map (\x->x+100) [1..10]
--结果是 [101,102,...110]

注意上面接受两个参数的第二种写法一定要注意空格,否则: img

列表推导式(List comprehension)

python语言可以这样写:


[(x,y) for x in xrange(10) for y in xrange(10)]

haskell语言中语法如下:


[(x,y)|x<-[0..9], y<-[0..9]]

自定义操作符

定义一个运算符, 要说明它的结合性和优先级.

infixr 右结合

infixl 左结合

infix 不结合

左右都可结合的运算符如 +, * 定义为左结合即可.

优先级从0到9, 0最低, 9最高

已定义的运算符有:

level 9 . and !!

level 8 ^

level 7 *, /, `div`, `rem` and `mod`

level 6 + and -

level 5 :, ++ and \

level 4 ==, /=,<, <=, >, >=, `elem` and `notElem`

level 3 &&

level 2 ||

level 1 (not used in the prelude)

举例:

    
   infixr 3  &&                       
   (&&)  :: Bool -> Bool -> Bool
   False && x   = False
   True  && x   = x         

   -- 先定义&&的结合性和优先级, 然后象定义函数一样定义它的功能.

   -- 运算符用括号括起来, 可以当作函数使用, 比如:

   map (3+) [1,2,3]

   map (+3) [1,2,3]

   -- 函数名用左引号`引起来, 也可以声明为运算符, 比如:

   fac n = product [1..n]

   infix 5 !^!, `choose`
   (!^!), choose :: Int->Int->Int                   
   n `choose` k = fac n `div` (fac k * fac (n-k))
   n !^! k = fac n `div` (fac k * fac (n-k))     

   -- 有了这些定义后,

   choose 5 2
   (!^!)  5 2
   5   !^!  2
   5 `choose` 2

   -- 都给出答案10.  

模块(Module)

Module是haskell源码的组织方式,可以将功能解耦到不同的Module中。

Module可以是被组织成树形,与树形文件系统基本一致,让我们来看个具体例子:

目录结构如下:


`
|-----Dict0                           #第一级目录
|       |-----Dict1                  #第二级目录
|       |       `-----Test0.hs        #Haskell源码文件Test0.hs
|       `-----Test1.hs                #Haskell源码文件Test1.hs
|------Test2.hs                       #Haskell源码文件Test2.hs
`------Main.hs                        #Haskell入口所在源码文件Main.hs

Test0.hs内容如下:


-- Test0.hs
module Dict0.Dict1.Test0 where
say::String
say = "hello world"

foo::Int
foo = 42


Test1.hs内容如下:


-- Test1.hs
module Dict0.Test1 where

bar::String
bar = "Bar!!!!"

say::String 
say = "hello world!In Test1"


Test2.hs内容如下:


-- Test2.hs
module Test2(Tree(Leaf, TreeNode), Node(..), test) where
-- Tree(Leaf, TreeNode) 表示Tree类型的Leaf TreeNode两个构造函数被export
-- Node(..) 表示Node类型全部构造函数都被export
data Node = MyInt Int|MyText String deriving(Show)

data Tree = Leaf|TreeNode Node Tree Tree |PrivateLeaf deriving(Show)

test::String
test = "Hi this is TEST"

-- test2相当于Test2模块的私有函数,不能被其他模块import
test2::String
test2 = "Hi this is TEST2"

Main.hs内容如下:


-- Main.hs
-- import Dict0.Dict1.Test0 -- Dict0.Dict1.Test0中的say 和 foo都可用
import Dict0.Dict1.Test0 (say)  -- Dict0.Dict1.Test0 中的foo将不可用
-- import qualified Dict0.Test1  -- 不能直接使用Test1中的函数,需要写全
import qualified Dict0.Test1 as T -- T作为Dict0.Test1的别名
import Test2

main::IO ()
main = do
    putStrLn say
    -- putStrLn foo  --错误,作用域内没有foo函数
    putStrLn T.say
    putStrLn test
    putStrLn $ show Leaf
    putStrLn $ show $ TreeNode (MyInt 42) Leaf Leaf
    -- putStrLn $ PrivateLeaf --错误,作用域内没有PrivateLeaf构造函数
    -- putStrLn test2 -- 错误,作用域内没有test2

Prelude tips

Prelude是haskell语言的命令行交互界面,这里记录一些常用的操作吧:

it is a special variable in ghci that allows us to reference the last computed value.


haskell学习总结

联系作者:aducode@126.com
更多精彩文章请点击:http://aducode.github.io