「Haskell 学习」二 类型和函数(上)

随着学习的深入,笔记会补充和修订。当然,这个补充修订也许会鸽,但我一定会坚持写完。
这个笔记假定你至少学过C/C++及Python,或与这两种语言类型相同的语言。

类型系统概述

“Haskell’s type system allows us to think at a very abstract level: it permits us to write concise, powerful programs.”
简单地说,Haskell拥有一个强、静态、自动推断的类型系统。Haskell的强同C/C++的区别体现在它不会隐式类型转换。比如,对一个取float参数的函数以int,C/C++就这么给过去了,但是Haskell会报错,必须要手动类型转换。静态类型的意思是编译器在编译时就知道表达式/变量的类型,如果不匹配就会报错。如:

# zuiho @ zuiho-pc in ~ [Time]
$ ghci
GHCi, version 8.0.2: http://www.haskell.org/ghc/  :? for help
Prelude> :set prompt "ghci> "
ghci> True && "false"

<interactive>:2:9: error:
    • Couldn't match expected type ‘Bool’ with actual type ‘[Char]’
    • In the second argument of ‘(&&)’, namely ‘"false"In the expression: True && "false"
      In an equation for ‘it’: it = True && "false"
ghci>

问题出现在&&操作符上。第一章的笔记里提到过,逻辑与操作符要求两个参数都为Bool类型。尽管看起来很严格,但是用原文的话,“Haskell has some support for programming with truly dynamic types, though it is not quite as easy as in a language that wholeheartedly embraces the notion.”
最后就是自动推断。编译器会推断大多数表达式的类型,但是你也可以钦定某些变量(或者更准确地说,声明变量)的类型。

基础类型

常用的有Char、Bool、Int、Integer、Double等。需要注意的是,Char存放的是一个Unicode字符,Int被确保大于228228,Integer是高精度整数。
如果想要查看某个数字的类型,可以用“expression :: MyType ”的形式来获得。强制声明变量的方法类似,如“12::Integer”;但如果显然不符会报错。具体示例见下。

ghci> :type 'a'
'a' :: Char
ghci> :type "abc"
"abc" :: [Char]
ghci> 'c' :: Char
'c'
ghci> [1,2,3] :: Int

<interactive>:7:1: error:
    • Couldn't match expected type ‘Int’ with actual type ‘[Integer]’
    • In the expression: [1, 2, 3] :: Int
      In an equation for ‘it’: it = [1, 2, 3] :: Int

用原文的话说,“The combination of :: and the type after it is called a type signature.
同样的,老生常谈的元素类型还有列表(list)与元组(tuple)。[]放列表,()放元组。它们的差别在于,元组“is a fixed-size collection of values, where each value can have a different type”,而列表“can have any length, but elements must all have the same type”。
一般而言,元组用来传递函数的多个返回值,或者“a fixed-size collection of values, if the circumstances don’t require a custom container type”。

“奇怪”的变量系统

这段话我全文抄录,因为我感觉我目前总结不出来比它好的:

In Haskell, a variable provides a way to give a name to an expression. Once a variable is bound to (i.e. associated with) a particular expression, its value does not change: we can always use the name of the variable instead of writing out the expression, and get the same result either way.

If you’re used to imperative programming languages, you’re likely to think of a variable as a way of identifying a memory location (or some equivalent) that can hold different values at different times. In an imperative language we can change a variable’s value at any time, so that examining the memory location repeatedly can potentially give different results each time.

The critical difference between these two notions of a variable is that in Haskell, once we’ve bound a variable to an expression, we know that we can always substitute it for that expression, because it will not change. In an imperative language, this notion of substitutability does not hold.

For example, if we run the following tiny Python script, it will print the number 11.

x = 10
x = 11
# value of x is now 11
print x

In contrast, trying the equivalent in Haskell results in an error.

-- chapter 02
-- file: Assign.hs
x = 10
x = 11

We cannot assign a value to x twice.

ghci> :load Assign
[1 of 1] Compiling Main             ( Assign.hs, interpreted )

