こんにちは。まっつんこと松藤です。今回は実際にKahuaでユーザ登録アプリケーションを作ってみたいと思います。

MATSUFUJI Hideharu

まずははじめてのKahuaを参考にホームディレクトリに開発用の環境を作ります。ユーザ登録アプリケーションを作ることが目的なので、必要最低限の設定で環境を作っていくことにします。

最初にディレクトリを作成します。

cd
mkdir -p kahua/etc kahua/checkout kahua/tmp

次にKahuaの設定ファイルであるkahua.confを、作成したetcディレクトリにコピーします。

cp /usr/local/kahua/etc/kahua/kahua.conf kahua/etc

コピーしたkahua.confの内容を適切な値で置換します。

(make 
  :sockbase             "unix:/home/matsu/kahua/tmp"
  :working-directory    "/home/matsu/kahua"
  )

そして起動。

/usr/local/kahua/bin/kahua-spvr -c /home/matsu/kahua/etc/kahua.conf -H localhost:8888

インストールや開発環境の構築に関するドキュメントはかなりしっかり書かれているので、あまり迷うことはないと思います。余談ですが、Kahuaのドキュメントはくだけた感じで書かれているものが多いので、読んでいて面白いです。

さて、いよいよ本題のユーザ登録アプリケーションを作ってみます。まずはアプリケーションサーバregistrationを登録しましょう。

kahua/app-servers
(
;; ( :arguments ( ...) :run-by-default )
;; See http://www.kahua.org/cgi-bin/index.cgi/kahua-spvr
 (registration :arguments () :run-by-default 1)
)

これで環境が整いました。が、このあとどうすればいいか、さっぱりわかりません。とりあえずサンプルアプリケーションを参考にフォームを表示してみます。

kahua/checkout/registration/registration.kahua
(define (registration)
  (html/ (head/ (title/ "Registration"))
         (body/ (h1/ "User Name")
           (form/
             (p/ "First Name:") (input/ (@/ (type "text") (name "firstname")))
             (p/ "Last Name:")  (input/ (@/ (type "text") (name "lastname")))
             (p/) (input/ (@/ (type "submit") (value "confirmation")))))))

(initialize-main-proc registration)

KahuaではHTMLの要素までSchemeで表現するようになっています。これはプロダクトの設定ファイルや定義ファイルをYAMLによる外部DSLで表現するPiece Frameworkとは対照的です。(現在開発中のPiece Framework 2.0ではYAMLに代わって独自の言語が提供されます。)

何はともあれ、http://localhost:8888/registrationにアクセスするとフォームが表示されます。

form.png

続いて確認ページを表示してみましょう。

