PaperWorkerについてもう少し具体的にメモ。

もう少し具体的なPaperWorkerの構成についてメモしておきます。自分がブレないように。

PaperWorkerは「シンプルな事務処理のビジネスロジック」に特化したシステムです。
H2Databaseを組み込みのRDBとしてデータストアに使用していますが、多分JDBCであれば何でもOK(のはず)です。

システムは、複数の「job」とそれにぶら下がる複数の「action」から構成されます。
jobとactionは次のようなイメージです。

job一つの業務。各種マスタのメンテナンスやら、出退勤時刻入力、休暇申請、通勤変更届等々。
actionjobに対して実行可能な機能。全てプラグインとして提供される。ロールによって実行権限の制約を付与する(予定)。

実際に使えるjobとactionは、設定として登録することで提供されます。
これらの設定情報もRDBに登録されます。(ちなみに、jobとactionの登録も、プラグイン機能を使用しています。)

また、「標準action」をいくつか提供する予定で、これらは設定で対象となるデータ(実際にはBeanのクラスパス)を指定できるようになっています。
そのため、典型的な機能であれば新たなactionクラスを実装せずとも、新しい業務を追加することが可能です。
標準機能だけで実現できない複雑な機能が必要になった場合のみ、専用のactionクラスを実装すればOKです。

現在、「標準action」は次の12種を実装するつもりでいます。

基本機能新規追加新規文書を登録する。実装済
更新既存文書を変更する。実装済
削除既存文書を削除する。実装済
詳細表示1つの既存文書の内容を表示する。実装済
一覧表示複数の既存文書のサマリーを表示する。実装済
承認機能承認依頼文書の承認を指定した人物に依頼する。未実装
承認承認依頼された文書を承認する。未実装
却下承認依頼された文書を却下する。未実装
帳票機能印刷文書を指定されたフォーマットで印刷する。未実装
Export文書を指定されたフォーマットの電子ファイルに出力する。未実装
通知機能リマインダー文書に対し所定のアクションが実行された場合、関係者に通知する。未実装
コメント機能コメント既存文書に対してコメントを付与する。未実装


というわけで次から承認機能についてメモ。

今更ながら、リフレクションとアノテーションを使ったフレームワークを実装してみる。

毎度毎度、今まで作ってたのをほっぽりだして次から次へと新しいモノを作り始めて自分でも何だかなぁと思いますが、ちょっとclominalは壁にぶち当たって実装お休み中です。。
(検索機能の実装はできたんですが、置換機能について、RSyntaxTextAreaの中のコードを読み始め、リフレクションで機能を切り貼りして使いまわせないかと思ったのですが、clojure側からRSyntaxTextArea内のprivateメソッドを呼び出す為のリフレクションがいまいちうまく機能せず・・・とはいえ、今私の普段使いのエディタはclominalですw)

でclominalとは全く無関係に、とある切っ掛けで事務処理用のフレームワークを書き始めています。目的は「リフレクションとアノテーション使ってフレームワークを書いてみよう」です。自分の勉強の為という色合いが強いです。
その経過やらアイデアは別のところにメモ書きしてたのですが、こちらの方が書きやすくなったのでこっちにメモ書きすることにします。

今考えているのは、ビジネスロジック部分を全てプラグイン化できる構成のフレームワークです。
基本的な処理は「標準機能」として提供されますが、それらでカバーできないような複雑な業務は、新規で専用のプラグインを実装して提供することが可能になる予定です。

で、今まで私が少なからず仕事で関わってきた事務処理システムを振り返ると、次の機能があれば大抵の業務は表現できるのではないかと予想しています。

・新規追加
・更新
・詳細表示
・一覧表示(&検索含む)
・削除(一括削除含む)
・承認依頼、承認、却下
・リマインダー
・コメント
・帳票印刷

上から5つは既に実装済みで(細かいバグはありますけども)、
現在は承認絡みの機能について実装中です。

というわけで、今後しばらく、承認関係のプラグインの実装についてメモ書きしていきます。(多分内容は推敲しません。)

あ、ちなみに名前は「PaperWorker」にしました。

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関数を実装した時よりもスムーズに理解できましたか?

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

標準のmap関数の挙動

最初に実装する関数は「map」という名称の関数です。
(データ型のマップではありません。)

次のようなデータについて考えます。