Assign.hs:4:0:
    Multiple declarations of `Main.x'
    Declared at: Assign.hs:3:0
                 Assign.hs:4:0
Failed, modules loaded: none.

函数与自定义函数

概述

同变量工作的是函数。在haskell中,使用函数只需要“函数名 参数1 参数2 …”这样用就可以。

ghci> odd 3
True
ghci> odd 4
False
ghci> compare 2 3
LT
ghci> compare 3 3
EQ
ghci> compare 4 3
GT

同时,函数表达式的优先级高于操作符,所以以下两个表达式的值是一致的。

ghci> (compare 2 3) == LT
True
ghci> compare 2 3 == LT
True

但是,以函数作为函数的参数时,加个括号就是必要的了:

ghci> compare (sqrt 6) (sqrt 9)
LT

没有括号看起来像是给compare函数传递了四个参数。

操作列表与元组的函数

headtail 函数分别取列表的头元素和尾元素。对空列表操作时,会抛出“empty list”异常。
takedrop 分别取/删列表的头n个元素。

ghci> take 4 [1,9,1,9,8,1,0]
[1,9,1,9]

对二元元组(我们一般称之为pair),fstsnd 分别取第一个和第二个。

函数的参数与类型

表达式的计算是从左而右的;换句话说,a b c d等价于(((a b) c) d)。这样的嵌套同样带来了许多操作,如:

ghci> head (drop 4 "democracy")
'c'

去掉括号会造成Type Error,因为head 的参数应当仅是一个列表。
函数与大多数语言一样也有自己的类型。

ghci> :type lines
lines :: String -> [String]
ghci> lines "I am
the king
of the world!"
["I am","the king","of the world!"]

这里的意思是,输入一个String,返回一个String元素的列表;事实上,lines就是这么干的。

应用函数:程序文件

ghci,如书中所说,有很多局限:“ghci is not a good environment for this. It only accepts a highly restricted subset of Haskell: most importantly, the syntax it uses for defining functions is not the same as we use in a Haskell source file.”因此写一个源程序文件来运行它是一个常态。
我们新建一个文件,叫add.hs

-- chapter 03
-- file: add.hs
add a b = a + b

等号左边时函数的名字以及它的参数(们);右边就是它要做的事情了。这件事做好后,保存该文件,我们可以在ghci中加载它:

ghci> :cd ~/Documents/codes/Haskell/
ghci> :load add.hs
[1 of 1] Compiling Main             ( add.hs, interpreted )
Ok, modules loaded: Main.
ghci> add 1 2
3

注意到:cd没有?它切换了ghci的工作目录,让我们可以使用:load加载我们的自定义程序。加载成功后,我们就可以直接使用了。
Haskell没有C/C++中的那种return语句,这就是它函数式编程的精髓:一个函数其实仅仅只是一个表达式(但是它确实有return,不过意义不一样)。

多态

造个drop的轮子:if语句、循环、递归

我们通过重新实现drop函数的功能来学习这三种语法。
我们用if语句造个drop的轮子。先看看drop的输入输出:

ghci> drop 2 "foobar"
"obar"
ghci> drop 4 "foobar"
"ar"
ghci> drop 4 [1,2]
[]
ghci> drop 0 [1,2]
[1,2]
ghci> drop 7 []
[]
ghci> drop (-2) "foo"
"foo"

这些输入输出涵盖了各种可能的情况。接下来就是写一个函数了:

-- chapter 02
-- file: myDrop.hs
myDrop n xs = if n <= 0 || null xs
              then xs
              else myDrop (n-1) (tail xs)

接下来是解释:

  • Haskell的缩进和Python一样严格。
  • 起xs的变量名的用意是“plural of x”。
  • null是一个函数,检查参数(a list)是否为空。
  • || 与 null 一样,其实都是个函数:
ghci> :type null
null :: [a] -> Bool
ghci> :type (||)
(||) :: Bool -> Bool -> Bool
  • if - then - else 就是这么用的:if后跟一个Bool表达式,而then/else就是分支语句。分支语句应当具有相同的Type,不然无法编译通过(这符合整个Haskell的特性,整体回顾一下就知道了)。基于这个理由,else无法省略。
  • 最后的else语句使用了递归,意思是继续对xs列表的后n-1个元素继续进行myDrop操作。
  • 可以在一行写完整个函数。
    运行大家自己尝试一下,结果是一样的。

Lazy evaluation

实际上,Haskell对表达式的计算仅仅在真正用到它时才会计算:其他时候,“we create a ‘promise’ that when the value of the expression is needed, we’ll be able to compute it. The record that we use to track an unevaluated expression is referred to as a thunk. This is all that happens: we create a thunk, and defer the actual evaluation until it’s really needed. If the result of this expression is never subsequently used, we will not compute its value at all. ”

如非注明,原创内容遵循GFDLv1.3发布;其中的代码遵循GPLv3发布。
原文地址:https://www.cnblogs.com/samhx/p/Haskell-2a.html