Overview

Clojureのフロー制御のシンタックスまとめ. 📝Clojure 条件分岐はこっち.

関数型パラダイムにおいて手続き的な処理をどう書くか

ほかのパラダイムだと条件式のロジックで使われるandやorは, Lispの世界だと式を順次評価していくような使い方なので, シーケンス制御と論理制御を合わせてこのメモで扱う(シーケンスとはフロー制御の中でのな直線的な処理の意でつかっている).

📝手続き型プログラミングのパラダイムからすると, LISP手続き的に処理を記述するにはどうすればいいんだろうということははじめにぶち当たる壁だったりする.

Clojureの世界では実はフロー制御のフォームは少なく, if, do, loop/recur程度で事足りてしまう.

Clojure: 副作用のあるシーケンス制御(do/doto)

いわゆるdoなんちゃら.

  • do
  • doto
  • dorun(cf. doall)

do

do は複数のフォームを手続き的に評価するときに利用する. いちおうなくてもいいけどClojureは関数型パラダイムなので副作用を伴う一連の手続きはdoを書いたほうが親切.

doの戻り値は最後の評価.

doto

doto は 第一引数に対して手続き的な操作を行うときに利用する. 主な利用シーンはDBに対してのコマンド発行.

Javaのライブラリをつかうときによくつかう. とくにJavaのメソッドチェーンが使われている場合. hoge.foo().bar().zzz().

dotoの戻り値は入力されたx. なので, 途中でなにをしても戻り値にxを流していきたいときにつかう(スレッディングマクロの途中で中身をのぞく).

dorun/doall

dorun, doall は遅延シーケンスのコンテキストで登場する. どちらも遅延評価されたものを強制的に評価して実行する.

そのさい, dorunは単に評価してその結果を考慮せずにnilを返す, つまり副作用があろうが関係ない. 一方doallは遅延シーケンスを評価した結果をシーケンスにして返す.

cf. Clojure: 遅延評価/遅延シーケンス

Clojure: 再帰(loop/recur)

副作用のない繰り返し. Clojureではloop/recurを利用することで再帰を実装する. recurを利用すれば末尾再帰がかける.

何らかの繰り返し処理中に更新や参照をしたい値を一時的に保持しておきたい場合(手続き型プログラミングでよくあるやつ), 関数型のClojureではloopを利用して一時変数をloopのinputとしてわたしていく必要がある.

example

  • accはaccumulatorの略で再帰実装の一次変数としてよく使われる名前.
  • iterはiteratorの略, 現在のポイントを示す, indexでもlistでもいい.
  • loopの中の配列に初期値を並べる.
  • recurはloopで宣言した引数と同じ数の引数となる.
    • 慣習としてaccにはloop内で計算した結果を集めていく.
    • loop内部でfirstをつかって計算対象を取り出したりする.
    • 別の変数でrestをつかって計算に使わなかった残りをわたしていく.
  • loopを重ねていくたびにリストから一つずつとってaccに計算結果を集めることが出来る.
(loop [iter 1
       acc  0]
  (if (> iter 10)
    (println acc)
    (recur (inc iter) (+ acc iter))))

Clojure: 副作用あり繰り返し(dotimes/doseq/for)

繰り返しのシーケンス制御について.

  • dotimes
    • 式n回評価, nilを返す.
  • doseq
    • シーケンスに対して順列に繰り返す.
    • 遅延シーケンスは評価を強制.
    • nilを返す.
    • 複数のシーケンスに対しはforEachのように振る舞う.
  • for

💡Clojureの繰り返し: doseqはforEachでforはリスト内包表記

doseqはforeach, forはリスト内包表記. forは遅延シーケンスを作成して返すのに対しdoseqは副作用を実行してnilを返す.

複数のリストから一つのリストを生成するならfor, 副作用のある外部に対する一連の処理(データベース書き込み, ログ出力, 核爆弾の発射などw)はdoseqをつかう.

ref. Difference between doseq and for in Clojure - Stack Overflow

💡Clojureの繰り返し: map vs doseq(for)

どちらもシーケンスに対する処理を実施する.

副作用があるときにdoseq(for)を利用する, そうでないときにmapを利用する.

なるべく副作用がないようにプログラミングを構築するというClojureの考えとしてはdoseqよりもmapのほうが登場回数が多い.

mapは関数をすぐには適用せずに遅延シーケンスを構築する. 実際に中の値の評価をするにはdoall, intoなどの方法が必要.

(doall (map coll))
(into [] (map coll))
(into-array (map coll))

リスト内包表記(for)はmap/filterよりもhuman-readableという利点はある. 以下は同じである.

