2012年11月2日星期五

3 定义函数

3 定义函数

这是对<Learning J>(by Roger Stokes)的尝试翻译。

Roger Stokes的版权声明如下:
Copyright © Roger Stokes 2012. This material may be freely reproduced, provided that this copyright notice is also reproduced.
感谢作者的无私奉献,请您转载时也注明以上版权,也请同时注明转载地址http://corwindong.blogspot.com,我将不胜感激。

----------------------------------------------------------------------------------

J有一些内置函数,比如我们之前看到的 *+。在本节中我们看看如何通过各种各样的方法将这些内置函数组合起来,来构造我们自己的函数。

3.1 重命名

最简单的定义函数方式就是为内置函数起一个我们想要的名字。这种定义就是一个赋值式。例如,定义和内置 *: 一样的square函数
   square =: *:
   
   square 1 2 3 4
1 4 9 16 
当我们想要更好记的函数名时就可以这么做。我们还可以给一个内置函数两个不同的名字,
一个代表一元模式,另一个代表二元模式。 
   Ceiling =: >.
   Max     =: >.
Ceiling 1.73 Max 4
24

 

3.2 插入

回忆一下(+/ 2 3 4),它表示2+3+4,同样地,(*/ 2 3 4)表示2*3*4。我们可以通过赋值定义一个sum函数:
   sum =: + /
   
   sum 2 3 4
9
sum =: + /向我们展示了 +/ 表达式本身就是一个函数。 
表达式 +/ 可以解读为:“插入”(/)应用在 + 函数上,生成了一个列表求和函数。
也就是说,/ 本身是这样一种函数:它接受一个左参数,它的参数和应用结果都是函数。 

3.3 术语:动词,运算符和副词

我们已经看到了两类函数。第一类是“普通函数”,例如 +*,操作数据在J中这类函数称为“动词”。

第二类函数,比如 / ,以函数为参数,计算结果也是函数。这类函数称为“运算符”,以区分“动词”。

接受一个参数的运算符称为“副词”。副词在左边接受参数。因此对于表达式 (+ /),我们说副词 / 应用于动词 +,生成一个列表求和动词。

这些术语来自于英语语法:动词作于用事物,而副词修饰动词。

3.4 交换

我们已经看过了副词(/),现在看看另一个副词。副词 ~ 的作用是交换动词的左右参数。
'a' , 'b''a' ,~ 'b'
abba

对于二元函数f,参数为x和y,那么,
        x f~ y 相当于 y f x

再看个例子,“剩余”动词 |:2 | 7表示“7 模 2”。我们可以这样定义“求模”动词:
   mod =: | ~
   
7 mod 22 | 7
11


下面我来用几幅图说明一下副词。首先是函数f 应用于参数y 得到结果(f y)。在图中f 用矩形表示,箭头表示参数输入给函数,或者函数输出结果。每个箭头上都注明相应的表达式:












接下来是二元函数f应用于x和y得到结果(x f y):














现在来看看函数(f~),它可以展示为自身内部包含了函数f,同时交换了f的参数。














3.5 绑定

假设我们要定义一个动词double,double表示两倍(double x就是x * 2)。我们可以这样定义:
   double =: * & 2
   
   double 3
6
在这里我们绑定二元 * 的一个参数(double中的2),生成了一元函数。
运算符 & 用来绑定函数和一个参数。规则是:如果f 是一个二元函数,
k是提供给f的右参数,那么
        (f & k) y    相当于    y f k
类似地,如果我们绑定了左参数,那么规则就是:
        (k & f)  y   相当于    k f y 
 
例如,假设购物税是10%,那么通过销售价格计算税额的函数就可以定义为:
   tax =: 0.10 & *
   
   tax 50
5
 
下面的图用来说明函数k&f:


 


  





3.6 术语:连词和名词

表达式(*&2)的含义是:运算符 & 是一个函数,接受两个参数(动词 * 和数字2),得到的结果是动词“doubling”。
接受两个参数的运算符(如 &)在J中称为“连词” ,因为它将两个参数连接起来。与之相比回忆一下“副词”,副词就是只接受一个参数的运算符。

J中的每一个函数,无论是内置的还是用户自定义的,都属于以下四类之一:一元动词,二元动词,副词和连词。我们以符号 - 为例,- 代表了两个不同的动词:一元的“求负”和二元的“减法”。

J中的每一个表达式都是某个类型的值,所有不是函数的值都算作数据(实际上就是阵列,就像前面章节看到的)。

