OnLisp with Clojure: find2

GW だし OnLisp でも進めようかな、ということで進めている。
ただ、 OnLisp はもともと Clojure ではなくて Common Lisp を対象としているので、 Clojure への翻案などを書き記していったりするかもしれない。

さて今回の本題となるのは 4.1 節で紹介されている find2 である。

下準備

まず、 bookshop を勘違いしていた。
おそらく、本で意図された bookshop 関数は以下のようにデータ入出力系の副作用を含む関数なのだろう。

(defn bookshop
  [town]
  ;; DB or file I/O
  )

自分は以下のような関数だと思って読み進めてしまったので、少し混乱してしまった。

(defn bookshop
  [town bookshop-lst]
  ((keyword town) bookshop-lst))

;; 引数となる bookshop-lst は hash-map
{:imperial-city ["First Edition"]}

結局は以下のような multi-arity 関数を定義して使ってみることにした。

(defn bookshop
  ([town]
    (bookshop town bookshop-lst))
  ([town bookshop-lst]
    ((keyword town) bookshop-lst)))

データとしては以下のような vector, hash-map を作成している。

(def towns
  ["imperial-city" "cheydinhal" "bruma" "bravil" "chorrl" "skingrad" "leyawiin" "kvatch" "anvil"])

(def bookshop-lst
  {:imperial-city ["First Edition"]
   :cheydinhal ["Mach-Na's Books"]
   :chorrl ["Renoit's Books"]
   :leyawiin ["Southern Books"]})

find2

find2 はこんな感じに。
確か Clojure は末尾再帰だと Stack Overflow の危険があったはずなので、代わりに looprecur の組み合わせを使っている。
また、 Clojure では多値返却のために values ではなく、 vector を利用するのでそのように変更している。
あとはまぁ、 carcdr が first, rest に変わるとかの普通の方言間の差異ぐらい。

(defn find2
  [fn-lst lst]
  (loop [fn-lst fn-lst
         lst lst]
    (if (nil? lst)
      nil
      (let [val (fn-lst (first lst))]
        (if val
          [(first lst) val]
          (recur fn-lst (rest lst)))))))

使い方は Common Lisp 版と同じ。

(find2 bookshop towns)
=> ["imperial-city" ["First Edition"]]
(find2 bookshop (reverse towns))
=> ["leyawiin" ["Southern Books"]]

findn

さて、折角だから最初に自分が勘違いしたように、 bookshop で検索対象のリストを指定できるようにした関数も作っておくことにする。
しかし、単純に引数を 3 つに増やすのではなく、汎用化して 3 つめ以降の引数はまとめて検索対象として検索関数への入力に利用するようにしてみる。
名前は findn としておく。

(defn findn
  [fn-lst & lst]
  (loop [fn-lst fn-lst
         find-lst (first lst)
         val-lst (rest lst)]
    (if (nil? find-lst)
      nil
      (let [val (apply fn-lst (first find-lst) val-lst)]
        (if val
          [(first find-lst) val]
          (recur fn-lst (rest find-lst) val-lst))))))

ただ単に変数を増やしたわけではなく、検索を行う際に検索関数の fn-lst を直接実行するのではなく、 val-lst を展開するために apply を適用するようにしている。

これに伴って bookshop 関数も可変長引数を受け取れるようにしておく。

(defn bookshop
  ([town]
   (bookstore-in-town town bookshop-lst))
  ([town & val-lst]
   ((keyword town) (apply conj val-lst))))

使用時は検索対象の hash-map を増やしていけばいい感じ。

(findn bookshop towns)
=> ["imperial-city" ["First Edition"]]
(findn bookshop (reverse towns) bookstore bookshop-lst)
=> ["leyawiin" ["Southern Books"]]
(findn bookshop cities bookstore bookshop-lst)
=> ["Tokyo" ["Shosen" "Kinokuniya" "Taiseido"]]