借助Q语言理解编程核心思想

背景

前几年我在研究时序数据库时,了解到了kdb+这款当时号称全世界最快的数据库。kdb+主要的应用在金融领域,用在一些量化分析的场景下,在计算机领域用的相对比较少。

kdb+的最底层是K语言,他是一种基于符号的语言,继承了APL的核心思想,但是换用更标准的ascii符号来编码。而它的上层界面是在K语言基础上封装的Q语言,使用可读性更高的单词来代替符号。

最近由于需要对大规模的业务数据做一些分析,又重新学习了一下Q语言,对编程有了一些新的理解和感悟,在这里记录一下算是自我检查, 也希望对大家有所启发。

重新思考函数的本质

函数这个概念最开始是从数学中提出的,可能是从初中阶段引入的。

数学上的函数代表了一种映射关系。比如我们说平方函数,他代表着一个映射关系,可以将一个数字映射成它的平方。这种映射关系是明确的,也就是我输入一个A,必然会得到一个固定的B。

编程里的函数就没有那么幸运,因为他要处理的是现实世界的问题,,就不可能有数学函数定义的那么单纯。一方面它仍可以作为一种映射关系,同时又引入了一个副作用的概念,比如在一个函数中,它不仅有输出,可能还会有一些I/O操作,比如写文件。

与副作用相对的是一种叫做纯函数的编程范式,在纯函数式编程的范式下,函数的概念在数学和编程上达到了一致,一个输入就是对应着明确的输出,并且没有副作用。Haskell 就是纯函数式编程的代表,他用一些特殊的手段隔离了副作用。

但是就像前面说的,编程有很多时候是用来处理现实世界的问题,他不可能是数学世界那么单纯的理想模型,所以就导致纯函数听起来很美好,但是现实世界应用的并不多。

当然,我们这里说的Q语言也是一种函数式编程语言,但是同时他也引入了副作用,在函数式编程的同时保留了一定的灵活性。

这里举一个比较典型的例子来说明函数的本质是一种映射关系。

斐波那契数列是一个正整数组成的数列,数列的前两项都是1,后面的某一项是该项前面相邻两项的和。

假设我们要定义一个函数,该函数可以接受一个参数,返回对应位置的斐波纳契数字。按照传统习惯,位置从零开始计数,也就是输入是0或者是1的时候返回的都是1。

下面我们使用函数定义的方式来实现这个数列的逻辑计算,并且验证一下结果:

1
2
3
q)fib1: {$[x<=1; 1; fib1[x-1]+fib1[x-2]]}
q)fib1 9
55

然后我们来直接定义映射关系,并且检查一下他的结果:

1
2
3
q)fib2: 0 1 2 3 4 5 6 7 8 9!1 1 2 3 5 8 13 21 34 55
q)fib2 9
55

从调用的方式和实现的功能上来看,两者其实是完全等价的。也就是抛开副作用的话,函数的本质就是一种映射。

四则运算是函数吗?

四则运算和逻辑运算, 其实本质就是一种映射关系,只是因为他的使用过于高频,所以有很多语言把它作为了表达式的一部分。

举个例子,在 python里面,整数的加法底层仍然是一个函数,可以通过下面的命令去查看函数的具体定义:

1
2
3
4
5
>>> help(int.__add__)
Help on wrapper_descriptor:

__add__(self, value, /) unbound builtins.int method
Return self+value.

加法跟斐波纳契函数的一个不同之处在于它的参数是有两个,而且一般情况下,这两个参数是分布在加法的左右两侧的。

在Q语言里,我们可以看到加法的这两种不同的格式 (其中第二种格式为Q语言的函数调用格式):

1
2
3
4
q)1 + 2
3
q)+[1; 2]
3

所以我们知道函数是语言中很重要的一类组成部分, 在某些语言里函数也叫做verb, 即动词。

编程语言与自然语言的关系是什么?

刚才我们已经提到了动词, 这个本是自然语言的一个概念。回过头来看, 编程语言与自然语言的边界又在哪里呢?

编程语言跟自然语言的本质是相同的,都是为了沟通而存在的。不同点只是用于沟通的双方,是我们要跟机器沟通,还是要跟人沟通。

编程语言同样存在语法结构,而且同样存在多种语法结构迥异的语言分支。

我们已经了解了动词, 编程语言实际也有自己的名词和副词,甚至我了解到有些语言(比如J语言)在定义语言的组成元素时,就是使用了verb、noun和adverb。

动词表现形式就像上面说的, 在大部分语言里就是函数。

名词其实也挺好理解的,就是常量、变量或者对象。

副词是一个挺有意思的组成元素,有点像是Python里面的装饰器,主要作用就是改变动词的行为。这也是函数式编程的一个特点,由于函数是一个变量,所以他也可以作为副词修饰的目标(参数)。

下面是一个比较典型的示例,通过each这个副词,我们可以在保留原函数核心逻辑的情况下,很方便的改变函数的整体行为。

1
2
q)fib1 each 0 1 2 3 4 9
1 1 2 3 5 55

所以通过语言的类比, 编程语言其实也有自己的语法结构, 跟自然语言极其相似。

借助交互式理解SQL?

kdb+的牛逼之处,他通过Q语言提供了一个叫做qSQL的DSL,使其既能够像MySQL一样可以灵活的查询数据,同时又具有极高的可解释性。

下面我们来通过一个典型的案例,看一下qSQL的牛逼之处(这里需要再补充一点说明,就是Q语言的求值是从右向左的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
q)tbl:([] id:1 1 2 2 2;val:100 200 300 400 500)
q)tbl
id val
------
1 100
1 200
2 300
2 400
2 500
q)select val by id from tbl where val < 500
id| val
--| -------
1 | 100 200
2 | 300 400

下面我们来看where的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
q)tbl.val
100 200 300 400 500
q)tbl.val < 500
11110b
q)where tbl.val < 500
0 1 2 3
q)tbl where tbl.val < 500
id val
------
1 100
1 200
2 300
2 400

可以看到where本身其实就是一个动词,一个纯粹的可以单独使用的动词。所以其实不管这里的where多复杂都是可以实现的,他本身就是Q语言的一部分,并没有像MySQL语法上的限制,是完全完备的一种查询语法。

再来看select的部分:

1
2
3
4
q)tmp: tbl where tbl.val < 500
q)tmp.val group tmp.id
1| 100 200
2| 300 400

可以看到select仍然是一层薄薄的封装,并不需要在语法上的过多解析就可以执行。

所以从这里可以看到qSQL的牛逼之处在于以下几点:

  1. 语法的每个部分都是语言级别的支持,是完备的,完全不限制大家的想象
  2. qSQL只是一层薄薄的抽象,不需要做太多的解析,对性能上来说几乎是零损耗