clojure備忘録[replを使った開発手法 その3(filter関数)]

標準のfilter関数の挙動

続いて実装する関数は「filter」という名称の関数です。
まずは概要を。

表記(filter f coll)
引数f引数にcollの要素を受け取り、論理値を返却する関数オブジェクト。
coll入力シーケンス
返却値シーケンスcollの要素に関数fを適用してその返却値がtrueのものだけを抽出した、新しいシーケンス。

さっそく動きを確認しましょう。
サンプルデータはmap関数のエントリのときに作成したlanguagesを使用します。

(例1)1990年以降にリリースされた言語のみを抽出

まずは、1990年以降にリリースされた言語のみを抽出します。
sample.cljに次のコードを記述し保存してください。

; 関数名と年代のサンプルデータ
(def languages '(("FORTRAN"    1954)
                 ("Lisp"       1958)
                 ("C"          1972)
                 ("C++"        1983)
                 ("Perl"       1987)
                 ("Python"     1990)
                 ("Haskell"    1990)
                 ("Java"       1995)
                 ("JavaScript" 1995)
                 ("Ruby"       1995)
                 ("Scala"      2003)
                 ("Clojure"    2007)))

;;
;;(中略)※mapのときに記述したコードがいっぱい。。
;;
              
;1990年以降にリリースされた言語を抽出
(filter (fn [language]
          (let [year (first (rest language))]
            (<= 1990 year)))
        languages)

早速実行してみましょう。pprintを使用してください。

user=> (pprint (load-file "sample.clj"))
(("Python" 1990)
 ("Haskell" 1990)
 ("Java" 1995)
 ("JavaScript" 1995)
 ("Ruby" 1995)
 ("Scala" 2003)
 ("Clojure" 2007))
nil
user=> 

意図通り、1990年以降の言語が表示されました。


filter関数の第1引数に指定する関数オブジェクトは、次の条件を満たす必要があります。


・引数を一つ受け取ること。(本サンプルではlanguageとして「(言語名 年代)」を受け取る)
・true/falseの論理値を返却すること。(本サンプルではlanguageの「年代」を抽出して、1990以降ならtrueを、そうでなければfalseを返却している)
本サンプルではそれぞれ次のように実装されています。

・languageとして「(言語名 年代)」を受け取る。
・languageの「年代」を抽出して、1990以降ならtrueを、そうでなければfalseを返却している。

さて抽出がうまくできたところで、念のためにlanguagesの中身が変更されていないかどうか確認しましょう。

user=> (pprint languages)
(("FORTRAN" 1954)
 ("Lisp" 1958)
 ("C" 1972)
 ("C++" 1983)
 ("Perl" 1987)
 ("Python" 1990)
 ("Haskell" 1990)
 ("Java" 1995)
 ("JavaScript" 1995)
 ("Ruby" 1995)
 ("Scala" 2003)
 ("Clojure" 2007))
nil
user=> 

mapの時と同じで、元のデータに対して破壊的変更は行われていません。
filter関数も返却値は新しいシーケンスとして生成しています。


あ、あとこの例1の仕上げとして、
使用したフィルタリングするための無名関数を名前をつけて関数定義してしまいましょう。
後ほど使用するので。

;1990年以降の言語?
(defn is-later-1990
  [language]
  (let [year (first (rest language))]
    (<= 1990 year)))

これをsample.cljに追記しておきましょう。


(例2)言語名の先頭が"C"で始まる言語を抽出

他の条件でも試してみましょう。
言語名の先頭が"C"で始まる言語を抽出してみます。
この条件を記述するには、languageから言語名を抽出し、先頭の文字を比較する必要があります。
実装は次のようになります。

; 言語名が"C"で始まる言語を抽出
(filter (fn [language]
          (let [name (first language)]
            (= "C" (subs name 0 1))))
        languages)

無名関数の中の、letで言語名を束縛するところはまぁOKでしょう。
その束縛したnameに対し、先頭1文字を取得するため、「subs関数」を使用しています。
これは部分文字列を取得する関数で、
第1引数に元になる文字列データを、
第2引数に部分文字列の開始インデックス(0が先頭)を、
第3引数に文字列の長さを指定します。


上記サンプルコードではインデックス0から1文字分の部分文字列を抽出し、
それと"C"が一致するかどうかを比較しているわけです。


では早速実行してみましょう。

user=> (load-file "sample.clj")
(("C" 1972) ("C++" 1983) ("Clojure" 2007))
user=> 

Cで始まる言語の情報がシーケンスとして取得できました。

この例2も無名関数を名前をつけて関数定義しておきましょう。