; 関数名と年代
'(("FORTRAN"    1954)
  ("Lisp"       1958)
  ("C"          1972)
  ("C++"        1983)
  ("Perl"       1987)
  ("Python"     1990)
  ("Haskell"    1990)
  ("Java"       1995)
  ("JavaScript" 1995)
  ("Ruby"       1995)
  ("Scala"      2003)
  ("Clojure"    2007))

このデータから、言語名だけを抽出するコードを記述するとしましょう。
早速、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 (fn [language]
       (first language))
     languages)

このコードについて少し説明します。


languagesは、前述のデータを(後で何回も使いたいので)束縛した名称です。


次にmapの呼び出しですが、このコードの目的は「関数名だけを抽出する」なので、
第2引数に先ほどのlanguagesがセットされ、第1引数に変換用の無名関数がセットされています。
この無名関数の引数「language」にはシーケンスlanguagesの要素である、

(言語名 年代)

という構成のS式が渡されてきます。
我々が欲しいのは先頭要素の「言語名」だけですのでfirst関数を使い、

(first language)

とやると目当ての関数名が取得でき、無名関数としてはそれをそのまま返却すればOK、
ということになります。


一応、ダミーのデータを使って確認してみましょう。

user=> (first '("FORTRAN" 1954))
"FORTRAN"
user=> 

ばっちり言語名が取得できそうです。


では、上記のコードをsample.cljに保存し、
replを起動してload-file関数にsample.cljを指定して評価させてみてください。

user=> (load-file "sample.clj")
("FORTRAN" "Lisp" "C" "C++" "Perl" "Python" "Haskell" "Java" "JavaScript" "Ruby" "Scala" "Clojure")
user=> 


このように評価結果が返ってくればOKです。


改めて整理すると、


入力データ→「言語名」と「年代」を一対のペアとする要素のシーケンス
出力データ→入力のペアから「言語名」のみが抽出されたシーケンス
となっていることが分かると思います。


念のため、languagesの中身が変更されていないかどうか確認しましょう。

user=> 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))
user=> 

・・・横に長くてだいぶ見にくいですね。
こういう時は、pprint関数を使用しましょう。
もう少し見やすい形に整形して表示してくれます。

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は受け取ったデータに対して、
破壊的変更をしない関数であることがお分かり頂けるかと思います。


また、mapの第2引数である無名関数を工夫すると、言語名と年代を使った、
「文字列"XX is created in YYYY."を要素としたシーケンスを生成する」
なんてマネもできます。

先ほどの、関数名だけを抽出した無名関数を参考にして、
年代を抽出するにはどうすればよいかを、
ダミーデータ「'("FORTRAN" 1954)」を使って考えましょう。


年代はfirstでとれないのですからrestを使います。
replに入力して試してみましょう。

user=> (rest '("FORTRAN" 1954))
(1954)
user=> 

・・・アトムとしての数値だけが欲しいのですが、シーケンス(リスト)で返ってきています。
restはあくまでも「2番目の要素以降をシーケンスとして取得する」ので、
要素が一つしか残っていなくてもシーケンスとして返却されてしまうのです。
では、どうすればよいでしょうか。
ヒントは、


・「(1954)」もシーケンスである。
・1952という数値は、「(1954)」というシーケンスの先頭要素である。

ということです。
そうです、この評価結果のシーケンスに対して、
更にfirst関数を適用するというのが正しいです。

user=> (first (rest '("FORTRAN" 1954)))
1954
user=> 

期待した通り、ちゃんと年代情報がアトムとして取得できました。
これで必要な材料は揃いました。


あとは、「"XX is created in YYYY."」という文字列を生成できればOKです。
おあつらえ向きに「str」という関数があります。
この関数は、可変長の引数を受け取り、全ての引数の文字列表現を使用して、
それらを全て連結した文字列を生成して返却します。


language(languagesにあらず)、言語名の取得の仕方、年代の取得の仕方、
そしてstr関数を使うと、目的の処理が記述できます。

(fn [language]
  (str (first language) " is created in " (first (rest language)) "."))

これでOKなはずです。
んが、もう一つ贅沢を言えば、ちょっと横に長くて見づらいので、
let特殊系式を使用してすっきりさせましょう。

(fn [language]
  (let [name (first language)             ;言語名を取得してnameに束縛
        year (first (rest language))]     ;年代を取得してyearに束縛
    (str name " is created in " year ".") ;name、yearを使って合成した文字列を返却
    ))

いかがでしょうか、少し見やすくなったと思います。
手続き型言語に慣れた方にはそれでも見づらいと思いますがw)


というわけで、mapの第2引数になっている無名関数を次ように変更してみましょう。

; mapの評価
(map (fn [language]
       (let [name (first language)         
             year (first (rest language))]
         (str name " is created in " year ".")))
     languages)


・・・このままでも良いのですが、
この無名関数は後で自前map関数を実装した際にまた使用したいので、
無名ではなく、名前を付けた関数として独立させてしまいましょう。
こんな風に表記できます。

;メッセージ生成関数
(defn create-message
  [language]
  (let [name (first language)
        year (first (rest language))]
    (str name " is created in " year ".")))

; mapの評価
(map create-message languages) ;上記で定義した関数データを第1引数として渡す。

ついでに関数名だけ返す関数も作っておきましょう。

;関数名取得関数
(defn get-language-name
  [language]
  (first language))


最終的に、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)))
              
