目录
haskell的一等公民是function(函数,体现形式就是method),还有一个核心的概念是type(类型),所以haskell是用方法联系着类型,据说一心想干翻范畴论。我觉得可以这样讲:haskell是“面向函数编程”,而函数的形参类型是haskell中的各种强类型,例如:Int这种代数类型(太多值,范围[-2^29 .. 2^29-1]),Bool这种代数类型(俩值,False 或 True),Num这种类型类(可以有代数类型对它进行实现)也可以,传参就传值(代数类型的值)。
构造代数类型
以下简称类型。
data Bool = False | True
data关键字,=的左端标明类型的名称即Bool
,=
的右端就是值构造子(Value Constructor),它们明确了该类型可能的值。|
读作“或”,所以可以这样阅读该声明:Bool
类型的值可以是True或False。类型名和值构造子的首字母必大写。
data Shape = Circle Float Float Float | Rectangle Float Float Float Float
Circle
的值构造子有三个参数,都是Float,Rectangle
的值构造子有四个Float类型参数。值构造子就跟普通函数并无二致。
ghci> :t Circle
Circle :: Float -> Float -> Float -> Shape
ghci> :t Rectangle
Rectangle :: Float -> Float -> Float -> Float -> Shape
surface函数:
surface :: Shape -> Float
surface (Circle _ _ r) = pi * r ^ 2
surface (Rectangle x1 y1 x2 y2) = (abs $ x2 - x1) * (abs $ y2 - y1)
ghci> surface $ Circle 10 20 10
314.15927
ghci> surface $ Rectangle 0 0 100 100
10000.0
该函数取一个Shape值并返回一个Float值。写Circle -> Float
是不可以的,因为Circle并非类型,真正的类型应该是Shape。这与不能写True->False
的道理是一样的。再就是,我们使用的模式匹配针对的都是值构造子。之前我们匹配过[]
、False
或5
,它们都是不包含参数的值构造子。
类型别名
type String = [Char] --String类型是char数组类型的别名
type SimpleState s a = s -> (a, s) -- 这个SimpleState s a带参数类型的类型是匿名函数类型的别名
Record Syntax
data Person = Person String String Int Float String String deriving (Show)
ghci> let guy = Person "Buddy" "Finklestein" 43 184.2 "526-2928" "Chocolate"
ghci> guy
Person "Buddy" "Finklestein" 43 184.2 "526-2928" "Chocolate"
获取Person类型实例的各个参数值的函数: (很无聊)类似java中的get
firstName :: Person -> String
firstName (Person firstname _ _ _ _ _) = firstname
lastName :: Person -> String
lastName (Person _ lastname _ _ _ _) = lastname
age :: Person -> Int
age (Person _ _ age _ _ _) = age
height :: Person -> Float
height (Person _ _ _ height _ _) = height
phoneNumber :: Person -> String
phoneNumber (Person _ _ _ _ number _) = number
flavor :: Person -> String
flavor (Person _ _ _ _ _ flavor) = flavor
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"
Record Syntax写法:(deriving 自动派生一节讲)
--Record Syntax式定义
data Person = Person { firstName :: String
, lastName :: String
, age :: Int
, height :: Float
, phoneNumber :: String
, flavor :: String
} deriving (Show)
--通过Record Syntax,haskell就自动生成了这些访问器函数:
--firstName,lastName,age,height,phoneNumber和flavor
ghci> :t flavor
flavor :: Person -> String
ghci> :t firstName
firstName :: Person -> String
另一个例子:
data Car = Car String String Int deriving (Show)
ghci> Car "Ford" "Mustang" 1967
Car "Ford" "Mustang" 1967
--Record Syntax式构造Car的实例
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}
类型参数
data Maybe a = Nothing | Just a
a就是个类型参数,类似泛型。 如,Just 'a'
的类型就是Maybe Char。
ghci> Just "Haha"
Just "Haha"
ghci> Just 84
Just 84
ghci> :t Just "Haha"
Just "Haha" :: Maybe [Char]
ghci> :t Just 84
Just 84 :: (Num t) => Maybe t -- “(Num t) =>” 对t的类型约束
ghci> :t Nothing
Nothing :: Maybe a
ghci> Just 10 :: Maybe Double
Just 10.0
另一个例子:
data Car = Car { company :: String
, model :: String
, year :: Int
} deriving (Show)
--改成:
data Car a b c = Car { company :: a
, model :: b
, year :: c
} deriving (Show)
tellCar :: Car -> String
tellCar (Car {company = c, model = m, year = y}) = "This " ++ c ++ " " ++ m ++ " was made in " ++ show y
ghci> let stang = Car {company="Ford", model="Mustang", year=1967}
ghci> tellCar stang "This Ford Mustang was made in 1967"
--another test
tellCar :: (Show a) => Car String String a -> String
tellCar (Car {company = c, model = m, year = y}) = "This " ++ c ++ " " ++ m ++ " was made in " ++ show y
ghci> tellCar (Car "Ford" "Mustang" 1967)
"This Ford Mustang was made in 1967"
ghci> tellCar (Car "Ford" "Mustang" "nineteen sixty seven")
"This Ford Mustang was made in \"nineteen sixty seven\""
ghci> :t Car "Ford" "Mustang" 1967
Car "Ford" "Mustang" 1967 :: (Num t) => Car [Char] [Char] t
ghci> :t Car "Ford" "Mustang" "nineteen sixty seven"
Car "Ford" "Mustang" "nineteen sixty seven" :: Car [Char] [Char] [Char]
构造类型类
类型类定义了一系列函数,这些函数对于不同数据类型(类型类的实例)使用不同的函数实现。
class BasicEq a where
isEqual :: a -> a -> Bool
class 是关键字,a是类型参数,它是这个BasicEq 类型类的实例类型(instance type)。 以下代码将 Bool 类型作为 BasicEq 的实例类型,实现了 isEqual 函数:
instance BasicEq Bool where
isEqual True True = True
isEqual False False = True
isEqual _ _ = False
--
data Bool = False | True
在 ghci 里验证这个程序:
*Main> isEqual True True
True
*Main> isEqual False True
False
如果试图将不是 BasicEq 实例类型的值作为输入调用 isEqual 函数,那么就会引发错误:
*Main> isEqual "hello" "moto"
<interactive>:5:1:
No instance for (BasicEq [Char])
arising from a use of `isEqual'
Possible fix: add an instance declaration for (BasicEq [Char])
In the expression: isEqual "hello" "moto"
In an equation for `it': it = isEqual "hello" "moto"
如下构造,只要实现这两个函数中的一个,就可以顺利使用这两个函数:
class BasicEq a where
isEqual :: a -> a -> Bool
isEqual x y = not (isNotEqual x y)
isNotEqual :: a -> a -> Bool
isNotEqual x y = not (isEqual x y)
instance BasicEq Bool where
isEqual False False = True
isEqual True True = True
isEqual _ _ = False
Prelude> :load BasicEq_3.hs
[1 of 1] Compiling Main ( BasicEq_3.hs, interpreted )
Ok, modules loaded: Main.
*Main> isEqual True True
True
*Main> isEqual False False
True
*Main> isNotEqual False True
True
内置的Eq 类型类的定义:
class Eq a where
(==), (/=) :: a -> a -> Bool
-- Minimal complete definition:
-- (==) or (/=)
x /= y = not (x == y)
x == y = not (x /= y)
Show内置类型学类定义了show方法,Read 内置类型学类定义了read方法,可用于(反)序列化:
Main> :type show
show :: Show a => a -> String
Prelude> :type read
read :: Read a => String -> a
他俩的实例类型举例:
Main> data Color = Red | Green | Blue;
Main> show Red
<interactive>:10:1:
No instance for (Show Color)
arising from a use of `show'
Possible fix: add an instance declaration for (Show Color)
In the expression: show Red
In an equation for `it': it = show Red
Prelude> Red
<interactive>:5:1:
No instance for (Show Color)
arising from a use of `print'
Possible fix: add an instance declaration for (Show Color)
In a stmt of an interactive GHCi command: print it
-- Color类型实例化类型类Show
instance Show Color where
show Red = "Color 1: Red"
show Green = "Color 2: Green"
show Blue = "Color 3: Blue"
Prelude> read "3"
<interactive>:5:1:
Ambiguous type variable `a0' in the constraint:
(Read a0) arising from a use of `read'
Probable fix: add a type signature that fixes these type variable(s)
In the expression: read "3"
In an equation for `it': it = read "3"
Prelude> (read "3")::Int
3
Prelude> :type it
it :: Int
Prelude> (read "3")::Double
3.0
Prelude> :type it
it :: Double
自动派生
Haskell 编译器可以将类型派生(derivation)为 Read 、 Show 、 Bounded 、 Enum 、 Eq 和 Ord 这些内置类型类的实例。
接着上面的一些例子:
data Color = Red | Green | Blue
deriving (Read, Show, Eq, Ord)
*Main> show Red
"Red"
*Main> (read "Red")::Color
Red
*Main> (read "[Red, Red, Blue]")::[Color]
[Red,Red,Blue]
*Main> Red == Red
True
*Main> Data.List.sort [Blue, Green, Blue, Red]
[Red,Green,Blue,Blue]
*Main> Red < Blue
True
这个例子也是接着上面的例子:
data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)
所以才可以在ghci中显示:
ghci> Circle 10 20 5
Circle 10.0 20.0 5.0
ghci> Rectangle 50 230 60 90
Rectangle 50.0 230.0 60.0 90.0
接上面的例子继续举例:
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)
ghci> surface (Rectangle (Point 0 0) (Point 100 100))
10000.0
ghci> surface (Circle (Point 0 0) 24)
1809.5574
另一个例子:
-- 记录语法构造Person这个数据类型
data Person = Person { firstName :: String
, lastName :: String
, age :: Int
} deriving (Eq)
ghci> let mikeD = Person {firstName = "Michael", lastName = "Diamond", age = 43}
ghci> let adRock = Person {firstName = "Adam", lastName = "Horovitz", age = 41}
ghci> let mca = Person {firstName = "Adam", lastName = "Yauch", age = 44}
ghci> mca == adRock
False
ghci> mikeD == adRock
False
ghci> mikeD == mikeD
True
ghci> mikeD == Person {firstName = "Michael", lastName = "Diamond", age = 43}
True
Person如今已经成为了Eq的成员,我们就可以将其应用于所有在类型声明中用到Eq类约束的函数了,如elem。
ghci> let beastieBoys = [mca, adRock, mikeD]
ghci> mikeD `elem` beastieBoys
True
show和read可以(反)序列化一个类型实例。
data Person = Person { firstName :: String
, lastName :: String
, age :: Int
} deriving (Eq, Show, 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}
ghci> read "Person {firstName =\"Michael\", lastName =\"Diamond\", age = 43}" == mikeD
True
例子:
data Bool = False | True deriving (Ord)
ghci> True `compare` False
GT
ghci> True > False
True
ghci> True
False
如果Maybe a数据类型也是类型类Ord的实例,则:
ghci> Nothing
True
ghci> Nothing > Just (-49999)
False
ghci> Just 3 `compare` Just 2 -- Just 3 or Just 2 :: (Num t) => Maybe t
ghci> Just 100 > Just 50 -- 先比较Nothing 和 Just,再比较 t
True
haskell的类型总结:haskell有内置类型类,如:Eq,可以自定义类型类,如:文中;类型类可以派生出它的类型实例,就是一般称为数据类型,haskell有内置数据类型,如:Int,Bool,可以自定义数据类型,如:Person;数据类型有数据类型的实例,我们可以称它为类型的值,如:True或False。
或这样和java对比,有待商榷,理解有误还请指出。类型类与接口(抽象类),类型与父类(抽象类),值(类型的实例)与子类这样对应。
newtype,data,type
Haskell 提供我们另外一种方式来创建新类型,即采用 newtype
关键字。
-- file: ch06/Newtype.hs
data DataInt = D Int
deriving (Eq, Ord, Show)
newtype NewtypeInt = N Int
deriving (Eq, Ord, Show)
*Main> N 1 < N 2
True
*Main> N 313 + N 37
<interactive>:9:7:
No instance for (Num NewtypeInt) arising from a use of ‘+’
In the expression: N 313 + N 37
In an equation for ‘it’: it = N 313 + N 37
如果newtype不使用自动派生来公开基础类型(本例中Int)对类型类的实现(Num),我们可以自由地编写新实例类型去实现或保留类型类的未实现(data也可以这样公开) 。比起 data
关键字,它有更多的限制于其使用上。 说白了, newtype
只能有一个值构造子,并且那个构造子只有一个参数(field)。这使得在运行的时候,无论在时间还是空间上都更有效率。
-- file: ch06/NewtypeDiff.hs
-- 可以:任意数量的构造器和字段(这里的两个Int为两个字段(fields))
data TwoFields = TwoFields Int Int
-- 可以:恰一个字段
newtype Okay = ExactlyOne Int
-- 可以:类型变量是没问题的
newtype Param a b = Param (Either a b)
-- 可以:记录语法是友好的
newtype Record = Record {
getInt :: Int
}
-- 不可以:没有字段
newtype TooFew = TooFew
-- 不可以:多于一个字段
newtype TooManyFields = Fields Int Int
-- 不可以:多于一个构造器
newtype TooManyCtors = Bad Int
| Worse Int
为了理解不同点,看:
--如果在运行时 undefined 被求值会导致崩溃
Prelude> undefined
*** Exception: Prelude.undefined
-- 模式匹配 DataInt类型的 D Int 值构造子 ,构造子也是函数
*Main> case (D undefined) of D _ -> 1
1
*Main> case undefined of D _ -> 1
*** Exception: Prelude.undefined
-- 模式匹配 NewtypeInt 类型的 N Int 值构造子 ,构造子也是函数
*Main> case (N undefined) of N _ -> 1
1
*Main> case undefined of N _ -> 1
1
没有崩溃!因为newtype
的值构造子,只用于编译时,在运行时是不存在的。在运行时,已经没有NewtypeInt
类型的值构造器N
了。
对“N _
”进行匹配,事实上就是对“_
”进行匹配。而“_
”总是会满足匹配条件,所以undefined
是不需要被求值的。
当我们在表达式中应用N Int构造子时,就我们和编译器而言,我们意愿是强制表达式类型从Int类型转为NewtypeInt类型,但在运行时绝对不会发生任何事情。类似地,当我们在模式中匹配N Int构造子时,我们强制表达式从NewtypeInt类型转到Int,但同样在运行时不涉及任何开销。
再总结:命名类型的三种方式 这里简要回顾一下Haskell为类型引入新名称的三种方法。1.data关键字引入了一种真正新的代数数据类型。 2. type关键字为我们提供了用于现有类型的同义词,我们可以互换地使用该类型及其同义词。 3.newtype关键字为现有类型提供了不同的标识,基础原始类型和新类型不可互换。
二三函数
shiftL
函数实现逻辑左移, (.&.)
实现二进制位的并操作, (.|.)
实现二进制位的或操作, ord
函数则返回给定字符对应的编码值。
-- file: ch04/Adler32.hs
import Data.Char (ord)
import Data.Bits (shiftL, (.&.), (.|.))
base = 65521
adler32 xs = helper 1 0 xs
where helper a b (x:xs) = let a' = (a + (ord x .&. 0xff)) `mod` base
b' = (a' + b) `mod` base
in helper a' b' xs
helper a b [] = (b `shiftL` 16) .|. a
-- code by java
{-public class Adler32
{
private static final int base = 65521;
public static int compute(byte[] data, int offset, int length)
{
int a = 1, b = 0;
for (int i = offset; i < offset + length; i++) {
a = (a + (data[i] & oxff)) % base
b = (a + b) % base;
}
return (b << 16) | a;
}
}
-}
传入参数的数量,少于函数所能接受参数的数量,这种情况被称为函数的部分应用(partial application of the function)函数.
Prelude> :type zip3
zip3 :: [a] -> [b] -> [c] -> [(a, b, c)]
Prelude> :type zip3 "foo"
zip3 "foo" :: [b] -> [c] -> [(Char, b, c)]
Prelude> :type zip3 "foo" "bar"
zip3 "foo" "bar" :: [c] -> [(Char, Char, c)]
Prelude> :type zip3 "foo" "bar" "quux"
zip3 "foo" "bar" "quux" :: [(Char, Char, Char)]
在上面的例子中, zip3 "foo"
就是一个部分函数,它以 "foo"
作为第一个参数,部分应用了 zip3
函数;而 zip3 "foo""bar"
也是另一个部分函数,它以 "foo"
和 "bar"
作为参数,部分应用了 zip3
函数。
只要给部分函数补充上足够的参数,它就可以被成功求值:
Prelude> let zip3foo = zip3 "foo"
Prelude> zip3foo "bar" "quux"
[('f','b','q'),('o','a','u'),('o','r','u')]
Prelude> let zip3foobar = zip3 "foo" "bar"
Prelude> zip3foobar "quux"
[('f','b','q'),('o','a','u'),('o','r','u')]
Prelude> zip3foobar [1, 2, 3]
[('f','b',1),('o','a',2),('o','r',3)]
部分函数的应用被称为柯里化(currying).
-- file: ch04/niceSum.hs
niceSum :: [Integer] -> Integer
niceSum xs = foldl (+) 0 xs
实际上,并不需要完全应用 foldl
[译注:完全应用是指提供函数所需的全部参数],niceSum
函数的 xs
参数,以及传给 foldl
函数的 xs
参数,这两者都可以被省略,最终得到一个更紧凑的函数,它的类型也和原本的一样:
-- file: ch04/niceSumPartial.hs
niceSumPartial :: [Integer] -> Integer
niceSumPartial = foldl (+) 0
Prelude> :load niceSumPartial.hs
[1 of 1] Compiling Main ( niceSumPartial.hs, interpreted )
Ok, modules loaded: Main.
*Main> niceSumPartial [1 .. 10]
55
Haskell 提供了一种方便的符号快捷方式,用于对中序函数进行部分应用:使用括号包围一个操作符,通过在括号里面提供左操作对象或者右操作对象,可以产生一个部分应用函数。这种类型的部分函数应用称之为节(section)。
Prelude> (1+) 2
3
Prelude> map (*3) [24, 36]
[72,108]
Prelude> map (2^) [3, 5, 7, 9]
[8,32,128,512]
之前提到过,通过使用反括号来包围一个函数,可以将这个函数用作中序操作符。这种用法可以让节使用函数:
Prelude> :type (`elem` ['a' .. 'z'])
(`elem` ['a' .. 'z']) :: Char -> Bool
Prelude> (`elem` ['a' .. 'z']) 'f'
True
Prelude> (`elem` ['a' .. 'z']) '1'
False
代数数据类型的访问器函数:
data T a b = MkT { getA :: a, getB :: b }
上面的代码会自动定义2个辅助函数 :
getA :: (T a b) -> a
getA (MkT x _) = x
getB :: (T a b) -> b
getB (MkT _ y) = y
data ParseState = ParseState {
string :: L.ByteString
, offset :: Int64 -- imported from Data.Int
} deriving (Show)
newtype Parse a = Parse {
runParse :: ParseState -> Either String (a, ParseState)
}
-- 上面的代码会自动定义辅助函数 :
runParse :: (Parse a) -> ParseState -> Either String (a, ParseState)
runParse (Parse a) = ParseState -> Either String (a, ParseState)
Case 表达式:
fromMaybe defval wrapped =
case wrapped of
Nothing -> defval
Just value -> value
case
关键字后面可以跟任意表达式,这个表达式的结果即是模式匹配的目标。of
关键字标识着表达式到此结束,以及匹配区块的开始,这个区块将用来定义每种模式及其对应的表达式。
该区块的每一项由三个部分组成:一个模式,接着一个箭头 ->
,接着一个表达式;如果这个模式匹配中了,则计算并返回这个表达式的结果。这些表达式应当是同一类型的。第一个被匹配中的模式对应的表达式的计算结果即是 case
表达式的结果。匹配按自上而下的优先级进行。
没有参数的函数:
getState :: Parse ParseState
getState = Parse (\s -> Right (s, s))
five' :: () -> Int
five' () = 5
five :: Int
five = 5
{-
void scream() {
printf("Aaaah!\n");
}
-}
scream :: IO()
scream = putStrLn "Aaaah!"
<$>
是 fmap(Functor类型类的函数)
的中缀表达式版本。
记外
我们来看一下范畴论在现代数学中的排位: