当前位置:网站首页 > Haskell函数式编程 > 正文

haskell函数式编程入门_python定义函数的规则

既然我们已经学会了如何应用函数,那么是时候回过头来,学习怎样去编写函数。

因为 ghci 只支持 Haskell 特性的一个非常受限的子集,因此,尽管可以在 ghci 里面定义函数,但那里并不是编写函数最适当的环境。更关键的是, ghci 里面定义函数的语法和 Haskell 源码里定义函数的语法并不相同。综上所述,我们选择将代码写在源码文件里。

Haskell 源码通常以 .hs 作为后缀。我们创建一个 add.hs 文件,并将以下定义添加到文件中:

-- file: ch02/add.hs add a b = a + b 

[译注:原书代码里的路径为 ch03/add.hs ,是错误的。]

= 号左边的 add a b 是函数名和函数参数,而右边的 a + b 则是函数体,符号 = 表示将左边的名字(函数名和函数参数)定义为右边的表达式(函数体)。

add.hs 保存之后,就可以在 ghci 里通过 :load 命令(缩写为 :l )载入它,接着就可以像使用其他函数一样,调用 add 函数了:

Prelude> :load add.hs [1 of 1] Compiling Main ( add.hs, interpreted ) Ok, modules loaded: Main. *Main> add 1 2 -- 包载入成功之后 ghci 的提示符会发生变化 3 

[译注:你的当前文件夹(CWD)必须是 ch02 文件夹,否则直接载入 add.hs 会失败]

当以 12 作为参数应用 add 函数的时候,它们分别被赋值给(或者说,绑定到)函数定义中的变量 ab ,因此得出的结果表达式为 1 + 2 ,而这个表达式的值 3 就是本次函数应用的结果。

Haskell 不使用 return 关键字来返回函数值:因为一个函数就是一个单独的表达式(expression),而不是一组陈述(statement),求值表达式所得的结果就是函数的返回值。(实际上,Haskell 有一个名为 return 的函数,但它和命令式语言里的 return 不是同一回事。)

变量

在 Haskell 里,可以使用变量来赋予表达式名字:一旦变量绑定了(也即是,关联起)某个表达式,那么这个变量的值就不会改变 —— 我们总能用这个变量来指代它所关联的表达式,并且每次都会得到同样的结果。

如果你曾经用过命令式语言,就会发现 Haskell 的变量和命令式语言的变量很不同:在命令式语言里,一个变量通常用于标识一个内存位置(或者其他类似的东西),并且在任何时候,都可以随意修改这个变量的值。因此在不同时间点上,访问这个变量得出的值可能是完全不同的。

对变量的这两种不同的处理方式产生了巨大的差别: 在 Haskell 程序里面, 当变量和表达式绑定之后, 我们总能将变量替换成相应的表达式。 但是在声明式语言里面就没有办法做这样的替换,因为变量的值可能无时不刻都处在改变当中。

举个例子,以下 Python 脚本打印出值 11

x = 10 x = 11 print(x) 

[译注:这里将原书的代码从 print x 改为 print(x) ,确保代码在 Python 2 和 Python 3 都可以顺利执行。]

然后,试着在 Haskell 里做同样的事:

-- file: ch02/Assign.hs x = 10 x = 11 

但是 Haskell 并不允许做这样的多次赋值:

