隐式编程

隐式tacit)编程[1],或称为函数级编程,是一种编程范型,也叫做无点(point-free)样式。其中函数定义不标示所要运算的被称为“”的参数,转而函数定义只是其他函数的复合,比如那些操纵参数的组合子。隐式编程有着理论价值,因为严格的使用复合导致程序适配于等式推理英语Equational logic[2]。隐式编程是特定编程语言的天然样式,这包括了APL的一些现代实现和方言[3],和串接式语言比如Forth。由于缺少参数命名,认为这种风格导致了不必要的晦涩难懂的人,给它起了个绰号叫做“无意义”(pointless)风格[2]

APL语言家族

在一些APL方言中,允许将函数组合入服从几个规则的“列车”(train);这允许建立复杂的派生函数,而不需要显式指定任何参数;实现了列车的APL方言包括:J语言、Dyalog APL、dzaima/APL、ngn/apl和NARS2000[1]

J语言

J语言中,下列在一个数值的列表(阵列)上计算平均值的函数采用了一种无参数样式代码:

avg=: +/ % #

+/通过将求和(+)插入(/)到一个阵列的所有元素之间来计算它们的合计值。#总计一个阵列的元素数目。%+/这个阵列的结果值除以#这个阵列的结果值。

欧拉公式 可隐式表达为:

cos =: 2 o. ]
sin =: 1 o. ]
Euler =: ^@j. = cos j. sin

这里定义的函数Euler在任何输入值上都恒等于1,即这个等式永远为真。其中用到了一些原语(primitive)函数:=:表示全局定义;o.表示圆函数,由左侧名词参数选择具体的函数;]不变动的返回右侧名词参数;^的一元定义为指数函数;j.的一元定义为虚数单位0j1乘以右侧参数y,而它的二元定义为x + 0j1*y,即组合左侧参数x和右侧参数y成为复数,而二者分别是其实部和虚部;@表示数学函数复合=等于布尔运算,两侧参数相等返回1而不等返回0

Dyalog APL

上述相同的隐式计算用APL的现代版本Dyalog APL[4]表达为:

avg  + ÷ 

cos  2  
sin  1  
j    {0  +0j1×}  ⍝ j函数的定义不是隐式的
Euler  *j = cos j sin

这里采用直接函数英语Direct function定义了j函数,其中在{}之间由分隔的是守卫的表达式序列(这里只有表达式),指示左参数而指示右参数,⍺←指示一元定义需要的缺省左参数。

Unix管道

Unix shell脚本中,命令是从标准输入接收数据并发送结果至标准输出过滤器英语Filter (software)。例如:

sort | uniq -c | sort -rn

可以视为一个隐式或无点复合,它返回它的每个参数的计数和这些参数,并按照这个计数的递减次序来排序。命令sortuniq相当于函数,而-c-rn是控制这些函数的选项,但是不提及参数。管道|相当于函数复合算子。下面是其用例:

$ echo '2 4 3 1 3 12 2' | xargs -n1 | sort | uniq -c | sort -rn
      2 3
      2 2
      1 4
      1 12
      1 1

Python

如下Python代码是对应上节示例中Unix管道中命令的函数定义和一序列的运算:

def sort(argv):
    return sorted(argv, key=str)

def uniq_c(argv):
    counts = {}
    for i in argv:
        counts[str(i)] = counts.get(str(i), 0) + 1
    return [(v, int(k)) for k , v in counts.items()]

def sort_rn(argv):
    sort_rk2 = sorted(argv, key=lambda x:str(x[1]), reverse=True)
    return sorted(sort_rk2, key=lambda x:x[0], reverse=True)

a = [2, 4, 3, 1, 3, 12, 2]
b = sort_rn(uniq_c(sort(a)))

这里的sort()由于uniq_c()所采用的实现方法不再依赖于它而没有存在必要,只是为了与上节Unix示例保持一致。

基于标准库的函数式编程工具functools中的部份应用英语Partial application函数partial()归约函数reduce(),这个例子可以写为无点样式的没有参数的一序列函数的复合[5]

from functools import partial, reduce

def compose(*func_list):
    return partial(reduce, lambda argv,func:func(argv), func_list)

pipeline = compose(sort, uniq_c, sort_rn)
c = pipeline(a)
assert b == c

jq语言

jq是面向JSON纯函数式编程语言,它使用|符号来连接过滤器形成流水线。例如:

$ echo '[1,2]' | jq 'add/length'
1.5
$ jq -n '[1,2] | add/length'
1.5