...
  (define (form)
    ...
             (form/cont/
               (@@/ (cont confirmation))
                 ...

  (define confirmation
    (entry-lambda (:keyword firstname lastname)
      (html/ (head/ (title/ "Registration"))
             (body/ (h1/ "User Name")
               (form/
                 (p/ "First Name:" firstname)
                 (p/ "Last Name:" lastname)
                 (p/) (input/ (@/ (type "submit") (value "reedit")))
                 (p/) (input/ (@/ (type "submit") (value "register"))))))))
  ...

前回、a/cont/を使用した例をお見せしましたが、今回はそれのフォーム版のform/cont/を使って継続を登録します。確認ページを表示するconfirmation関数を継続に指定します。これで無事確認ページが表示されます。

confirmation.png

次に完了ページを表示します。

...
  (define confirmation
    ...
               (form/cont/
                 (@@/ (cont finish))
                   ...

  (define (finish)
    (html/ (head/ (title/ "Registration"))
           (body/ (h1/ "User Name")
                  (h2/ "Registration Finished!"))))
  ...

基本的に今までと同じで完了ページを表示するfinish関数を定義し、確認ページから継続に指定して呼び出します。

finish.png

とりあえずバリデーションなしのページ遷移ならこれで完了のように見えるのですが、実は確認ページでreeditボタンとregisterボタンのどちらをクリックしても完了ページに遷移していまいます。サンプルを見てみたのですが、複数のボタンを持っているフォームがありませんでした。さて、どうしよう。

ここはオーソドックスにボタンに名前を付けて、その値によって遷移先を判定することにします。

...
  (define confirmation
    ...
                   (p/)(input/ (@/ (type "submit") (name "submit") (value "reedit")))
                   (p/)(input/ (@/ (type "submit") (name "submit") (value "register")))))))

  (define nextpage
    (entry-lambda (:keyword submit)
      (cond
        ((string=? submit "reedit") (form firstname lastname))
        ((string=? submit "register") (finish)))))
  ...

これでページが正しく遷移するようになりました。しかし、フォームに戻ったときに入力された値が表示されません。そこでform関数の定義をfirstnameとlastnameを引数として受け取るように変更します。

...
  (define (form firstname lastname)
    ...
                 (p/ "First Name:") (input/ (@/ (type "text") (name "firstname") (value (or firstname ""))))
                 (p/ "Last Name:")  (input/ (@/ (type "text") (name "lastname") (value (or lastname ""))))
                 ...
  (define nextpage
    (entry-lambda (:keyword submit)
      (cond
        ((string=? submit "reedit") (form firstname lastname))
        ((string=? submit "register") (finish)))))
  ...
  (form #f #f))
  ...

これでフォームに戻ったときにもFirst Name, Last Nameが表示される・・・と思ったのですが、表示されませんorz

調べてみるとFirst NameとLast Nameの値がフォームにないためnextpage関数にfirstname, lastnameに値が渡されないことがわかりました。一瞬、「hiddenで渡すか?」と考えたのですが、それではステートフルでなくなってしまうので却下。

仕方がないのでnextpage関数に通常の引数としてfirstnameとlastnameを渡すことにしました。

...
  (define (confirmation firstname lastname)
    ...
               (@@/ (cont (nextpage firstname lastname)))
               ...
  (define (nextpage firstname lastname)
    ...

この方法がKahuaの流儀に則ったものなのかは不明ですが、とりあえず動くのでこれでいきます。

最後にバリデーションを追加します。

...
  (define (form firstname lastname)
    ...
           (body/ (h1/ "User Name")
             (p/ (if (not (required-validator firstname)) "First Name is required" ""))
             (p/ (if (not (required-validator lastname)) "Last Name is required" ""))
             ...

  (define validate
    (entry-lambda (:keyword firstname lastname)
      (if (and (required-validator firstname) (required-validator lastname))
        (confirmation firstname lastname)
        (form firstname lastname))))
  ...
  (form #f #f))

(define (required-validator value)
  (cond
    ((equal? value #f) #t)
    ((string=? value "") #f)
    (else #t)))
...

バリデーションを行うためにvalidate関数とrequired-validator関数を定義しました。validate関数では入力項目のバリデーションを行い、その結果によって遷移先を決めます。実際の必須バリデーションはrequired-validator関数で行います。最終的なコードは以下になります。

(define (registration)
  (define (form firstname lastname)
    (html/ (head/ (title/ "Registration"))
           (body/ (h1/ "User Name")
             (p/ (if (not (required-validator firstname)) "First Name is required" ""))
             (p/ (if (not (required-validator lastname)) "Last Name is required" ""))
             (form/cont/
               (@@/ (cont validate))
                 (p/ "First Name:") (input/ (@/ (type "text") (name "firstname") (value (or firstname ""))))
                 (p/ "Last Name:")  (input/ (@/ (type "text") (name "lastname") (value (or lastname ""))))
                 (p/)(input/ (@/ (type "submit") (value "confirmation")))))))

  (define validate
    (entry-lambda (:keyword firstname lastname)
      (if (and (required-validator firstname) (required-validator lastname))
        (confirmation firstname lastname)
        (form firstname lastname))))

  (define (confirmation firstname lastname)
    (html/ (head/ (title/ "Registration"))
           (body/ (h1/ "User Name")
             (form/cont/
               (@@/ (cont (nextpage firstname lastname)))
                 (p/ "First Name:" firstname)
                 (p/ "Last Name:" lastname)
                 (p/)(input/ (@/ (type "submit") (name "submit") (value "reedit")))
                 (p/)(input/ (@/ (type "submit") (name "submit") (value "register")))))))

  (define (nextpage firstname lastname)
    (entry-lambda (:keyword submit)
      (cond
        ((string=? submit "reedit") (form firstname lastname))
        ((string=? submit "register") (finish)))))

  (define (finish)
    (html/ (head/ (title/ "Registration"))
           (body/ (h1/ "User Name")
                  (h2/ "Registration Finished!"))))

  (form #f #f))

(define (required-validator value)
  (cond
    ((equal? value #f) #t)
    ((string=? value "") #f)
    (else #t)))

(initialize-main-proc registration)

コードを見てみると関数の定義がPiece Frameworkのユーザ登録アプリケーションのRegistrationActionクラスのメソッドに似ているのがわかります。作ったのが私だからというのはあると思いますが、ステートフルが利用できるフレームワークで一関数(メソッド)、一機能という構造にしていくと必然的にこういった形に落ち着くのではないでしょうか。

どうにか年内に完成させることができました。今回ははじめての関数型プログラミング言語ということもあり、かなり苦労しました。とりあえず、みなさんにはプログラミング言語とフレームワークを同時に学ぶことはおすすめしませんw

それではみなさん良いお年を。

参考文献