(for [number [1 2 3]] (* number 2))
(map #(* % 2) [1 2 3])

シンプルな変換処理を有限なリストに適用するならばmap/filterよりもみやすいかもしれない(Pythonではmap/filterよりもリスト内包表記がよくつかわれる).

ひとつのシーケンスに複数の変化を施すならば遅延シーケンスを扱うmapに利点がある. 複数のシーケンスを順番に従って取り出して扱うならばforのほうが読みやすい(cf. mapcat, juxt).


💡Clojureでindexを考慮したfor文の書き方

map-indexedをつかうのがベストプラクティスのひとつ.

(doseq [[idx x] (map-indexed vector items)]
  (println idx ":" x))

💡Clojureで二重forループ(iとj)

これが…

for (int i=0;i<2;i++) {
    for (int j=i;j<3;j++) {
        [i, j];
    }
}

こう!

(for [x (range 2)
      y (range 3)]
  [x y])

Clojure and/or Macros

💡andとorを条件分岐につかう

orに与えられた式で真になるものが見つかったら残りを評価せずに真を返す(cf. yogthos/config).

andに与えられた式で偽になるものが見つからない限り残りを評価する.

ref: 📚Land of Lisp p47に書いてあった方法.

💡LISPの世界のand/orはフロー制御の評価器

Clojure(というよりもLisp)のandとorはほかのパラダイムとは少し違うことに注意. and と or は条件式(pred)を結合するのに使う. しかし, 真偽値を返すだけではなく, 値そのものが返る.

これは副作用に関わるClojureの述語(do/doto)に近い意味合いなのだ. Lispの世界において, and/or真偽を返すだけのものではない. 順番に式を評価しながら先に進んでいくようなフロー制御に使えるような評価器なのだ.

orははじめて真になった値を返してその後の評価を打ち切る. andは真になリ続ける限り次を評価して, falseになったら評価をやめる. これは面白い性質であり, これを用いていろんな制御が書ける. and, orを使いこなすとコードを短くすることができる(ref. andとorを条件分岐につかう).

ref. (clj 3) Clojure’s ‘and’ and ‘or’ are weird (but not really) | Joep Schuurkes

Clojure: Threading Macros

Clojure スレッディングマクロ.

  • 基本的には ->->> をつかう.
  • 入れ子構造の関数呼び出しを逐次処理な呼び出しに変える.

refs:

thread-first (->) と thread-last (->>)

-> も ->>も1つ目のフォームを初期値にして2つ目のフォームから逐次適用していく.

->はフォームの第一引数に引数が入る. 一方, ->>は最終引数に引数が入る.

->> 利用するケースは第一引数に高階関数を受取り末尾にリストを受け取るような関数である.(map, filter, reduce…)

see also: 💡Clojureデータ構造の操作関数の分類

as-> clojure thread-firstとthead-lastを混在させる

thread-as or as->をつかうことで, 混在させられる.

(-> [10 11]
    (conj 12)
    (as-> xs (map - xs [3 2 1]))
    (reverse))
; (11 9 7)

cond-> 条件つきスレッドマクロ

cond->は初期値と(条件, 処理)のリストを受ける. 条件が真のときのみ処理はされる.

(cond-> (初期値)
  (条件) (処理)
  (条件) (処理)
  (条件) (処理))

もし存在すれば(if-exists)そのデータを初期値に加えるようなときにつかう. 初期値がcollectionで, もしparamが存在すればそれをcollectionにaddやassocやconjみたいなケースでよく見かける.

条件付きMap操作: assoc-if/update-if 追加する値がnilでなければ操作

some->/some->> : 副作用のある呼び出しを中断

ref. some-> - Threading Macros Guide

Threadingマクロの途中で副作用のあるJavaの関数(往々にして戻り値がない, つまりnullを返す可能性がある)を呼び出す時, 途中結果がnullならばその後の処理を打ち切る用途としてsome->というスレッディングマクロがある.

Emacs clojure-mode: M-x clojure-thread

Emacs clojure-modeの threadingリファクタリングサポート.

ref: https://github.com/clojure-emacs/clojure-mode/#refactoring-support

  • clojure-unwind/clojure-unwind-all:
    • threading expressionを解く.
  • clojure-thread-first-all: -> へ変換.
  • clojure-thread-last-all: ->> へ変換.

threading macrosを理解するには, ->> の内側で M-x clojure-unwind-allを叩くとスレッディングマクロを使わない場合に変換される. もとに戻すには, M-x clojure-thread-last-all.

💡スレッディングマクロの途中で中身をのぞく

ref. Clojureで仕事をはじめて1年経った - さめたコーヒー

(->> data
      (map (fn [d] (...)))
      ((fn [a] (prn a) a))
      (filter (fn [d] (...))))

dotoも使える. inputはdotoの第一引数なことに注意(->).

(->> data
      (map (fn [d] (...)))
      (as-> x (doto x prn))
      (filter (fn [d] (...))))

スレッディングマクロをmacroexpandする

スレッディングマクロはマクロであるため, マクロ展開の関数であるmacroexpandを利用すると, 実際にどういうように評価されるのかを確認することができ, デバッグ作業を助ける.

Clojure フロー制御 Insights

✨doは特殊形式, and/orやThrading Macrosはマクロ

doもandもスレッディングマクロも, 式を手続き的に順次実行評価していくための記法であるという共通点がある.

しかしdoは副作用があっても順次実行する. andやスレッディングマクロは副作用があるフォームを紡いでいくことができない.

そしてand/orはマクロにすぎない, 真偽を返す関数やオペレーターではないというところも, 注意するべき点である.

✨Clojure Threading MacrosはR-langのdplyrのpipe記法に似ている

これは💡羽鳥教のdplyrに似てないか?

そしてこの記法の強力な魅力がデータ分析においてR言語をPythonよりも好む人がいるように, Clojureにおいても大変魅力的に違いない.

✨threadingとはわたしである

theading = 糸, 筋道. これはつまり, わたしではないか?

ref: 🌱経道とはThreadである

🔗References

up: 📂Clojure Language Spec