Prelude> :load Assign [1 of 1] Compiling Main ( Assign.hs, interpreted ) Assign.hs:3:1: Multiple declarations of `x' Declared at: Assign.hs:2:1 Assign.hs:3:1 Failed, modules loaded: none. 

条件求值

和很多语言一样,Haskell 也有自己的 if 表达式。本节先说明怎么用这个表达式,然后再慢慢介绍它的详细特性。

我们通过编写一个个人版本的 drop 函数来熟悉 if 表达式。先来回顾一下 drop 的行为:

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

从测试代码的反馈可以看到。当 drop 函数的第一个参数小于或等于 0 时, drop 函数返回整个输入列表。否则,它就从列表左边开始移除元素,一直到移除元素的数量足够,或者输入列表被清空为止。

以下是带有同样行为的 myDrop 函数,它使用 if 表达来决定该做什么。而代码中的 null 函数则用于检查列表是否为空:

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

在 Haskell 里,代码的缩进非常重要:它会延续(continue)一个已存在的定义,而不是新创建一个。所以,不要省略缩进!

变量 xs 展示了一个命名列表的常见模式: s 可以视为后缀,而 xs 则表示“复数个 x ”。

先保存文件,试试 myDrop 函数是否如我们所预期的那样工作:

[1 of 1] Compiling Main ( myDrop.hs, interpreted ) Ok, modules loaded: Main. *Main> myDrop 2 "foobar" "obar" *Main> myDrop 4 "foobar" "ar" *Main> myDrop 4 [1, 2] [] *Main> myDrop 0 [1, 2] [1,2] *Main> myDrop 7 [] [] *Main> myDrop (-2) "foo" "foo" 

好的,代码正如我们所想的那样运行,现在是时候回过头来,说明一下 myDrop 的函数体里都干了些什么:

if 关键字引入了一个带有三个部分的表达式:

  • 跟在 if 之后的是一个 Bool 类型的表达式,它是 if 的条件部分。
  • 跟在 then 关键字之后的是另一个表达式,这个表达式在条件部分的值为 True 时被执行。
  • 跟在 else 关键字之后的又是另一个表达式,这个表达式在条件部分的值为 False 时被执行。

我们将跟在 thenelse 之后的表达式称为“分支”。不同分支之间的类型必须相同。[译注:这里原文还有一句“the if expression will also have this type”,这是错误的,因为条件部分的表达式只要是 Bool 类型就可以了,没有必要和分支的类型相同。]像是 if True then 1 else "foo" 这样的表达式会产生错误,因为两个分支的类型并不相同:

Prelude> if True then 1 else "foo" <interactive>:2:14: No instance for (Num [Char]) arising from the literal `1' Possible fix: add an instance declaration for (Num [Char]) In the expression: 1 In the expression: if True then 1 else "foo" In an equation for `it': it = if True then 1 else "foo" 

记住,Haskell 是一门以表达式为主导(expression-oriented)的语言。在命令式语言中,代码由陈述(statement)而不是表达式组成,因此在省略 if 语句的 else 分支的情况下,程序仍是有意义的。但是,当代码由表达式组成时,一个缺少 else 分支的 if 语句,在条件部分为 False 时,是没有办法给出一个结果的,当然这个 else 分支也不会有任何类型,因此,省略 else 分支对于 Haskell 是无意义的,编译器也不会允许这么做。

程序里还有几个新东西需要解释。其中, null 函数检查一个列表是否为空:

Prelude> :type null null :: [a] -> Bool Prelude> null [] True Prelude> null [1, 2, 3] False 

(||) 操作符对它的 Bool 类型参数执行一个逻辑或(logical or)操作:

Prelude> :type (||) (||) :: Bool -> Bool -> Bool Prelude> True || False True Prelude> True || True True 

另外需要注意的是, myDrop 函数是一个递归函数:它通过调用自身来解决问题。关于递归,书本稍后会做更详细的介绍。

最后,整个 if 表达式被分成了多行,而实际上,它也可以写成一行:

-- file: ch02/myDropX.hs myDropX n xs = if n <= 0 || null xs then xs else myDropX (n - 1) (tail xs) 

[译注:原文这里的文件名称为 myDrop.hs ,为了和之前的 myDrop.hs 区别开来,这里修改文件名,让它和函数名 myDropX 保持一致。]

Prelude> :load myDropX.hs [1 of 1] Compiling Main ( myDropX.hs, interpreted ) Ok, modules loaded: Main. *Main> myDropX 2 "foobar" "obar" 

这个一行版本的 myDrop 比起之前的定义要难读得多,为了可读性考虑,一般来说,总是应该通过分行来隔开条件部分和两个分支。

作为对比,以下是一个 Python 版本的 myDrop ,它的结构和 Haskell 版本差不多:

def myDrop(n, elts): while n > 0 and elts: n = n -1 elts = elts[1:] return elts 

通过示例了解求值

前面对 myDrop 的描述关注的都是表面上的特性。我们需要更进一步,开发一个关于函数是如何被应用的心智模型:为此,我们先从一些简单的示例出发,逐步深入,直到搞清楚 myDrop 2 "abcd" 到底是怎样求值为止。

在前面的章节里多次谈到,可以使用一个表达式去代换一个变量。在这部分的内容里,我们也会看到这种替换能力:计算过程需要多次对表达式进行重写,并将变量替换为表达式,直到产生最终结果为止。为了帮助理解,最好准备一些纸和笔,跟着书本的说明,自己计算一次。