在J中,数据值,也就是阵列,按照英语语法的类比,称为“名词”。我们说某个事物是名词时强调它不是动词,说它是阵列时强调它有多个维度。

3.7 函数的组合

让我们看看汉语表达式:求数字1 2 3的平方和,即(1+4+9)等于14。我们之前定义过动词sum和suqare,现在就可以直接使用:
   sum square 1 2 3
14

也可以通过组合sum和square得到一个单独的求平方和函数:
   sumsq =: sum @: square
   
   sumsq 1 2 3
14
符号 @:(at符 冒号)叫做“组合”运算符。该运算符的规则是,如果f和g都是动词,那么
对于任何参数y:        
        (f @: g) y    相当于  f (g y)
 
下图用来说明该规则: 

读者可能会好奇,为什么不简单地使用(f g)来代表组合,而要使用(f @: g)呢?这是因
为(f g)另有含义,我们一会儿就会看到。
 
再来一个组合的例子:华氏温度可以通过减去32然后乘以5%9来转换成摄氏温度:
   s       =: - & 32
   m       =: * & (5%9)
   convert =: m @: s
 
s 212m s 212convert 212
180100100


为了清晰性,上例中用两个有名字的函数组合起来,其实我们也可以直接组合表达式:
   conv =: (* & (5%9)) @: (- & 32) 
   conv 212
100
 
我们还可以直接应用代表了函数的表达式,而不需要有名字:
   (* & (5%9)) @: (- & 32)  212
100  

上例中展示了组合两个一元函数。下面我们来展示一下组合一元函数和二元函数。通用规则为:       
         x (f @: g) y   相当于    f (x g y)
举个例子:要计算订单的总价,那么可以把每种物品的单价乘以数量的到物品的总价,再将
所有物品的总价加起来,得到订单总价:
   P =:  2 3        NB. prices
   Q =:  1 100      NB. quantities 
   
   total =: sum @: *
   
PQP*Qsum P * QP total Q
2 31 1002 300302302

关于组合的更多信息,请参考第8章。


3.8 动词链

看一下这个表达:“no pain, no gain”。这是一个简洁的习语,尽管语法结构不很完整但是很好理解。它不是一个句子,没有主动词。J也有类似记法:一种简洁的惯用结构,基于某种内定的规则,这规则定义了几个函数连接在一起时的语义。

3.8.1 挂钩(Hooks) 

回忆一下之前定义的tax动词,用来计算一次购物的税额(10%):
   tax =: 0.10 & *
 
一次购物要付的总金额就是销售价格加上税额。用来计算总金额的动词可以这么定义:
   payable =: + tax
 
如果销售价格是50美元,那么:
tax 5050 + tax 50payable 50
55555


在定义(payable =: + tax)中,我们使用了动词序列:+ 后面跟着tax。这个动词序列是孤立的,通过放在赋值式的右边达到孤立效果。这样一个孤立的动词序列叫做一个“链”,2个动词的链称为“挂钩”。

我们也可以通过将两个动词孤立在括号中来形成挂钩:
   (+ tax) 50
55

挂钩的通用规则是:如果f是二元函数,g是一元函数,那么对于任意的参数y,有
        (f g) y 相当于 y f (g y)

该规则可以用下图说明:

再看一个例子,回忆一下“下取整”动词 <.(计算其参数的整数部分)。测试一个数字是不是整数,可以通过测试它是否和自身取整后的结果相等来完成。动词“是否等于下取整”可以用挂钩来定义 (= <.):
   wholenumber  =:  = <.
   
y =: 3 2.7<. yy = <. ywholenumber y
3 2.73 21 01 0

3.8.2 分叉(Forks)

