没有变量的Haskell

Posted Haskell慢慢谈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了没有变量的Haskell相关的知识,希望对你有一定的参考价值。

说Haskell更抽象,其中一个重要原因在于,Haskell没有变量。


虽然在Haskell中有些语句看起来非常像是对某个变量赋值。例如下面的代码,仿佛是在给变量ab进行赋值,并在main函数中打印出b的值。

a = 1
b = a + 1
main = putStrLn $ show b

实则不然。比如,下面这段非常相似的“赋值”语句在Haskell中却是非法的。

a = 1
a = a + 1
main = putStrLn $ show a

因为,上述两段代码中的等式代表着一种绑定。a = 1是指将a这个符号与等号右边的表达式(即常数1)绑定。当在其它表达式中遇到a时,则可以将其用1代换。例如在b = a + 1中,a可以被1替换,从而得到b = 2


也有人习惯将等式的左边部分称为变量。这种称呼难免会引起误解。我倾向于称之为符号,以正其名。


在Haskell中,同一个符号在其作用域中的绑定是唯一的,否则将产生歧义。例如在第二段代码中,show a到底是应该被替换成show 1从而得到字符串"1"呢,还是应该被替换成show (a + 1)从而再继续进行替换呢?编译器无从知晓,所以出错。


更加有趣的是,因为这种绑定的唯一性,Haskell并不要求符号要先绑定再使用。第一段代码中的三行语句可以以任意顺序出现。例如以下代码是完全合法的。

main = putStrLn $ show b
b = a + 1
a = 1

对于像我一样从VB/Pascal/C这样的语言开始编程之路的程序员来说,变量仿佛是程序语言的天然存在一样。我们利用变量保存数据、传递函数参数、控制循环。变量可以说是编程解题的重要工具,不可或缺。但是,居然有一种程序语言没有变量,这听起来有些不可思议。更不用说,当我发现,因为没有办法声明循环变量,所以Haskell连循环也不支持时的惊讶感觉。显然,过往编程中那些惯用的经验在Haskell中不并适用。


为什么Haskell没变量,在回答这个问题之前,我们需要先讨论清楚变量是什么。


1 变量的本质


变量是数据的容器。我们可以从变量中读出数据,也可以向其写入数据。但在同一时刻,我们只可能或者从其读出或者向其写入数据,且新写入的数据将覆盖原有数据。无论是C++,Python,Java,Delphi还是其它支持变量的语言来说,其变量都满足以上描述。


编程入门书籍在介绍变量这个概念时通常都会用交换两变量内的数据为例来演示变量的功能。要交换两变量中的数据,必须引入第三个变量来保存中间数据。例如,在C++中交换两变量数据的函数模板如下。

template<typename T>
void swap(T &a, T &b)
{
T c = a; a = b; b = c; }


至于其它语言,虽然实现方式千差万别,但都或显式或隐式地引入第三个变量以保存中间数据。例如在Python中,通常用以下语句交换两个变量中的数据:

a, b = b, a # 隐含构造一个元组

等号右边的b, a实际上隐含构造了一个二元组,其中保存了变量ba中的数据。只有如此,ab中的数据才能正确交换。


可见,即使像“交换两个值”这样简单的算法,也会因为变量的约束而变得略微繁琐。更不用提排序、查找等等复杂的算法了。利用变量实现算法时,程序员除了要考虑数据之间的计算关系之外,还要考虑另一个非常重要的问题,即数据在变量中的存取时机。只有在正确的时机从变量中读出数据,做出正确的运算得到新数据,并在正确的时机写入正确的变量,才能使得程序得到正确的结果。


变量,无论其类型如何,最终都指代存储器(CPU寄存器,内存,或者外设中的存储器)中的某块存储空间。所以可以说,变量是程序语言对存储空间的抽象,也是程序员用以控制存储空间的媒介。变量的存在,反过来说明该程序语言还无法根据算法描述自动规划数据在存储器间的存取时机,而有赖于程序员通过对变量的操作给定。


1.1 摒弃变量的好处与不足


Haskell不提供变量,也就阻断了程序员直接操作存储器的通路。所以用Haskell实现某个算法时,必须将原本对存储器的直接操作需求,提炼成更为抽象的数据间的映射关系,即符号绑定。Haskell编译器将根据各种符号间的依赖关系,自动规划对存储器的读写和数据的运算,以正确实现算法的功能。