惰性求值

先从一个简单的、非递归例子开始,其中 mod 函数是典型的取模函数:

-- file: ch02/isOdd.hs isOdd n = mod n 2 == 1 

[译注:原文的文件名为 RoundToEven.hs ,这里修改成 isOdd.hs ,和函数名 isOdd 保持一致。]

我们的第一个任务是,弄清楚 isOdd (1 + 2) 的结果是如何求值出的。

在使用严格求值的语言里,函数的参数总是在应用函数之前被求值。以 isOdd 为例子:子表达式 (1 + 2) 会首先被求值,得出结果 3 。接着,将 3 绑定到变量 n ,应用到函数 isOdd 。最后, mod 3 2 返回 1 ,而 1 == 1 返回 True

Haskell 使用了另外一种求值方式 —— 非严格求值。在这种情况下,求值 isOdd (1 + 2) 并不会即刻使得子表达式 1 + 2 被求值为 3 ,相反,编译器做出了一个“承诺”,说,“当真正有需要的时候,我有办法计算出 isOdd (1 + 2) 的值”。

用于追踪未求值表达式的记录被称为块(chunk)。这就是事情发生的经过:编译器通过创建块来延迟表达式的求值,直到这个表达式的值真正被需要为止。如果某个表达式的值不被需要,那么从始至终,这个表达式都不会被求值。

非严格求值通常也被称为惰性求值。[注:实际上,“非严格”和“惰性”在技术上有些细微的差别,但这里不讨论这些细节。]

一个更复杂的例子

现在,将注意力放回 myDrop 2 "abcd" 上面,考察它的结果是如何计算出来的:

Prelude> :load "myDrop.hs" [1 of 1] Compiling Main ( myDrop.hs, interpreted ) Ok, modules loaded: Main. *Main> myDrop 2 "abcd" "cd" 

当执行表达式 myDrop 2 "abcd" 时,函数 myDrop 应用于值 2"abcd" ,变量 n 被绑定为 2 ,而变量 xs 被绑定为 "abcd" 。将这两个变量代换到 myDrop 的条件判断部分,就得出了以下表达式:

*Main> :type 2 <= 0 || null "abcd" 2 <= 0 || null "abcd" :: Bool 

编译器需要对表达式 2 <= 0 || null "abcd" 进行求值,从而决定 if 该执行哪一个分支。这需要对 (||) 表达式进行求值,而要求值这个表达式,又需要对它的左操作符进行求值:

*Main> 2 <= 0 False 

将值 False 代换到 (||) 表达式当中,得出以下表达式:

*Main> :type False || null "abcd" False || null "abcd" :: Bool 

如果 (||) 左操作符的值为 True ,那么 (||) 就不需要对右操作符进行求值,因为整个 (||) 表达式的值已经由左操作符决定了。[译注:在逻辑或计算中,只要有一个变量的值为真,那么结果就为真。]另一方面,因为这里左操作符的值为 False ,那么 (||) 表达式的值由右操作符的值来决定:

*Main> null "abcd" False 

最后,将左右两个操作对象的值分别替换回 (||) 表达式,得出以下表达式:

*Main> False || False False 

这个结果表明,下一步要求值的应该是 if 表达式的 else 分支,而这个分支包含一个对 myDrop 函数自身的递归调用: myDrop (2 - 1) (tail "abcd")

到此这篇haskell函数式编程入门_python定义函数的规则的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!

版权声明


相关文章:

  • java函数式编程的好处_Java语言程序设计2024-11-16 13:00:07
  • haskell函数式编程入门 第二版 pdf_c++小游戏编程代码2024-11-16 13:00:07
  • java支持函数式编程_java编写软件工具2024-11-16 13:00:07
  • Haskell的魔法 - 函数式编程初探之一2024-11-16 13:00:07
  • java函数写法_子程序调用编程实例2024-11-16 13:00:07
  • 如何从面向过程与面向对象转换过来_函数式编程和面向对象的区别2024-11-16 13:00:07
  • haskell functor_scratch游戏代码2024-11-16 13:00:07
  • 函数式编程的 Java 编码实践:利用惰性写出高性能且抽象的代码2024-11-16 13:00:07
  • haskell函数式编程入门 第二版 pdf_编程2024-11-16 13:00:07
  • haskell 函子_shell中if判断2024-11-16 13:00:07
  • 全屏图片