haskell函数基本

Haskell 函数基础语法

作为函数式编程语言,haskell里的函数是一个极为广阔且多变的概念,所有的内容基本都可以归纳为函数及函数的构造方法

函数的类型和类型类

Haskell 与一些传统语言不同,在运行时具有类型推断能力.我们可以直接写一个数字,而不必详细说明它是一个数字,Haskell 会自动推断出它的类型,确保函数类型符合规定.

常见类型问题:

  • 所有的显式类型使用大写字母开头
  • :: 是一种“具有…类型”的表示符
  • Int 表示有界整数类型,类似于c中的int和long,而 Integer 表示无界整数类型

一个值得注意的点,在使用数组长度length函数时

1
2
fromIntegral :: (Num b, Integral a) => a -> b
length :: [a] -> Int

不可直接加非int,属于讨厌的历史遗留问题


多态性

  • 类型变量 a:表示多态性.强调的是相同类型,而不是不同类型.例如,a -> a -> a 表示函数的输入和输出类型相同,而a -> b -> a 则不在乎两个参数的类型是否相同,而重视第一个参数和结果的类型相同


类型约束

  • 类型约束使用 => 表示.例如,(Eq a) => a -> a -> Bool 表示输入值的类型必须是 Eq 类型类的成员
  • Eq包含所有默认定义的类型,一般不用特意强调
  • Ord 类型类涵盖了所有标准比较函数(如 >, <, >=, <=).例如,compare 函数用于比较两个 Ord 成员,返回一个 Ordering 类型,该类型可以是 GT, LTEQ,分别表示大于、小于或等于.
  • 注意,Num 类型不直接包含 Ord,因此在定义函数时需要同时包含 NumOrd 约束

以下可以

1
2
abs :: (Num a, Ord a) => a -> a
abs x = if (x > 0) then x else -x

以下不行
1
2
abs :: Num a => a -> a
abs x = if (x > 0) then x else -x


模式匹配

模式匹配是 Haskell 中非常重要的特性,可以用来简洁地定义函数.

可以省去if-else

例如,以下是一个简单的模式匹配函数:

1
2
3
lucky :: (Integral a) => a -> String
lucky 7 = "LUCKY NUMBER SEVEN!"
lucky x = "Sorry, you're out of luck, pal!"

在这个函数中,模式会从上到下依次匹配,一旦匹配成功,就会使用对应的函数体.注意,尽管haskell对于不同函数的定义顺序没有要求,但是对同一函数参数的不同情况从上至下匹配


元组的模式匹配

模式匹配不仅可以用于列表,也可以用于元组.例如:

1
2
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)

这种方式比使用 fstsnd 更加直观和简洁.

如果我们只关心元组中的某些值,可以使用下划线 _ 来忽略不重要的部分:

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


as 模式

as 模式可以让我们在模式匹配的同时引用整体.例如:

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

在这个例子中,all@(x:xs) 匹配字符串的第一个字符和剩余部分,同时 all 引用了整个字符串.

牛逼之处,便捷拆分同时引用整体.


守卫(Guards)

守卫是对函数参数进行条件检查的另一种方式,使用 | 符号.本质上也是一种条件的化简.例如:

1
2
3
4
5
6
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!"

otherwise 实际上等价于 True,它可以捕获所有未被前面条件捕获的情况.

我们也可以在守卫中使用 where 绑定局部变量,使代码更具可读性:

1
2
3
4
5
6
7
8
9
10
bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
| bmi <= skinny = "You're underweight, you emo, you!"
| bmi <= normal = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= fat = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
where bmi = weight / height ^ 2
skinny = 18.5
normal = 25.0
fat = 30.0


case 表达式

case 表达式可以对某个值进行模式匹配:

1
2
3
4
5
describeList :: [a] -> String
describeList xs = "The list is " ++ case xs of
[] -> "empty."
[x] -> "a singleton list."
xs -> "a longer list."


let 表达式和 where 绑定