;名称が"C"で始まる言語?
(defn start-with-C
  [language]
  (let [name (first language)]
    (= "C" (subs name 0 1))))

later-1990関数と同じく、sample.cljに追記しておきましょう。


my-filterを末尾再帰で実装

それではmap関数の時と同様に、filter関数を自前実装していきましょう。
末尾再帰の練習です。


というわけで、早速「末尾再帰のロジックを記述するコツ」、


1.引数に、入力用シーケンスと、最後に返却する為のシーケンスの為の変数を定義してしまう。
2.終了条件を決定し、if特殊系式を書いてしまう。
3.if特殊形式のthen式に、返却する為の変数を書いてしまう。
4.if特殊形式のelse式に、自分自身を呼び出すS式を記述してしまう。
5.4の自分自身を呼び出すS式の第1引数に、「(rest 入力用シーケンス引数)」を書いてしまう。
6.4の自分自身を呼び出すS式の第2引数に、返却するためのデータを合成する式を記述してみる。
に従って、ロジックの骨格を作ってしまいましょう。

(defn my-filter
  [input-seq return-seq]        ;★1
  (if (???終了条件???)          ;★2
      return-seq                ;★3
      (my-filter                ;★4
        (rest input-seq)        ;★5
        (???return-seqに入力シーケンスの要素を連結したりしなかったり???)))) ;★6


実コードの部分は、my-map関数の時とまったく変わっていませんが、
やはり★2と★6を育てれば完成します。


<★2>
終了条件です。入力シーケンスは「再帰呼び出しされる度に少なくなっていく」、
という方針はmy-mapの時と同様変わりません。
なので、やはり「=」関数で引数のinput-seqと空リスト「'()」を比較すればOKです。
同じ条件式を使用しましょう。

