隱式編程

隱式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指令碼中,命令是從標準輸入接收數據並行送結果至標準輸出的電腦程式。例如:

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純函數式程式設計語言,它使用|符號來連接過濾器形成管線化。例如:

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

這裏的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,接着基於一個準則criterion來過濾元素:

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

串接式程式語言

串接式程式語言堆疊導向編程語言中,無點方法也很常用。例如,計算斐波那契數列的過程可以用Factor寫為如下[9]

: fib ( n -- Fn )
    dup 2 < [
        [ 1 - fib ] [ 2 - fib ] bi +
    ] unless ;

參見

註釋和參照

  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. 

外部連結