隱式編程

隱式（tacit）編程，或稱為函數級編程，是一種編程范型，也叫做無點（point-free）樣式。其中函數定義不標示所要運算的被稱為「點」的參數，轉而函數定義只是其他函數的複合，比如那些操縱參數的組合子。隱式編程有著理論價值，因為嚴格的使用複合導致程序適配於等式邏輯。隱式編程是特定程式語言的天然樣式，這包括了APL的一些現代實現和方言，和串接式語言比如Forth。由於缺少參數命名，認為這種風格導致了不必要的晦澀難懂的人，給它起了個綽號叫做「無意義」（pointless）風格。

## 管道主范型語言

以管道作為主范型的腳本語言有：

### Unix shell

在Unix shell腳本中，命令是從標準輸入接收數據並發送結果至標準輸出的過濾器 (軟體)。例如：

```bash
sort | uniq -c | sort -rn
```

可以視為一個隱式或無點複合，它返回它的每個參數的計數和這些參數，並按照這個計數的遞減次序來排序。命令`sort`和`uniq`相當於函數，而`-c`和`-rn`是控制這些函數的選項，但是不提及參數。管道`|`相當於函數複合算子。下面是其用例：

```console
$ send () { for i in "$@"; do echo $i; done }
$ send 2 4 3 1 3 12 2 | sort | uniq -c | sort -rn
      2 3
      2 2
      1 4
      1 12
      1 1
```

### jq語言

jq是面向JSON的純函數式程式語言，它使用`|`符號來連接過濾器形成流水線。例如：

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

這裡jq內的JSON陣列`[1,2]`是求值為陣列的一個jq過濾器。儘管類似於Unix流水線，jq流水線允許將到來數據，如同並行的發送到在`|`右手端的多於一個接收者。

上述Unix管道中的命令例子，在jq中等價的無點風格定義為：

```python
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
```

將這段代碼保存入`pipeline.jq`文件中，下面是其執行結果：

```console
$ jq -nc -f ./pipeline.jq 
[[2,3],[2,2],[1,4],[1,12],[1,1]]
```

## 串接式語言

在串接式程式語言和面向堆棧程式語言中，主要採用的就是無點方法，調用函數所給出的參數壓入堆棧之中而不需要給它們顯式的命名。例如，計算斐波那契數的過程，在Unix的dc命令中可實現為：

```bash
# F(x):
#   1 < x ? G(x) : x
# G(x):
#   F(x-1) + F(x-2)
# F()
[d1<G]sF [d 1- lFx r 2- lFx +]sG lFx
```

下面是其用例：

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

在Factor中可與其等價的實現為：

```factor
: 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。

### J語言

在J語言中，下列在一個數值的列表（陣列）上計算平均值的函數採用了一種無參數樣式代碼：

```j
avg=: +/ % #
```

`+/`通過將求和（`+`）插入（`/`）到一個陣列的所有元素之間來計算它們的合計值。`#`總計一個陣列的元素數目。`%`用`+/`這個陣列的結果值除以`#`這個陣列的結果值。

歐拉公式$e^{ix} = \cos x + i\sin x$可隱式表達為：

```j
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表達為：

```APL
avg ← +⌿ ÷ ≢

cos ← 2 ○ ⊢
sin ← 1 ○ ⊢
j   ← {⍺←0 ⋄ ⍺+0j1×⍵}  ⍝ j函数的定义不是隐式的
Euler ← *∘j = cos j sin
```

這裡採用直接函數定義了`j`函數，其中在`{`與`}`之間由`⋄`分隔的是守衛的表達式序列（這裡只有表達式），`⍺`指示左參數而`⍵`指示右參數，`⍺←`指示一元定義需要的預設左參數。

## 純函數式語言

下面是採用純函數式程式語言Haskell的一個簡單例子，它在一個列表上作合計的函數。編程者可以使用「有點」（pointed）也稱為值級編程的方法，而遞歸的定義這個合計為：

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

這是一種摺疊（fold）運算，編程者可以將它改寫為：

```haskell
sum xs = foldr (+) 0 xs
```

這裡的參數是不必須的，進而將它改寫成如下「無點」也稱為函數級編程的樣式：

```haskell
sum = foldr (+) 0
```

Haskell擁有函數複合 (電腦科學)算子：

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

它有如下性質：

```haskell
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`的定義：

```haskell
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$，經過如下推導：

```haskell
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
```

它可以歸約成無點的等價定義：

```haskell
p = ((.) f) . g
```

這裡最右側的`g`首先接受從左至右的實際參數，而隨後接受餘下的從左至右的實際參數的左分節`((.) f)`，由於複合運算的右結合性它必須保留最外層括號。

下面是一個複雜一些的例子，這裡的`mf`是一個先做映射（map）再加過濾器（filter）的函數，它接受一個列表`list`，向它應用一個函數`operator`，接著基於一個準則`criteria`來過濾元素：

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

在偽代碼形式下，設

```haskell
x = criteria; y = operator; z = list
```

經過如下推導：

```haskell
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)
```

它可以歸約為無點樣式：

```haskell
mf = (. map) . (.) . filter
```

這裡最右側的左分節`(.) . filter`首先接受從左至右的實際參數，由於複合運算的右結合性它可以省略最外層括號；而隨後接受餘下的從左至右的實際參數的是右分節`(. map)`，由於複合運算的右結合性它必須保留最外層括號。

## Python

如下Python代碼中的函數定義和一序列的運算，對應前面示例中Unix管道中的命令：

```python
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()`，這個例子可以寫為無點樣式的沒有參數的一序列函數的複合：

```python
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`。

## 外部連結

- From Function-Level Programming to Pointfree Style
- Pure Functions in APL and J How to use tacit programming in any APL-like language
- Closed applicative languages 1971 - 1976 ff , in John W. Backus (Publications)