(= input-seq '())


<★6>
この部分は少し考えなければなりません。
今回はfilterですので、


・input-seqの要素が条件に合致する場合は、return-seqに追加して再帰呼び出し
・input-seqの要素が条件に合致しない場合は、何もreturn-seqに追加せずに再帰呼び出し
という感じに分岐させる必要が発生します。
分岐といえばif特殊形式です。
判別条件を「1990年以降に初回リリースされた言語」と固定して実際に実装してみましょう。

;ここから
(???return-seqに入力シーケンスの要素を連結したりしなかったり???);分岐を実装するとこんな感じになる。
(if (???1990年以降に初回リリースされた??? (first (rest (first input-seq))))
    (???return-seqに連結する???)
    (???return-seqに連結しない???));条件式を具体的に実装
(if (<= 1990 (first (rest (first input-seq)))) ;←ここをいじった
    (???return-seqに連結する???)
    (???return-seqに連結しない???));連結したりしなかったりも実装。まずはthen式から。
(if (<= 1990 (first (rest (first input-seq))))
    (cons (first input-seq) return-seq)        ;←ここをいじった
    (???return-seqに連結しない???));次はelse式。条件が偽になった時の話なので、
;return-seqには何もせずにそのまま次の再帰呼び出しに渡してしまう。
;これにより、この時判別された要素は最終的な結果から落とされることになる。
(if (<= 1990 (first (rest (first input-seq))))
    (cons (first input-seq) return-seq)
    return-seq)                                ;←ここをいじった;(first input-seq)が何度も出てきているので、
;このif式全体をletで囲み、ローカル束縛にlanguageを定義。
(let [language (first input-seq)]              ;←ここをいじった
  (if (<= 1990 (first (rest language)))
      (cons language return-seq)
      return-seq))

いかがでしょうか。だいぶ分かりやすくなったかと思います。


ではmy-filter関数内にドッキングさせてみましょう。

(defn my-filter
  [input-seq return-seq]
  (if (= input-seq '())                         ;←ここが★2
      return-seq
      (my-filter
        (rest input-seq)
        (let [language (first input-seq)];      ;←このlet内が★6だった箇所
          (if (<= 1990 (first (rest language)))
              (cons language return-seq)
              return-seq)))))

ではこの関数をsample.cljの末尾に定義して保存、load-file関数でリロードし、
動作するかどうか確認してみましょう。

user=> (load-file "sample.clj")
#'user/my-filter
user=> (my-filter languages '())
(("Clojure" 2007) ("Scala" 2003) ("Ruby" 1995) ("JavaScript" 1995) ("Java" 1995) ("Haskell" 1990) ("Python" 1990))
user=> 

例によって順番が逆になっていますので、最終的な返却値にreverse関数を適用しましょう。

(defn my-filter
  [input-seq return-seq]
  (if (= input-seq '())
      (reverse return-seq)  ;←ここをいじった。
      (my-filter
        (rest input-seq)
        (let [language (first input-seq)]
          (if (<= 1990 (first (rest language)))
              (cons language return-seq)
              return-seq)))))

一応確認。

user=> (load-file "sample.clj")
#'user/my-filter
user=> (my-filter languages '())
(("Python" 1990) ("Haskell" 1990) ("Java" 1995) ("JavaScript" 1995) ("Ruby" 1995) ("Scala" 2003) ("Clojure" 2007))
user=> 

正しい順序になりました。


my-map関数の時と同じように、ここから末尾再帰最適化をしていきます。
まずは再帰呼び出し箇所をrecur特殊形式にしましょう。

(defn my-filter
  [input-seq return-seq]
  (if (= input-seq '())
      (reverse return-seq)
      (recur                ;←ここを「my-filter」→「recur」に変更して末尾再帰最適化。
        (rest input-seq)
        (let [language (first input-seq)]
          (if (<= 1990 (first (rest language)))
              (cons language return-seq)
              return-seq)))))

my-filterの引数をloop特殊形式に任せ、自分の引数はloop特殊形式の初期化用の入力シーケンスのみにします。

(defn my-filter
  [init-input-seq]                 ;←引数はloop特殊形式の初期化用入力シーケンスのみに減らす。
  (loop [input-seq  init-input-seq ;←全体をloop特殊形式で包む。初期値は入力用シーケンスがinit-input-seq、
         return-seq '()]           ;←返却用シーケンスが空リストとなる。
    (if (= input-seq '())
        (reverse return-seq)
        (recur                
          (rest input-seq)
          (let [language (first input-seq)]
            (if (<= 1990 (first (rest language)))
                (cons language return-seq)
                return-seq))))))

まずはここまでで動作を確認しましょう。

user=> (load-file "sample.clj")
#'user/my-filter
user=> (my-filter languages)
(("Python" 1990) ("Haskell" 1990) ("Java" 1995) ("JavaScript" 1995) ("Ruby" 1995) ("Scala" 2003) ("Clojure" 2007))
user=> 

大丈夫そうです。


では最後に外部から抽出用関数を渡して任意のフィルタリングが可能な実装に修正します。
標準のfilter関数に習い、引数「f」を第1引数として挿入します。

(defn my-filter
  [f init-input-seq]                        ;←関数オブジェクト用の引数「f」を追加。
  (loop [input-seq  init-input-seq
         return-seq '()]
    (if (= input-seq '())
        (reverse return-seq)
        (recur                
          (rest input-seq)
          (let [language (first input-seq)]
            (if (f language)                ;←関数fを要素languageに適用して判定するのでこのような記述になる。
                (cons language return-seq)
                return-seq))))))

さあできました!
例1と例2で定義しておいた「is-later-1990関数」と「start-with-C関数」を使用して、
ちゃんと動くかどうか確かめてみましょう。

user=> (load-file "sample.clj")
#'user/my-filter
user=> (my-filter is-later-1990 languages)
(("Python" 1990) ("Haskell" 1990) ("Java" 1995) ("JavaScript" 1995) ("Ruby" 1995) ("Scala" 2003) ("Clojure" 2007))
user=> (my-filter start-with-C languages)
(("C" 1972) ("C++" 1983) ("Clojure" 2007))
user=> 

正しく意図通り動いていますね!
現時点でのsample.cljの内容をまとめておきます。(但しmapのときに記述したコードは省略します。)

; 関数名と年代のサンプルデータ
(def languages '(("FORTRAN"    1954)
                 ("Lisp"       1958)
                 ("C"          1972)
                 ("C++"        1983)
                 ("Perl"       1987)
                 ("Python"     1990)
                 ("Haskell"    1990)
                 ("Java"       1995)
                 ("JavaScript" 1995)
                 ("Ruby"       1995)
                 ("Scala"      2003)
                 ("Clojure"    2007)))

;;
;;(中略)※mapのときに記述したコードがいっぱい。。
;;
              
;1990年以降の言語?
(defn is-later-1990
  [language]
  (let [year (first (rest language))]
    (<= 1990 year)))

;名称が"C"で始まる言語?
(defn start-with-C
  [language]
  (let [name (first language)]
    (= "C" (subs name 0 1))))

;自前filter関数
(defn my-filter
  [f init-input-seq]
  (loop [input-seq  init-input-seq
         return-seq '()]
    (if (= input-seq '())
        (reverse return-seq)
        (recur                
          (rest input-seq)
          (let [language (first input-seq)]
            (if (f language)
                (cons language return-seq)
                return-seq))))))


いかがでしたでしょうか?
my-map関数を実装した時よりもスムーズに理解できましたか?