;メッセージ生成関数
(defn create-message
  [language]
  (let [name (first language)
        year (first (rest language))]
    (str name " is created in " year ".")))

;関数名取得関数
(defn get-language-name
  [language]
  (first language))

;とりあえずメッセージ生成関数を使用してmapを適用
(map create-message languages)

map関数を呼び出しているS式がかなりシンプルになりました。
sample.cljを保存したら、再度load-file関数を使ってリロードしてください。
あ、その際、pprint関数を使って見やすくしましょう。

user=> (pprint (load-file "sample.clj"))
("FORTRAN is created in 1954."
 "Lisp is created in 1958."
 "C is created in 1972."
 "C++ is created in 1983."
 "Perl is created in 1987."
 "Python is created in 1990."
 "Haskell is created in 1990."
 "Java is created in 1995."
 "JavaScript is created in 1995."
 "Ruby is created in 1995."
 "Scala is created in 2003."
 "Clojure is created in 2007.")
nil
user=> 


期待した結果が帰ってきました。
さて、改めてmap関数の動作を文章化してみるとこんな感じになります。


入力されたシーケンスを構成している要素を利用して新しい要素を合成し、
それらで構成されるやはり新しいシーケンスを返却する。


言語名を抽出する処理を末尾再帰で実装

さて、map関数の使い方を題材にしてclojureでのコーディングのプロセスに慣れてもらっていますが、
ここからはそのmap関数を自前で実装していくことを題材として、
末尾再帰の考え方に慣れていきます。


それではまず、言語名のみを抽出する具体的なケースについて、
関数呼び出し時にデータがどのようになってゆけばよいか考えてみます。

<0回目>
入力シーケンス:'(("FORTRAN" 1954) ("Lisp" 1958) ("C" 1972) ... ("Clojure" 2007))
  ↑の第一要素:'("FORTRAN" 1954)
返却シーケンス:'()


<1回目>
入力シーケンス:'(("Lisp" 1958) ("C" 1972) ... ("Clojure" 2007))
  ↑の第一要素:'("Lisp" 1958)
返却シーケンス:'("FORTRAN")


<2回目>
入力シーケンス:'(("C" 1972) ... ("Clojure" 2007))
  ↑の第一要素:'("C" 1972)
返却シーケンス:'("FORTRAN" "Lisp")

<3回目>
入力シーケンス:'(... ("Clojure" 2007))
  ↑の第一要素:...
返却シーケンス:'("FORTRAN" "Lisp" "C")

...


<11回目>
入力シーケンス:'(("Clojure" 2007))
  ↑の第一要素:'("Clojure" 2007)
返却シーケンス:'("FORTRAN" "Lisp" "C" ... "Scala")

<12回目>
入力シーケンス:'()
  ↑の第一要素:nil
返却シーケンス:'("FORTRAN" "Lisp" "C" ... "Scala" "Clojure")

<13回目?>
★入力シーケンスの要素がなくなったので終了。

なんとなくイメージが沸きますでしょうか。
上記のデータの変遷から、思いつくままにどんな式が必要か考えると、
こんな感じになります。


