2012年5月18日金曜日

Yesod の Scaffold を使って Wiki らしきものを作ってみた

Haskell の文法とか理論的なところとかは少しわかったので、具体的なアプリケーションを書いてみようと思ったのだが、今どき具体的なアプリケーションと言ったらウェブアプリに決まってるので、Yesod を触ってみた。
初心者が言うのも何だが、Yesod book は内容が足りないと思う。書籍版では内容が追加されていることを期待したいが、Amazon.com の Look Inside を見る限り、ウェブと同じ内容のようだ。残念。

Yesod book の Examples は、1 アプリケーションが 1 ファイルに記述されていて、読みやすいのかもしれないけど、全く実用的ではない。あと、せっかく scaffold があるんだから、そのサンプルも欲しいところだ。

ということで、"scaffold を使って何か作る" という目的でとりあえず Wiki、というと Wiki ファンに怒られるようなものを作ってみた。どういう点で怒られるかというと、
  • markup なし
  • ページ一覧なし
  • 履歴管理なし
  • ユーザー管理なし
ということで、要するに
  • ページが作れる
  • 編集できる
だけ。今気がついたけど、ページ削除機能作るの忘れてた・・・。

早速 Yesod の scaffold を使ってみる。(Yesod は "cabal install yesod-platform" とか入力すればインストールできるし、インストール情報は結構あるので、ここでは省略する。)

cabal で yesod >= 1 を確かにインストールしておいたのに、最初に yesod init したときは、なぜか 0.8.x が使われてしまったので、yesod version でバージョンを確認しておくと良いだろう。

yesod init で聞かれることは、以下の 3 つ。(以前は Yesod instance にするデータタイプ名も聞かれたけど、無くなったようだ。)
  • 名前: ライセンス表記に使われる
  • プロジェクト名: cabal 名に使われる。全部小文字にして単語をハイフンで繋ぐのが Haskell 的(かな?)
  • データベース: 本格的なアプリを作るなら PostgreSQL を選ぶのだろうが、ここでは SQLite にする
最後に、なんだかよくわからない ASCII art とともに、次に入力すべきコマンドが表示される。

Twitter で「cabal-dev 使っとけ」的な発言を見かけたので、cabal-dev を使うことにする。cabal-dev install は時間がかかる。私の場合は 21 分かかった。かかりすぎ。このすきに珈琲をいれよう。しかし珈琲も冷めるだろう。
yesod --dev devel すると、アプリケーションが起動して http://localhost:3000/ でアクセスできるようになる。こんな感じ。

さて、あらためて hellowiki ディレクトリを見ると、こうなっている。ただし cabal-dev ディレクトリは省略した。
hostname:/tmp/hellowiki% tree
.
├── Application.hs
├── Foundation.hs
├── Handler
│   └── Home.hs
├── Import.hs
├── LICENSE
├── Model
├── Model.hs
├── Settings
│   ├── Development.hs
│   └── StaticFiles.hs
├── Settings.hs
├── config
│   ├── favicon.ico
│   ├── models
│   ├── robots.txt
│   ├── routes
│   ├── settings.yml
│   └── sqlite.yml
├── deploy
│   └── Procfile
├── devel.hs
├── hellowiki.cabal
├── main.hs
├── messages
│   └── en.msg
├── static
│   ├── css
│   │   └── bootstrap.css
│   └── js
├── templates
│   ├── default-layout-wrapper.hamlet
│   ├── default-layout.hamlet
│   ├── homepage.hamlet
│   ├── homepage.julius
│   ├── homepage.lucius
│   └── normalize.lucius
└── tests
├── HomeTest.hs
└── main.hs

ここからの開発方法はいろいろあると思うが、config/route を最初に変更して URL の設計をするのが良いだろう。
hostname:/tmp/hellowiki% cat config/routes
/static StaticR Static getStatic
/auth   AuthR   Auth   getAuth

/favicon.ico    FaviconR GET
/robots.txt     RobotsR GET

/               HomeR   GET POST
/new            NewR    GET POST
/wiki/#String   WikiR   GET POST
/wiki/#String/edit      EditR   GET
WikiR と EditR は 1 つのハンドラにしたいが、そういうことができるのか知らない。

yesod --dev devel を起動しっぱなしにしておくと、ファイルを書き換えるたびにコンパイルされる。Omake の polling mode みたいな感じ。config/route を編集してセーブした直後に、こんな感じでいろいろ怒られる。
Application.hs:27:1: Not in scope: `getNewR'

Application.hs:27:1: Not in scope: `postNewR'