这里jq内的JSON阵列[1,2]是求值为阵列的一个jq过滤器。尽管类似于Unix流水线,jq流水线允许将到来数据,如同并行的发送到在|右手端的多于一个接收者。

上述Python章节中例子,在jq中等价的无点风格定义为:

def uniq_c:
    map(tostring) 
    | reduce .[] as $i ({}; .[$i] += 1)
    | to_entries | map([.value, .key]);
def sort_nr:
    sort | reverse | map([.[0], .[1]|tonumber]);
[2, 4, 3, 1, 3, 12, 2] 
| sort | uniq_c | sort_nr

函数式编程语言

下面是采用纯函数式编程语言Haskell的一个简单例子,它在一个列表上作合计的函数。编程者可以使用“有点”(pointed)也称为值级编程的方法,而递归的定义这个合计为:

sum (x:xs) = x + sum xs
sum [] = 0

这是一种折叠(fold)运算,编程者可以将它改写为:

sum xs = foldr (+) 0 xs

这里的参数是不必须的,进而将它改写成如下“无点”也称为函数级编程的样式:

sum = foldr (+) 0

Haskell拥有函数复合英语Function composition (computer science)算子

(.) :: (b -> c) -> (a -> b) -> a -> c
(.) f g = \x -> f (g x)

它有如下性质[6]

f . g = f(g)
f . g = (.) f g
f . g = (f .) g 
f . g = (. g) f

这里的(f .)(. g),分别称为.的左“节”(section)和右“节”,是对这个中缀算子的左侧和右侧部份应用英语Partial application(f .) = ((.) f),本文采用后者形式。正如前面所说,在“无点”中的(point)指称参数,而非不使用点号(dot),这是常见误解[7]

下面的例子展示其用法,给出一个函数p的定义:

p x y z = f (g x y) z

经过如下推导:

p = \x -> \y -> f (g x y)
  = \x -> \y -> (f . (g x)) y
  = \x -> f . (g x)
  = \x -> ((.) f) (g x)
  = \x -> (((.) f) . g) x
  = ((.) f) . g

它可以归约成无点的等价定义:

p = ((.) f) . g

下面是一个复杂一些的例子[8],这里的mf是一个先做映射(map)再加过滤器(filter)的函数,它接受一个列表list,向它应用一个函数operator,接着基于一个准则criteria来过滤元素:

mf criteria operator list = filter criteria (map operator list)

经过如下推导:

mf = \x -> \y -> \z -> filter x (map y z)
   = \x -> \y -> \z -> (filter x) . (map y) z
   = \x -> \y -> (filter x) . (map y)
   = \x -> \y -> ((.) (filter x)) (map y)
   = \x -> \y -> ((.) (filter x)) . map y
   = \x -> ((.) (filter x)) . map
   = \x -> (. map) ((.) (filter x))
   = \x -> (. map) ((.) . filter x)
   = \x -> (. map) . ((.) . filter) x
   = (. map) . ((.) . filter)
   = (. map) . (.) . filter

mf可以表达为无点样式:

mf = (. map) . (.) . filter

串接式编程语言

串接式编程语言面向堆栈编程语言中,无点方法也很常用。例如,计算斐波那契数的过程,在Unixdc命令中可实现为:

$ echo 7 | dc -e '? [d 1<G]sF [1-d 1- lFx r lFx +]sG lFx p'
13

Factor中与其等价的实现为[9]

: fib ( n -- Fn )
    dup 1 > [
        [ 1 - fib ] [ 2 - fib ] bi +
    ] when ;

参见

注释和引用

  1. ^ 1.0 1.1 Tacit programming. [2022-06-11]. (原始内容存档于2022-07-20). 
  2. ^ 2.0 2.1 Manuel Alcino Pereira da Cunha (2005) Point-free Program Calculation
  3. ^ W. Neville Holmes, ed. (2006) Computers and People
  4. ^ Dyalog APL. [2022-06-14]. (原始内容存档于2022-06-28). 
  5. ^ Name code not values. Concatenative.org. [2020-10-16]. (原始内容存档于2013-09-29). 
  6. ^ Functional Composition with Multiple Parameters in Haskell. 
  7. ^ Pointfree - HaskellWiki. wiki.haskell.org. [2016-06-05]. (原始内容存档于2021-04-28). 
  8. ^ pipermail. [2020-04-18]. (原始内容存档于2012-02-18). 
  9. ^ Rosetta Code — Fibonacci sequence. 

外部链接