・入力シーケンスに毎回first関数を適用して要素を取得する式。
・取得した要素から言語名を取得する式。
再帰呼び出し時、入力シーケンスにrest関数を適用し残りの部分を渡す式。
再帰呼び出し時、返却シーケンスに言語名を連結する式。
・終了条件として、入力シーケンスの要素がなくなったことを判定する式。
・終了時に返却シーケンスを返す式。


で、いきなりですが、「末尾再帰のロジックを記述するコツ」は↓これです。


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

1のように実装すると、終了条件に合致した時に返却用データの引数を返すだけで済みますので非常に楽になります。
2では、全体のロジックの一番大きな骨格ができあがります。「肝その1」です。
3と4と5はもう機械的にやっちゃってください。
6が「肝その2」になります。ここは頭をひねる必要が出てくるでしょう。
材料(データ)や手段(関数)として何が必要になるかが見えてくれば、
もう出来たも同然です。


場合によってはifがletの入れ子になったり、
終了条件や返却値の合成が多少複雑になったために、
doやらletが必要になったりすることはありますが、
1〜5で、大体の骨組みはできてしまっています。


というわけで、分かるところからとりあえずコードを書いてみましょう。
分からないところは適当に「???」とかにしちゃってかまいません。
関数名は自前のmapなので「my-map」にしましょう。
わかりやすさの為、まずは自分自身を呼び出す再帰で実装してみます。
(後でloop/recur特殊形式に変更します。)

(defn my-map
  [input-seq return-seq]        ;★1
  (if (???終了条件???)          ;★2
      return-seq                ;★3
      (my-map                   ;★4
        (rest input-seq)        ;★5
        (???return-seqに連結??? (???言語名を取得???) ...)))) ;★6

「末尾再帰のロジックを記述するコツ」に従って、
ここまで一気に記述できました。
あとは★2と★6を埋めれば完成です。


<★2>
終了条件です。
前述のデータ遷移のセクションの「<12回目>」の入力シーケンスを見てみると、空リスト「'()」となっています。
これを判定するには、単純に「=」関数で比較すればOKです。

(= input-seq '())


<★6>
この行は、「(???言語名を取得???)」という式と、
その結果を使って「(???return-seqに連結??? ...)」を行う式の、
2つの式の入れ子になっています。


言語名を取得する式は、「input-seqの先頭要素」を取得後、
更に「その要素の先頭要素」が「言語名」になっていますので、このように記述できます。

(first (first input-seq))


では、return-seqに連結する式はどのように記述したら良いでしょうか。
とりあえずあまり悩まずにconsを使ってしまいましょう。
(順番は逆転してしまいますが、もう一度逆転させればよいですし、
そもそも順番を逆転させる関数を作るのは割と簡単ですので。)
このように記述できます。

(cons (first (first input-seq)) return-seq)


さあ、では「???」になっていた部分を埋めてしまいましょう。

(defn my-map
  [input-seq return-seq]
  (if (= input-seq '())         ;★2
      return-seq
      (my-map
        (rest input-seq)
        (cons (first (first input-seq)) return-seq)))) ;★6

このような定義になります。早速sample.cljに保存しましょう。
あ、それからもう多分使用することはないですので、
現在記述されているmap関数の呼び出しはコメントアウトしちゃいましょう。
load-file時に余計な出力があるのはうるさいので。

; 関数名と年代のサンプルデータ
(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)))
              
;メッセージ生成関数
(defn create-message
  [language]
  (let [name (first language)
        year (first (rest language))]
    (str name " is created in " year ".")))

;関数名取得関数
(defn get-language-name
  [language]
  (first language))

;;;とりあえずメッセージ生成関数を使用してmapを適用
;;(map create-message languages)