Application.hs:27:1: Not in scope: `getWikiR'

Application.hs:27:1: Not in scope: `postWikiR'

Application.hs:27:1: Not in scope: `getEditR'
Build failure, pausing...
怒られたので、Handler ディレクトリに以下に、New.hs, Edit.hs, Wiki.hs を作り、そして Application.hs に import を追加する。例えば Handler/New.hs はこう書いた。
module Handler.New where

import Data.Text
import Import

import Model.WikiForm

getNewR :: Handler RepHtml
getNewR = do
  (formWidget, formEnctype) <- generateFormPost $ wikiForm Nothing
  defaultLayout $ do
    setTitle "Creating new wiki page"
    $(widgetFile "new")

postNewR :: Handler RepHtml
postNewR = do
  ((result, _), _) <- runFormPost $ wikiForm Nothing
  case result of
    FormSuccess (title, body) -> do
      runDB $ insert $ Wiki title $ unTextarea body
      redirect $ WikiR $ unpack title
    _ -> undefined -- エラー処理してない・・・
更にいろいろ怒られるはずなので、がんがん直していく。なお、"yesod --dev devel" はいかなる変更も完璧に拾ってくれるわけではないので、困ったときは "rm -rf dist" して "yesod --dev devel" を再起動するとよい。

試行錯誤を繰り返して、"yesod --dev devel" が文句を言わなくなったら、今作ったページにアクセスしてみよう。例えば http://localhost:3000/new とか。「Haskell ではコンパイルが通ればほぼバグは無い」と言われているのだから、これで動くはずだ。


動かない・・・。"yesod --dev devel" の出力を見ると、
Devel application launched: http://localhost:3000
GET /new
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Exit code: ExitFailure 11
となっている。

これが今回の最大の敵だった。"ExitFailure 11" では何もわからないし、Yesod book にも何も書いてない (*1)。困ったなと思って Twitter でつぶやいたら天の助けが。


これが正解で、hellowiki.cabal を編集すれば動くようになった。まぁアプリケーションが落ちる気持ちは分からなくはないが、ExitFailure 11 からそれは推測できない、絶対に。(*2)

このバグ(yesod のバグ?)さえなければ、あとはコンパイラのメッセージを見つつ、型合わせをしていけば動くようになる。

というわけで、Yesod の Scaffold を使ってみるという目的が達成できたので、Wiki まがいのアプリ自体には改良の余地が広大に残っているが、これで終わったことにする。コードはこちら。コメント・批判は大歓迎。
完成画面。なんと、"Pages"と"About"はどこにもリンクしていない・・・。

正直、Yesod の中身は未だにほとんど理解できていない。特にフォーム周りとか。Yesod book にも記述があるけど、これだけじゃねぇ・・・。しかし、全然理解していなくても、型を合わせていくと動いてしまうところが Haskell の凄い(怖い)ところ。ジグゾーパズルやってる感じ。が、それで良いはずはないので、ドキュメントの充実を望みたい。

個人的には、
  • テストの書き方
  • Shakespearn Templates の使い方。hamlet でコメントどう書くのか、とか。
を知りたい。あと、Hackage で、例えば getBy404 が出てこないのはそういうものなんだろうか?

ここまで書いたものを読み返してみると、文句ばっかり書いているようだが、Yesod は素晴らしいフレームワークだと思う。
Rails を最初に触ったときは、「フルスタックフレームワーク」というものに感動したものだが、Yesod はフルスタックフレームワークの手軽さに加えて、Haskell の type check を活かした堅牢性(というのか?)にさらに感動できる。
リンク切れを起こさない routing や、テンプレート内の変数が提供されること、XSS が起こらないことが、全て "type check" によって保証されている。Rails では、テストで確認するか、運を天に任せるしかできなかったことだ。(私の Rails の知識は 3 ~ 5 年前で止まってるので間違ってたらすみません。)

次は、実用的なアプリを作って見よう。

(*1)
と思ったら、ここに軽く書いてあった。

(*2)
よく見ると、"yesod --dev devel" が最初に警告していた。
WARNING: the following source files are not listed in exposed-modules or other-modules:
./Handler/Edit.hs
./Handler/New.hs
./Handler/Wiki.hs
./Model/WikiForm.hs
exposed-modules と other-modues との違いは分かってない。