隐式(tacit)编程,或称为函数级编程,是一种编程范型,也叫做无点(point-free)样式。其中函数定义不标示所要运算的被称为“点”的参数,转而函数定义只是其他函数的复合,比如那些操纵参数的组合子。隐式编程有着理论价值,因为严格的使用复合导致程序适配于。隐式编程是特定编程语言的天然样式,这包括了APL的一些现代实现和方言,和串接式语言比如Forth。由于缺少参数命名,认为这种风格导致了不必要的晦涩难懂的人,给它起了个绰号叫做“无意义”(pointless)风格:
: fib ( n -- Fn )
dup 1 > [
[ 1 - fib ] [ 2 - fib ] bi +
] when ;
这里的( n -- Fn )是叫做“堆栈作用”声明的一种注释。
APL系语言
在APL的一些当代方言中,允许将函数组合入服从几个规则的“列车”(train);这允许建立复杂的派生函数,而不需要显式指定任何参数;实现了列车的APL方言包括:J语言、Dyalog APL、dzaima/APL、ngn/apl和NARS2000表达为:
avg ← +⌿ ÷ ≢
cos ← 2 ○ ⊢
sin ← 1 ○ ⊢
j ← {⍺←0 ⋄ ⍺+0j1×⍵} ⍝ j函数的定义不是隐式的
Euler ← *∘j = cos j sin
这里采用定义了j函数,其中在{与}之间由⋄分隔的是守卫的表达式序列(这里只有表达式),⍺指示左参数而⍵指示右参数,⍺←指示一元定义需要的缺省左参数。
纯函数式语言
下面是采用纯函数式编程语言Haskell的一个简单例子,它在一个列表上作合计的函数。编程者可以使用“有点”(pointed)也称为值级编程的方法,而递归的定义这个合计为:
sum (x:xs) = x + sum xs
sum [] = 0
这是一种折叠(fold)运算,编程者可以将它改写为:
sum xs = foldr (+) 0 xs
这里的参数是不必须的,进而将它改写成如下“无点”也称为函数级编程的样式:
sum = foldr (+) 0
Haskell拥有算子:
(.) :: (b -> c) -> (a -> b) -> a -> c
(.) f g = \x -> f (g x)
它有如下性质:
f . g = f(g) = (.) f g
f . g = (f .) g = ((.) f) g
f . g = (. g) f
这里的((.) f)和(. g),分别称为.的左“分节”(section)和右“分节”,是对这个中缀算子的左侧和右侧部份应用。左分节的((.) f)表示形式,在从左至右分析之时比(f .)要易于区分于右分节(. g)。在“无点”中的点(point)指称参数,而非不使用点号(period)。
下面的例子展示其用法,给出一个函数p的定义:
p x y z = f (g x y) z
在伪代码形式下,对函数f(x, y)的右结合性的柯里化形式\text{curry}(f) = \lambda x.(\lambda y.(f(x,y))) = \lambda x. \lambda y. f(x,y),采用括号显式的表示出其的左结合性,即\text{curry}(f) \; x \;y = (\text{curry}(f) \; x) \;y,经过如下推导:
p = \x -> \y -> \z -> f (g x y) z
= \x -> \y -> \z -> (f ((g x) y)) z
= \x -> \y -> f ((g x) y)
= \x -> \y -> (f . (g x)) y
= \x -> ((.) f) (g x)
= \x -> (((.) f) . g) x
= ((.) f) . g
它可以归约成无点的等价定义:
p = ((.) f) . g
这里最右侧的g首先接受从左至右的实际参数,而随后接受余下的从左至右的实际参数的左分节((.) f),由于复合运算的右结合性它必须保留最外层括号。
下面是一个复杂一些的例子,这里的mf是一个先做映射(map)再加过滤器(filter)的函数,它接受一个列表list,向它应用一个函数operator,接着基于一个准则criteria来过滤元素:
mf criteria operator list = filter criteria (map operator list)
在伪代码形式下,设x = criteria; y = operator; z = list,经过如下推导:
mf = \x -> \y -> \z -> filter x (map y z)
= \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
= (. map) . ((.) . filter)
它可以归约为无点样式:
mf = (. map) . (.) . filter
这里最右侧的左分节(.) . filter首先接受从左至右的实际参数,由于复合运算的右结合性它可以省略最外层括号;而随后接受余下的从左至右的实际参数的是右分节(. map),由于复合运算的右结合性它必须保留最外层括号。
Python
如下Python代码中的函数定义和一序列的运算,对应前面示例中Unix管道中的命令:
def sort(argv):
return sorted(argv, key=str)
def uniq_c(argv):
counts = {}
for i in argv:
counts[i] = counts.get(i, 0) + 1
return ((v, 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)))
这里的uniq_c()所采用的实现方法,不要求其输入数据是有序的。
基于标准库的函数式编程工具functools中的部份应用函数partial()和归约函数reduce(),这个例子可以写为无点样式的没有参数的一序列函数的复合:
from functools import partial, reduce
def compose_left(*func_list):
return partial(reduce, lambda v, f: f(v), func_list)
c = compose_left(sort, uniq_c, sort_rn)(a)
assert b == c
这里用到的标准库函数:
*,从左至右地在可迭代对象iterable的项目上,累计应用有两个实际参数的函数function,从而将iterable归约成一个单一的值。给function的左参数是累计值,而右参数是来自iterable的更新值。如果可选的初始值initial存在,则将它用作最初的累计值。
,返回一个新的partial对象。这个对象在被调用时表现得如同func(args, **keywords),这里args是位置实际参数列表,而keywords是关键字实际参数字典。它在被调用之时,如果提供了更多的实际参数,则将它们添加到args;如果提供了额外的关键字实际参数,则用它们扩展和覆盖keywords。
参见
- 组合子逻辑
- 函数级编程
注释和引用
外部链接
- [https://function-level.github.io/ From Function-Level Programming to Pointfree Style]
- [http://portal.acm.org/citation.cfm?id=114065&dl=GUIDE&coll=GUIDE Pure Functions in APL and J] How to use tacit programming in any APL-like language
- [http://dirkgerrits.com/publications/john-backus.pdf#section.8 Closed applicative languages 1971 - 1976 ff] , in John W. Backus (Publications)
评论 (0)