プログラミングHaskellのunfoldを3言語で

プログラミングhaskell 7章の練習問題にある unfold について。
Haskell 標準関数 unfoldr の話ではないです。

●まず haskell

本で提示されてるコード

unfold p h t x | p x = []
               | otherwise = h x : unfold p h t (t x)

わかりにくいので、パラメータの意味に則した名前を付けてみる。

unfold finish nextElem nextSeed seed
    | finish seed = []
    | otherwise = nextElem seed : unfold finish nextElem nextSeed (nextSeed seed)

さらに再帰によって値が変化するパラメータと、固定値を切り分けてみる。

unfold finish nextElem nextSeed seed
    | finish seed = []
    | otherwise = nextElem seed : unfold' (nextSeed seed)
    where unfold' = unfold finish nextElem nextSeed

unfold' は、unfold の評価の過程で常に変ることのない3引数を部分適用で固定した関数。
再帰のたびに更新されるのは seed で次の seed は nextSeed により得られる。
(nextElem seed) が次のリスト要素を生成し、1階層下の unfold' 再帰評価の結果(であるところのリスト)に cons する。再帰は (finish seed) が真になったところで停止。

練習問題の解答 4関数

int2bin = unfold (==0) (`mod` 2) (`div` 2)
chop8 = unfold null (take 8) (drop 8)
map' f = unfold null (f . head) tail
iterate' f = unfold (\_ -> False) id f

それぞれ、

-- 整数をビットパターンへ分解
int2bin 10 -- => [0,1,0,1]
-- リストを 8 要素ずつに分割
chop8 [1..20] -- => [[1,2,3,4,5,6,7,8],[9,10,11,12,13,14,15,16],[17,18,19,20]]
-- map と iterate の再実装
map' (*2) [1..5]         -- => [2,4,6,8,10]
take 5 (iterate' (*2) 1) -- => [1,2,4,8,16]

Clojure でも書いてみる

unfold の実装。Haskell と違い cons は遅延評価ではないので、lazy-seq で遅延を明示。

(defn unfold [finish nextElem nextSeed seed]
  (letfn [(unfold- [seed]
                   (if (finish seed)
                     '()
                     (lazy-seq (cons (nextElem seed)
                                     (unfold- (nextSeed seed))))))]
    (unfold- seed)))
Clojure の unfold で関数実装 その1(int2bin, chop8)
(def int2bin (partial unfold zero? #(bit-and % 1) #(bit-shift-right % 1)))
(def chop8 (partial unfold empty? (partial take 8) (partial drop 8)))

bit-and, bit-shift-right は、2による剰余算と除算。
ロジックは Haskell 版と一緒だが、部分適用に partial を明記しなければならない分、煩雑になってしまう。
なお、chop8 は組み込み関数 partition-all を使うことで簡単に作れる。

(def chop8 (partial partition-all 8))
Clojure の unfold で関数実装 その2(my-map-1, my-map-2)
(def my-map-1 (fn [f coll] (unfold empty? (comp f first) rest coll)))
(def my-map-2 (fn [f] (partial unfold empty? (comp f first) rest)))

Haskell と違い関数がデフォルトでカリー化されるわけじゃないので、カリー化されてる版、されてない版で使い分けが必要(?)

(my-map-1 #(* % 2) (range 1 6))    ;=> (2 4 6 8 10)
((my-map-2 #(* % 2)) (range 1 6))  ;=> (2 4 6 8 10)
Clojure の unfold で関数実装 その3(my-iterate-1, my-iterate-2)

これも map の場合と同様。

(def my-iterate-1 (fn [f seed] (unfold (fn [_] false) identity f seed)))
(def my-iterate-2 (fn [f] (partial unfold (fn [_] false) identity f)))
(take 5 (my-iterate-1 (partial * 2) 1))    ;=> (1,2,4,8,16)
(take 5 ((my-iterate-2 (partial * 2)) 1))  ;=> (1,2,4,8,16)

iterate は無限シーケンスを生成するので、unfold 定義が lazy でないと、無限ループに陥って帰って来なくなる。

Python では ... できなかった

ジェネレータや itertools 使えば作れるだろ、と思ったけどやってみたら一筋縄ではいかなかった。
以下それっぽく作った正格評価版。

from functools import partial

def unfold(finish, nextElem, nextSeed, seed):
    def unfold_ (seed):
        if finish(seed):
            return []
        else:
            return [nextElem(seed)] + unfold_(nextSeed(seed))
    return unfold_(seed)

int2bin = partial(unfold,
                  lambda n:n == 0,
                  lambda n:n & 1,
                  lambda n:n >> 1)

chop8 = partial(unfold,
                lambda xs:not bool(xs),
                lambda xs:xs[:8],
                lambda xs:xs[8:])

my_map = lambda f,xs: unfold(lambda xs: not bool(xs),
                             lambda xs: f(xs[0]),
                             lambda xs: xs[1:],
                             xs)

遅延評価ではないので、 iterate は無理。
unfold は再帰つかってるけど、スタック溢れ対策もしてない。