Python函數語言程式設計系列010:惰性列表之動手實現List

三次方根發表於2021-10-23

這篇文章,我們要動手實現一個List,不過和一般的文章不同,我們這裡不用類來實現,而是用基本的資料結構,二元元組(a, b)和空元組()來實現。這兩個都可以通過lambda直接定義出來,具體方法可以參考上一篇的內容。

我們考慮一下,List(也叫連結串列),最關鍵的是建立一個模式,可以無窮展開自己,儲存一個值和下一個資料的,例如[1, 2, 3, 4]我們可以用(1, (2, (3, (4, ()))))。我們必須指定一個結尾,這個就是()在其中的作用,()同時代表空列表和列表結尾的含義。很容易地,我們可以將列表定義如下(我這裡包了個函式,只是為了將資料隔離,防止我們使用自帶的比較來實現一些功能):

def cons(head, tail):
    def helper():
        return (head, tail)
    return helper

然後我們定義兩個函式,來獲取裡面的資料,類似上一篇介面中的firstsecond

head = lambda cons_list: cons_list()[0]
tail = lambda cons_list: cons_list()[1]

我們可以定義一個函式表示空的變數empty_list_base,這之後,為了方便計算,我們可以寫一個生成cons的的方便的方法(當然這個實現用了*arg的概念,我們預設使用這個語法糖特性):

def cons_apply(*args):
    if len(args) == 0:
        return empty_list_base
    else:
        return cons(args[0], cons_apply(*args[1:]))

這樣我們就可以很方便地完成新建List了:

>>> cons_apply(1, 2, 3) # 返回cons(1, cos(2, cos(3, ())))

為了方便比較,我們也可以定義一個判斷列表是否相等的函式:

def equal_cons(this: ListBase[S], that: ) -> bool:
    if this == empty_list_base and that != empty_list_base:
        return False
    elif this != empty_list_base and that == empty_list_base:
        return False
    elif this == empty_list_base and that == empty_list_base:
        return True
    else:
        return head(this) == head(that) and equal_cons(tail(this), tail(that))

現在我們就可以很方便地做一些驗證了。

>>> assert equal_cons(cons_apply(1, 2, 3), cons(1, cons(2, cons(3, ()))))

現在我們需要就是要實現一些不需要迴圈實現的列表運算,就是上一篇說的mapfold_leftfilter

map的作用是將函式f帶入到列表的每一個值,即我們帶入f到列表的頭之後,再把map應用到tail中,即:

def map_cons(f, cons_list):
    if cons_list == ():
        return empty_list_base
    else:
        return cons(f(head(cons_list)), map_cons(f, tail(cons_list)))

同理,我們可以實現filterfold_left:

def filter_cons(f, cons_list):
    if cons_list == ():
        return empty_list_base
    else:
        hd, tl = head(cons_list), tail(cons_list)
        if f(hd):
            return cons(hd, filter_cons(f, tl))
        else:
            return tl

def fold_left_cons(f, init, cons_list):
    if cons_list == ():
        return init
    else:
        return fold_left_cons(f, f(init, head(cons_list)), tail(cons_list))

這樣,我們就可以實現一些基本功能了,比如將[1, 2, 3, 4, 5]每個元素加一,篩選偶數求和,就可以寫成:

>>> res = fold_left_cons(lambda x, y: x + y, 0,
>>>    filter_cons(lambda x: x % 2 == 0, 
>>>        map_cons(lambda x: x + 1,
>>>            cons_apply(1, 2, 3, 4, 5)
>>>    )))
>>> res == 12

當然,這種風格的程式碼,巢狀的可讀性很差,這裡我們就想到了之前我們實現的and_thencompose函式,可以組合這些水管構造的東西。不過,我們將這些函式改成科裡化會更方便的寫。這樣就可以用函式組合的風格了:

map_cons_curry = lambda f: lambda cons_list: map_cons(f, cons_list)
filter_cons_curry = lambda f: lambda cons_list: filter_cons(f, cons_list)
fold_left_cons_curry = lambda f: lambda init: lambda cons_list: fold_left_cons(f, init, cons_list)

具體的呼叫就是下面的方法了:

>>> f = and_then(
>>>    map_cons_curry(lambda x: x + 1),
>>>    filter_cons_curry(lambda x: x % 2 == 0),
>>>    fold_left_cons_curry(lambda x, y: x + y)(0),
>>> )
>>>
>>> assert f(cons_apply(1, 2, 3, 4, 5)) == 12

如果你使用了我維護的這個fppy(點選這裡進入)的例子的話,你也可以使用一個F_的修飾器輪子,這樣就可以實現另一種基於類的鏈式寫法:

from fppy.base import F_, I

F_(I)\
    .and_then(map_cons_curry(lambda x: x + 1))\
    .and_then(filter_cons_curry(lambda x: x % 2 == 0))\
    .and_then(fold_left_cons_curry(lambda x, y: x + y)(0))\
    .apply(cons_apply(1, 2, 3, 4, 5)) # 返回12

這篇之中,我們簡單僅用二元元組、相等、函式的概念,維護了一個列表的結果,並能通過一些列表函式對齊進行遍歷計算、篩選。下一篇之中,我們講開始粗略地討論類、型別這些概念,這將方便我們以後的討論。

相關文章