这样做的一个明显好处,就是用Haskell实现算法时不必花费大量精力去规划变量的存取时机,而可以专注于算法本身的数据映射的正确性上。程序设计中,有很大一部分错误是由于不当的变量存取时机引起的。由Haskell编译器自动规划的变量存取,总可以保证其正确反映代码的意图(当然,前提是编译器能做出正确的规划)。所以,只要数据映射关系正确,则编译出的目标代码就可以得到正确的结果。这大大降低了程序出错的概率,使得代码更加可读也更易维护。《Real World Haskell》的作者曾在该书前言中说到:

Even with years of experience, we remain astonished and pleased by how often our Haskell programs simply work on the first try, once we fix those compilation errors.

对此,仅有一点Haskell编程经历的我深表赞同。


当然,禁用变量也有代价。编译器自动规划的存储器存取肯定不如有经验的程序员所作的规划那样高效,所以Haskell的程序运行时间无法与C/C++程序相比。另外,虽然存储器规划由编译器包办,但其具体实现还是依赖于算法的具体代码。同一个算法,可以有多种Haskell代码实现,也就有可能导致不同的变量规划,从而导致程序运行效率之不同。要提高Haskell程序的运行效率,还是需要对Haskell编译器所使用的变量规划,内存管理有一定了解。


2 无变量的其他语言


回头想想,没有变量也并不值得惊讶。在我以前的C++编程经历中,也曾经尝试过某种无变量的编程方式,即C++泛型编程。


C++泛型编程用于C++编译器在编译期进行类型推导和常数演算。举个非常简单的例子,设计一个元函数(即进行类型推导的代码)prime_type可以从任意深度的指针类型中提取出其基本类型,比如从int***中提取出intprime_type可以用如下的代码实现。

#include <iostream>
#include <typeinfo>

// 元函数prime_type定义开始
template<typename T>
struct prime_type{
typedef T result; };

template
<typename T>
struct prime_type<T*> {
typedef typename prime_type<T>::result result; };
// 元函数prime_type定义结束

int main() {
using namespace std;
prime_type<int***>::result i; // i的类型是int prime_type<char**********>::result c; // c的类型是char cout << typeid(i).name() << endl; cout << typeid(c).name() << endl;
return 0; }

C++泛型编程有几个基本技巧:

  1. 用类模板实现元函数,模板参数即元函数的输入类型,约定类模板内嵌的某个类型标识符为其输出类型;

  2. 用类模板的特例(specialization)实现对类型的条件判断逻辑;

  3. typedef定义类型推演的中间结果和最终结果。


在C++泛型编程中,同样没有变量的概念。所有类型推演的步骤只能用typedef A B的形式将推演结果A绑定到类型标识符B。根据C++的语法定义,类型标识符在其作用域中也不能重复定义。这些都与Haskell的符号绑定有异曲同工之处。不同之处在于,C++对泛型逻辑推演的支持非常有限,远不如Haskell那样自然流畅。即使如此,人才辈出的C++程序员群体还是利用有限的语法支持,发展出了各种功能强大的泛型编程库,如boost::mpl以及boost::fusion,其中对类型进行构造的数据结构、遍历算法应有尽有,俨然一个完备的语言子集。可见,变量对于算法实现并非不可或缺。


在我有限的知识库中,还能找到另外一大类没有变量的语言,即硬件描述语言(HDL)。其典型代表如Verilog。硬件描述语言用于描述硬件系统中的信号与响应,最其终实现是不计其数的逻辑器件之间的互联。在Verilog中,最普通的一个实现加法逻辑的信号响应可以用以下的语句描述。

assign a = b + c;

这句的含义是信号a中的数值始终是信号b与信号c所载数值之和。同样,在Verilog中也不允许对同一个信号进行多次描述。


原来,语言的范畴是如此广泛。只是我一直耽于变量之中,太久没有仰望星空了。


以上是关于没有变量的Haskell的主要内容,如果未能解决你的问题,请参考以下文章

在 Haskell 中编写 AI Solver 时的类型变量不明确

哈斯克尔。我很困惑这个代码片段是如何工作的

如何使用Android片段管理器传递变量[重复]

“类型变量不明确”在 Haskell Yesod 中使用 Persistent

Haskell 是不是支持未绑定的变量?

haskell 中的***可变变量