(defn my-map
  [input-seq return-seq]
  (if (= input-seq '())
      return-seq
      (my-map
        (rest input-seq)
        (cons (first (first input-seq)) return-seq))))

では、毎度お馴染みload-fileを評価してください。

user=> (load-file "sample.clj")
#'user/my-map
user=> 


早速my-map関数を評価してみましょう。最初に指定する引数は、
「<0回目>」の入力シーケンス、返却シーケンスを参考にしてください。

user=> (my-map languages '())
("Clojure" "Scala" "Ruby" "JavaScript" "Java" "Haskell" "Python" "Perl" "C++" "C" "Lisp" "FORTRAN")
user=> 

いかがでしょうか、上述のように結果出力されましたか?
見事に逆転してはいますが、言語名のみ抽出できています。


さて、このままでは末尾再帰最適化されていないので落ち着きません。
recurを呼び出す形に変更してしまいましょう。
なぁ〜んにも難しいことはありません、
再帰呼び出ししている箇所の「my-map」を「recur」に変更するだけです。

(defn my-map
  [input-seq return-seq]
  (if (= input-seq '())
      return-seq
      (recur              ;←★ここです。
        (rest input-seq)
        (cons (first (first input-seq)) return-seq))))

これだけで末尾再帰最適化の形にできます。
が、最初のmy-map関数の呼び出しが美しくありません。
使う側が返却用シーケンスの入れ物を用意するなんてのは使いにくいです。

そこでloop特殊形式の登場です。

my-map関数の引数を、「init-input-seq」という引数一つだけにしてしまいましょう。
もちろんmy-mapを最初に評価するときに指定するのはlanguagesになります。
そして、input-seqとreturn-seqを、letのようにローカル束縛できるのでその形にしてしまいます。
一段ネストが深くなります。

(defn my-map
  [init-input-seq]
  (loop [input-seq  ???初期化するための入力シーケンス???
         return-seq ???初期化するための入力シーケンス???]
    (if (= input-seq '())
        return-seq
        (recur
          (rest input-seq)
          (cons (first (first input-seq)) return-seq)))))

loopのS式の中に終了条件やら次の再帰呼び出しが含まれました。
これにより、recurの呼び出しはloopの呼び出し位置にジャンプすることになります。


ところで「???」で記述した箇所には何を入れればよいでしょうか。
これらはあくまもで初期値ですので、最初の呼び出し時の値にしかなりません。
故に、次のようになります。

(defn my-map
  [init-input-seq]
  (loop [input-seq  init-input-seq ;my-mapの入力シーケンス引数で初期化。
         return-seq '()]           ;空リストの入れ物で初期化
    (if (= input-seq '())
        return-seq
        (recur
          (rest input-seq)
          (cons (first (first input-seq)) return-seq)))))

loop/recur特殊形式を使用したおかげで余計な引数を外部から渡す必要がなくなったので、
my-map関数の呼び出しが自然な感じに変わりました。
例によってload-fileでリロードし、本関数を試してみてください。
動き自体に変化はなく、相変わらず逆転した関数名のみのシーケンスが返却されると思います。

user=> (my-map languages)
("Clojure" "Scala" "Ruby" "JavaScript" "Java" "Haskell" "Python" "Perl" "C++" "C" "Lisp" "FORTRAN")
user=> 


ここまで来ましたが、recurの引数が分かりにくいのが気に入りません。
recur呼び出しの前に、letを使って意図の分かるコードに変更しましょう。

(defn my-map
  [init-input-seq]
  (loop [input-seq  init-input-seq
         return-seq '()]
    (if (= input-seq '())
        return-seq
        (let [element           (first input-seq)                    ;入力シーケンスの先頭要素
              converted-element (first element)                      ;言語名
              next-input-seq    (rest input-seq)                     ;次の再帰呼び出しの為の入力シーケンス
              next-return-seq   (cons converted-element return-seq)] ;次の再帰呼び出しの為の返却用シーケンス。変換・合成された新しい要素(言語名)を連結する。
              
          (recur
            next-input-seq
            next-return-seq)))))


如何でしょうか。
letのローカル束縛により、recurが何をしようとしているのかがわかりやすくなったと思いませんか?
ちなみにletはローカル束縛後に複数のS式を記述できますが、
最後に記述されたS式の返却値がletの返却値になります。
故にこのコードの場合、recurが最後に評価されるので末尾再帰最適化の条件に合致することになります。
(recurは特殊形式ですが、特殊形式ならばどんなものでも引数が最後に評価される、というわけではありません。
そもそもrecurは末尾再帰最適化の為の特殊形式ですし。)


ここまで来ると、あとは任意の要素変換用関数を外部から引数で渡してくれば、
my-mapが完成するイメージが沸いてくると思います。


渡してきた変換用関数はどこに記述すれば良いでしょうか。
languageの行、firstを、任意の変換関数で置き換えてやればOKとなります。

(defn my-map
  [func init-input-seq] ;任意の変換用関数を引数で渡す。
  (loop [input-seq  init-input-seq
         return-seq '()]
    (if (= input-seq '())
        return-seq
        (let [element           (first input-seq)
              converted-element (func element)    ;←ここで引数として渡されてきた変換・合成用関数をelementに適用するように変更。
              next-input-seq    (rest input-seq)
              next-return-seq   (cons converted-element return-seq)]
          (recur
            next-input-seq
            next-return-seq)))))


sample.cljに記述・保存して、load-fileでリロードしてください。

user=> (load-file "sample.clj")
#'user/my-map
user=> 

読み込みは問題なさそうです。
では以前作成したget-language-name関数、create-message関数を使用して動作を確認してみましょう。

user=> (my-map get-language-name languages)
("Clojure" "Scala" "Ruby" "JavaScript" "Java" "Haskell" "Python" "Perl" "C++" "C" "Lisp" "FORTRAN")
user=> (pprint (my-map create-message languages))
("Clojure is created in 2007."
 "Scala is created in 2003."
 "Ruby is created in 1995."
 "JavaScript is created in 1995."
 "Java is created in 1995."
 "Haskell is created in 1990."
 "Python is created in 1990."
 "Perl is created in 1987."
 "C++ is created in 1983."
 "C is created in 1972."
 "Lisp is created in 1958."
 "FORTRAN is created in 1954.")
nil
user=> 


大丈夫そうですね。
最後に順序を逆にする関数についてですが、私ちょっと疲れたのでズルして標準のreverse関数を使用しちゃいます。
ifのthen式で返却しているreturn-seqにreverse関数を適用してください。
それでいけちゃいます。

(defn my-map
  [func init-input-seq] ;任意の変換用関数を引数で渡す。
  (loop [input-seq  init-input-seq
         return-seq '()]
    (if (= input-seq '())
        (reverse return-seq)
        (let [element           (first input-seq)
              converted-element (func element)
              next-input-seq    (rest input-seq)
              next-return-seq   (cons converted-element return-seq)]
          (recur
            next-input-seq
            next-return-seq)))))

一応確認。。

user=> (my-map get-language-name languages)
("FORTRAN" "Lisp" "C" "C++" "Perl" "Python" "Haskell" "Java" "JavaScript" "Ruby" "Scala" "Clojure")
user=> 


よっしゃ。




如何でしたでしょうか。
手続き型の人が、段階を追ってLisp的コーディングを理解できるようにしたため、
アホみたいな長さになってしまいました。誰も最後まで読まねぇだろうなぁコレw


ま、Lispの「基礎的なコード」というのは、


・first、rest、consを駆使したデータの分解・合成
・末尾再帰
・関数オブジェクト(クロージャ
の3つじゃないかと思います。
これを押さえて、次のステップであるより便利なシーケンス操作関数(リスト操作関数)を知っていくと、
「繰り返し構文て要らなくね?」という境地に立てる・・・かも?


この後のエントリでは、filter関数、reduce関数、sort関数の使い方を紹介します。
いずれもかなり頻繁に使用するシーケンス関数です。
これらの関数の内部では同じような(いや実際にはもっと効率的になっているんじゃないかなぁと思いますが)手法が使われているとイメージしつつ、
シーケンス関数群の使い方を一段深いレイヤーで理解する助けになればと思います。



あー疲れた。。

clojure備忘録[replを使った開発手法 その1(シーケンスを扱う関数)]

では早速、replとsample.cljを使って、
シーケンスを扱う関数を作って行きましょう。


と思ったのですが、「シーケンス」の説明が必要であることに気づいたので、
このエントリはこれについて先に説明しておきたいと思います。
clojureとは切っても切り離せない関係にあるので。


というわけで、「シーケンス」という単語が突然でてきましたが、
これは何かというと今まで散々出てきた「リスト」のようなものです。
リストは「データの実体」を伴っている連続データであるのに対し、
「シーケンス」はより抽象化された連続データになります。
javaの用語を使うのであれば、
「シーケンスというインタフェースを実装したオブジェクト」
という表現になります。
故に、これを実装している連続データは全てシーケンスとして扱えます。


シーケンスとして扱える具体的なデータとしては、


・リスト
・ベクタ
・マップ(後々説明します)
・セット(後々説明します)
・IO(ファイル入出力等を遅延したシーケンスとして)

等が挙げられます。多分まだあると思いますが。
これらについてはデータリテラルのまとめとして、
また別途エントリを記載する予定です。


さて、なぜ「シーケンスを扱う関数を自前で実装していくか」というと、
理由は3つあります。


・末尾再帰プログラムの練習にぴったり。
Lispではリスト(シーケンス)を扱う関数は非常に良く使用されるので、
この機会にこれらの関数の動きを覚えてしまう。
・「手続き型言語的な考え方」から「Lisp的な考え方」へと脱皮する。

シーケンス関数を自前実装するだけで、これらを網羅できます。
・・・と思ってます個人的に。もちろん保証はしませんw



ここに至るまでだいぶ長かったですが、
今度こそ自前関数の実装に入っていきましょう!

clojure備忘録[replを使った開発手法 その0(ソースファイルの扱い方)]

これまでのエントリで、clojureを書いていく際に必要な基本的情報は説明したつもりです。
が、実際にどのように開発をしていくのかはまだあまりイメージが沸いていないと思います。


以前のエントリではしばしば小さなコード断片(S式)を
replのプロンプトに直接入力して評価してきましたが、
実際の開発では、毎回いちいちプロンプトから入力なんてやってられません。
当然のことながら、ファイルにコードを記述し、
繰り替えし評価できるようにするのが普通です。


今回は最も初歩的なファイルのロードの仕方を紹介します。

★注意事項★

いきなりですが、一つ注意事項があります!(今後増えるかも知れませんが。)
windowsで試している人は、clojureコードの編集に「メモ帳」を使用しないようにしてください。


理由は、clojureソースコードが処理系に読み込まれると、
読み込まれたそばからコンパイルされます。
(特にclassファイルとして出力はされませんけれども。)


故に、clojureのソースファイルはjavaのソースファイルの書き方に習い、
エンコードUTF-8」、「BOMコードは無し」にします。
ところがメモ帳はUTF-8で保存するとファイルの先頭に勝手にBOMコードが挿入されてしまいます。


当然ながらJVMはこのBOMコードに対応していない為、
コンパイルが通らずまったく評価ができません。
なので、clojureコードを記述する際には、


[必須1]エンコードUTF-8を指定できること。
[必須2]その時、BOMを付与せずに保存できること。
[おまけ]括弧の対比が見やすくなる設定がついていること。

の必須条件2つを満たすテキストエディタを使用するようにしてください。
メモ帳は[必須2]の機能が無いため、失格になります。
windowsで試されている方は、サクラエディタ秀丸あたりなら大丈夫だと思います。
unix系の方は多分こだわりのエディタを使用しているでしょう。

sample.cljを作る

では、これからしばらくの間お付き合いする、
clojureのコードを試す為のソースファイルを作成しましょう。


端末エミュレータDOS窓を開き、CLOMINAL_HOMEディレクトリに移動します。
CLOMINAL_HOME直下に、「sample.clj」という名称でファイルを作成してください。
このようになります。


CLOMINAL_HOME
├ lib/
│ ├clojure-1.4.0.jar
│ └clojure-contrib-1.2.0.jar
├ src/
├ repl.sh(またはrepl.bat)
└ sample.clj


次に、今作成したsample.cljをテキストエディタで開き、
1行、次のように記述して保存してください。

(println "Hello, this is 'sample.clj' file.")

何度か出てきていますが、printlnは引数を全て文字列として標準出力に出力する関数です。(末尾に改行が入ります。)
さ、準備が整いました。


load-file関数

replに対し、今コーディングしたsample.cljをロードします。
そのためには「load-file関数」を使用します。

CLOMINAL_HOME直下にいる状態からreplを起動し、
次のS式を評価させてみてください。

(load-file "sample.clj")

次のように表示されれば、sample.cljがロードされ、
評価結果が出力された事になります。

user=> (load-file "sample.clj")
Hello, this is 'sample.clj' file.
nil
user=> 

意図した通り文字列が出力されました。
文字列が出力された後に表示された「nil」は、
println関数の返却値ですので混同しないように注意してください。


では、今度はsample.cljの内容を次のように修正し、保存してください。

(def hello (fn []
             (println "Hello, this is 'sample.clj' file.")))

もう一度replに戻り、先ほどのload-fileのS式を再度評価させてみてください。

user=> (load-file "sample.clj")
#'user/hello
user=> 

今度は文字列が出力されません。


今、sample.cljに書いたコードは、


修正前と同じ文字列を出力する「関数オブジェクト」を、「hello」という変数に束縛する。

というコードです。
評価内容はあくまでも「関数オブジェクトを変数に束縛した」だけなので、
文字列が出力されないのは当然です。
今、sample.cljを評価したことにより、
replのプロセスそのものにhelloが生成されたことになっています。
故に、helloを関数のように呼び出して評価させることができる状態になっています。

replのプロンプトに次のS式を入力して評価させてみてください。

(hello)


変数helloに束縛した関数オブジェクトが評価され、
修正前と同じ文字列が出力されます。

user=> (hello)
Hello, this is 'sample.clj' file.
nil
user=> 


更にいじってみましょう。
今度は呼び出し時に引数として名前を指定すると、
その名前を組み込んだ文字列を出力する関数オブジェクトに修正してみます。
sample.cljを次のように修正し、保存してください。

(def hello (fn [name]
             (println "Hello" name ", this is 'sample.clj' file.")))

例によってload-fileでsample.cljをリロードさせます。

user=> (load-file "sample.clj")
#'user/hello
user=> 

これで以前の引数なしの関数オブジェクトの束縛は、
今新しく定義した引数ありの関数オブジェクトの束縛で上書きされました。
変数helloに新しく束縛された関数オブジェクトを、
引数を指定して評価してみましょう。

user=> (hello "AWACIO")
Hello AWACIO , this is 'sample.clj' file.
nil
user=> 

"Hello" の後ろに指定した名前が挿入されて出力されました。
意図した通りに動作しています。


このように、ソースファイルを使用して開発を行う場合(まー普通いつもこうなると思いますけども)、


「コーディング」
→「ファイルのリロード」
→「動作確認(デバッグ)」
→「コーディング」
→・・・

というサイクルをreplを使いつつ繰り返すことになります。
最近のスクリプト言語perlpythonrubyあたり?)では、
このスタイルでの開発が一般的なんじゃないかなと勝手に思っていますが、
どうなんでしょうかね。
他方で、C、C++C#java当たりの、コンパイルが必要な言語に親しい方にとっては、
結構面食らうスタイルなんじゃないかなと思います。(自分がそうだったので。)



さ、この開発スタイルに慣れる為にも、
また、Lisp的な(あるいは関数型言語的な)考え方に慣れる為にも、
いくつかの非っ常〜によく使用する関数を題材にし、
自前で実装してみましょう。


思いっきり車輪の再発明ですが、
車輪の再発明は「勉強」として効果があることも事実ですので、
どうかお付き合いくださいませ。

clojure備忘録[clojureの基本的な関数 その4(=関数)]

atomは存在が確認できなかったのでclojureの基本関数としてはこれが最後です。
値が等しいか等しくないかを返却する関数です。

表記

(= form0 form1)

引数を2つとり、両者の「S式の構成も含めて」、「値」が同じであれば論理値のtrueを、
同じでなければfalseを返却します。
上記の説明は実例を見た方が早いでしょう。
論理値の表記はtrueとfalseです。(後のエントリで正式に掲載します。)

実例

まずは単純なアトムから。

user=> (= 1 1)
true
user=> (= 1 2)
false
user=> 

ま、問題無いでしょう。次も問題ないと思います。
(「=」は関数なので、引数のS式を計算した結果を扱うことが分かれば。)

user=> (= (+ 3 4) (- 10 3))
true
user=> (= (+ 3 4) (- 10 2))
false
user=> 


次はリストを比較してみます。

user=> (= '(a b (c d)) '(a b (c d)))
true
user=> (= '(a b (c d)) '(a b (C d)))
false
user=> (= '(a b (c d)) '(a (b c d)))
false
user=> 

1番目は、第1引数も第2引数もまったく同じ構成、値になっているので真が返却されます。
2番目は、構成は同じですが、第1引数が小文字の「c」で、
第2引数が大文字の「C」になっているため、偽が返却されます。
3番目は、構成している要素は全て小文字の「a」〜「d」なのですが、
構成が異なる為、やはり偽が返却されます。