let 表达式和 where 绑定都可以用来定义局部变量,但它们的作用范围不同.

  • where 绑定的变量在整个函数中可见.
  • let 表达式的作用范围更小,只在 in 之后的代码中有效.

例如:

1
2
3
4
5
cylinder :: (RealFloat a) => a -> a -> a
cylinder r h =
let sideArea = 2 * pi * r * h
topArea = pi * r ^ 2
in sideArea + 2 * topArea

let 绑定本身也是一个表达式,因此可以在任何地方使用,例如列表推导式中:
1
2
calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0]


递归

haskell中的递归较c系更为优美,主要体现在边界条件的处理上一目了然

递归函数通常包括一个前置基准条件来终止递归,例如 quicksort [] = [],然后通过递归调用进行计算.

1
2
3
4
5
6
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


lambda 表达式

Haskell 中的 lambda 表达式使用 \ 表示,这主要是因为\看起来比较像$\lambda$.例如:

1
2
numLongChains :: Int
numLongChains = length (filter (\xs -> length xs > 15) (map chain [1..100]))

\xs -> length xs > 15 定义了一个匿名函数,接受参数 xs,返回一个布尔值.


柯里化 Curried

柯里化 (Currying) 是一种将一个接受多个参数的函数转换为一系列嵌套的单参数函数的技术.在 Haskell 和许多函数式编程语言中,所有函数默认都是柯里化的.这种技术源于逻辑学家 Haskell Curry 的工作,因此得名.是haskell比较实验性的一部分,直接把token分解放在了语言部分而非底层,其实c系语言在编译过程中接受新的token也是类似的过程.


核心思想

在柯里化中,一个多参数函数不直接接收所有参数,而是逐个接受参数,每次接受一个参数后,返回一个新的函数,用于处理剩余的参数.

例如:

  • 假设有一个函数 f(x, y, z),它接收三个参数.
  • 在柯里化的形式下,它可以看作 f(x)(y)(z).
  • 即调用 f(x) 返回一个函数 g(y),然后调用 g(y) 返回另一个函数 h(z),最后调用 h(z) 计算并返回最终结果.


Haskell 中的实现

Haskell 默认将所有函数看作柯里化函数.这意味着一个定义为 a -> b -> c -> d 的函数被视为:

  1. 首先接受一个参数 a,返回一个类型为 b -> c -> d 的函数.
  2. 然后,这个函数再接受参数 b,返回类型为 c -> d 的函数.
  3. 最后,这个函数接受参数 c,返回最终结果 d.

示例,定义一个接受三个参数的函数:

1
2
addThree :: Int -> Int -> Int -> Int
addThree x y z = x + y + z

在实际使用中,addThree 可以被拆解为:

  1. addThree 1 返回一个函数,该函数接受两个参数 yz.
  2. addThree 1 2 返回另一个函数,该函数接受一个参数 z.
  3. 最后,addThree 1 2 3 直接计算并返回结果.

以下代码说明了这一过程:

1
2
let addTwo = addThree 1 2  -- addTwo 是一个函数,类型为 Int -> Int
addTwo 3 -- 结果为 6


柯里化与部分应用

柯里化的直接好处之一是 部分应用,即只提供部分参数来创建一个新函数.这在 Haskell 中很常见:

1
2
3
4
5
multThree :: Int -> Int -> Int -> Int
multThree x y z = x * y * z

let doubleWithSix = multThree 6
doubleWithSix 2 3 -- 等价于 multThree 6 2 3,结果为 36


非柯里化 (Uncurried)

对比柯里化,非柯里化函数会接受所有参数作为一个元组.例如:

1
2
addThreeUncurried :: (Int, Int, Int) -> Int
addThreeUncurried (x, y, z) = x + y + z

调用方式:

1
addThreeUncurried (1, 2, 3)  -- 参数必须打包为一个元组

Haskell 中的柯里化使得函数更加灵活,而非柯里化则更接近传统的多参数函数形式.