読者です 読者をやめる 読者になる 読者になる

夢とガラクタの集積場

落ちこぼれ三流エンジニアである管理人の夢想=『夢』と、潰えた夢=『ガラクタ』の集積場です。

Clojure勉強日記(その6 2.6 forループはどこにある?

というわけで、続きです。

これまで見てきたが、forループがないし、直接変更可能な変数というものも登場していない。
単に、関数とインプット、アウトプットがあるだけという非常にシンプルなIPOを示すのみとなっている。

・・・というのがJava辺りから入った人間の抱く疑問だそうな。私も実際疑問です。

そんなわけで、Javaで行っているindexOfAnyのコードを実際にClojureで追ってみる形になる。
2重forループ、ローカル変数を含んだコードとなっている。

public static int indexOfAny(String str, char[] searchChars) {
    if (isEmpty(str) || ArrayUtils.isEmpty(searchChars)) {
        return -1;
    }
    for (int i = 0; i < str.length(); i++) {
        char ch = str.charAt(i);
        for (int j = 0; j < searchChars.length; j++) {
            if (searchChars[j] == ch) {
                return i;
            }
        }
    }
    return -1;
}

要は対象の文字列から指定した文字列のインデックスを返す関数。
単に一致した文字を返すだけなら下記のようにfilter関数で可能。

user=> (defn filtertest [chars target]
    (filter chars target))
user=> (filtertest #{\a \T} "TargetTest")
(\T \a \T)

ただ、これだとインデックスが返せないためさらに別のことを行う必要がある。
(defn indexed [coll] (map vector (iterate inc 0) coll))
上記の関数で、「インデックス 要素」という組を返すことができる。
内訳は・・・
(iterate inc 0)で0オリジンのインデックスを提供。collが要素を提供。
vectorは[インデックス 要素]のベクタを生成する。
ただ、vector自体は空要素でもベクタを生成できるため、うっかり下記のように実行すると無限ループになる。

(vector (iterate inc 0) "TestTest")

下記のコメントにもある通り、map関数はどれか一つの要素が尽きた段階でmapの生成を止める。
そのおかげでcoll部分の要素が尽きた段階でmapとして返る。

user=> (doc map)
-------------------------
clojure.core/map
([f coll] [f c1 c2] [f c1 c2 c3] [f c1 c2 c3 & colls])
  Returns a lazy sequence consisting of the result of applying f to the
  set of first items of each coll, followed by applying f to the set
  of second items in each coll, until any one of the colls is
  exhausted.  Any remaining items in other colls are ignored. Function
  f should accept number-of-colls arguments.
nil
user=> (map vector (iterate inc 0) "TargetTest")
([0 \T] [1 \a] [2 \r] [3 \g] [4 \e] [5 \t] [6 \T] [7 \e] [8 \s] [9 \t])

後はこれを引数を受け取ってmapを返すことを関数化したものがindexedとなる。

user=> (defn indexed [coll] (map vector (iterate inc 0) coll))
#'user/indexed
user=> (indexed "Test")
([0 \T] [1 \e] [2 \s] [3 \t])

あとは、マッチするもののインデックスを返せばいい。

(defn index-filter [pred coll]
    (for [[index targetChar] (indexed coll) :when (pred targetChar)] index))
user=> (index-filter #{\a \b} "aadcdd")
(0 1)

ここで初めてforが登場したが、forはループでなく、「シーケンスの内包表記」という模様。
後でまた登場するそうなのでとりあえず詳細はさておき、順に実行するという意味ではJavaとそう変わらない気がするが・・・
尚、上記の式は「indexed coll」で分割した結果を「index targetChar」に割り振り、
targetCharが与えた条件に一致した場合にindexを返す・・・ということを繰り返した結果となる。
初めの値を返すようにするには下記のようにする。

user=> (defn index-of-any [pred coll]
    (first (index-filter pred coll)))
#'user/index-of-any
user=> (index-of-any #{\a \b} "aaa")
0
user=> (index-of-any #{\a \b} "CCCCC")
nil

なんか妙に単純に書けてしまったが、JavaClojureに比べて下記のように余分な要素がある。

  1. 文字列が空の場合、探索対象が空の場合などいくつかの特殊ケースを扱う。だが、Clojureの場合マッチしたものだけ扱うためシンプル。
  2. Javaの場合コレクションを順に見るためのローカル変数が必要だが、Clojureではmapのような高階関数とloopで対応可能

尚、index-of-anyは下記のように「はじめに登場したもの」であればKeywordでも判定可能ではある。

user=> (index-of-any #{:a} [:b :b :b :a])
3