现在要计算一列数字L 的算术平均数:用数列L的和除以L的元素个数(还记得吗?求元素数可以使用一元动词 #)。
L =: 3 5 7 9sum L# L(sum L) % (# L)
3 5 7 92446

用来求算术平均数的动词可以这样定义,将三个动词链接起来:sum % # 。
   mean =: sum % #
   
   mean L
6
孤立的三动词序列称为分叉。通用规则为:如果f是一元动词,g是二元动词,h是一
元动词,那么对于任意参数y,有:
        (f g h) y     相当于   (f y) g (h y) 

该规则可以用下图说明:


我们再用一个例子展示一下分叉:一个数列的范围可以用分叉  
smallest , largest 来表示(中间的动词是“追加” ,)。
回忆一下,在第1章中,求一列数字的最大值用动词 >./,最小值则用 <./
   range =: <./  ,  >./
   
Lrange L
3 5 7 93 9


挂钩和分叉是动词序列,也叫做“动词链”。关于动词链的更多内容,请参考第9章。

 

3.9 综合回顾

我们用一个大一点的例子把上面学到的概念串起来。
这个例子的目的是定义一个动词,用来展示一列数字,显示每个数字占总数的百分比。

一开始我先给出完整的程序,这样后面就可以清楚地知道我们到了哪一步。现在不需要探究具体的细节,因为在下面会给出程序的解释。现在只需要知道我们看到的是一个6行的程序,定义了一个叫做 display 的动词以及一些辅助动词。
   percent  =: (100 & *) @: (% +/)
   round    =: <. @: (+&0.5)
   comp     =: round @: percent
   br       =: ,.  ;  (,. @: comp)
   tr       =: ('Data';'Percentages') & ,
   display  =: (2 2 & $) @: tr @: br
 
我们从简单数据开始:
   data =: 3 5 
然后看一下display动词显示每个数字及其占总数的百分比(四舍五入):3是8的38%。
   display data
+----+-----------+
|Data|Percentages|
+----+-----------+
|3   |38         |
|5   |63         |
+----+-----------+

动词percent计算百分比,把每个数除以总数(通过挂钩 % +/)然后乘以100。为了免去来回
翻页的麻烦,在这里重复一下percent的定义:
   percent  =: (100 & *) @: (% +/)
看一下percent的效果:
data+/ datadata % +/ data(% +/) datapercent data
3 580.375 0.6250.375 0.62537.5 62.5


让我们把百分比四舍五入成整数:通过加上0.5然后用动词 <. 向下取整。动词round定义为:
   round    =: <. @: (+&0.5)
 
用来计算要显示的百分比的动词为:
   comp     =: round @: percent
   
datacomp data
3 538 63


我们想要数据竖着显示。要将一个列表转换成1列阵列,可以使用内置动词 ,.(逗号 点,叫做“散开元素”)。
data,. data,. comp data
3 53
5
38
63


为了构造显示结果中底下那一行,我们定义动词br,br是一个分叉,将数据和计算结果以列形式连接起来:
   br  =: ,.  ;  (,. @: comp)
   
databr data
3 5+-+--+
|3|38|
|5|63|
+-+--+

为了构造显示结果中上面那一行(标题栏),我们可以这么操作:底下那一行可以看作是两
个盒子的列表,在这个序列的前边我们在加入两个盒子用来表示标题栏,一共是4个盒子。
我们定义动词 tr 来完成上述操作:
 
   tr  =: ('Data';'Percentages') & , 

databr datatr br data
3 5+-+--+
|3|38|
|5|63|
+-+--+
+----+-----------+-+--+
|Data|Percentages|3|38|
|    |           |5|63|
+----+-----------+-+--+


剩下要做的事就是将4个盒子的列表转换成2x2的阵列:
   (2 2 & $)  tr br data
+----+-----------+
|Data|Percentages|
+----+-----------+
|3   |38         |
|5   |63         |
+----+-----------+
 
将这些整合起来:
   display =: (2 2 & $) @: tr @: br
   
   display data
+----+-----------+
|Data|Percentages|
+----+-----------+
|3   |38         |
|5   |63         |
+----+-----------+

display函数包括两大部分:函数comp完成计算部分(四舍五入后的百分数),剩下的函数完
成数据的显示工作。通过改变comp的定义,我们可以显示其他函数的计算结果。假如我们将
comp定义为内置的求平方根函数(%:):
   comp =: %:
同时替换标题栏,修改tr函数:
   tr   =: ('Numbers';'Square Roots') & ,
   
   display 1 4 9 16
+-------+------------+
|Numbers|Square Roots|
+-------+------------+
| 1     |1           |
| 4     |2           |
| 9     |3           |
|16     |4           |
+-------+------------+
 
总结一下,我们已经通过一小段程序看到了J的一些标志性的特性:绑定,组合,挂钩和分叉。
至于这整个程序,这里展示的只是众多写法中的一种。
 
本章中我们首先学习了如何定义函数。存在两类函数:动词和运算符。目前我们只关注如何
定义动词,在下一章中我们将看到另一种定义动词的方法。而在第13章中我们将看到如何定
义运算符。
 
好了,就到这里。 

没有评论:

发表评论