「ふつける」勉強会 資料用 このページをアンテナに追加 RSSフィード

 | 

2006-09-23

ふつける勉強会 第12回目 第十二章 「Wikiエンジンの開発」  ふつける勉強会 第12回目 第十二章 「Wikiエンジンの開発」 - 「ふつける」勉強会 資料用 を含むブックマーク はてなブックマーク -  ふつける勉強会 第12回目 第十二章 「Wikiエンジンの開発」 - 「ふつける」勉強会 資料用  ふつける勉強会 第12回目 第十二章 「Wikiエンジンの開発」 - 「ふつける」勉強会 資料用 のブックマークコメント

このレジュメでは、Wikiってなに?CGIってなに?とかそういう話はばっさり割愛。LazyLinesのソースの理解のみにフォーカス。全部やる根性はなかったので、だいたい本書に従って主要な部分だけを解説する。ソース理解は、import関係を把握して全体図を頭にいれてから、各関数の型を追っていけば理解するのはそんなに難しくないと思う。静的型付き万歳。

ソース一覧

http://www.loveruby.net/ja/stdhaskell/samples/ の 12、13章のところで参照可能。

全体の構造

LazyLinesのファイル間のimport関係を図示。矢印の根元のファイルが矢印の先のファイルをimportしている。

f:id:sshi:20060924054136p:image:w870

簡易CGIフレームワーク(CGI.hs)の上に、Wikiエンジン(LazyLines.hs)がのっかっている構成。

Wikiエンジンコントローラーリクエストを受けてがんばるタイプMVCモデルになっていて(p.294「設計」参照)、主にDatabaseがモデルとしてWikiページを管理し、TemplateがビューとしてHTML生成を受けもつ。コントローラーはLazyLines内に書いてある。SyntaxはWiki記法を解釈する部分で、次の章のトピック。残りのファイルは各種のユーティリティ

エントリポイント

http://www.loveruby.net/ja/stdhaskell/samples/lazylines/CGIMain.hs.html

CGIMainがプログラム全体のエントリポイントで、この中にmainがある。mainでは、Wikiエンジンの主要関数であるappMainに、設定であるContext情報を与えてから、CGIフレームワーク中のrunCGIを呼びだしている。

main = do ctx <- loadContext "./config"
          runCGI (appMain ctx)

簡易 CGIフレームワーク CGI.hs

http://www.loveruby.net/ja/stdhaskell/samples/lazylines/CGI.hs.html

CGIフレームワークには、HTTPResuestからIO HTTPResponseへの関数を渡せば、標準入出力や環境変数の処理を肩代わりしてくれる。runCGIが簡易CGIフレームワークの実体。

runCGIはこんなことをしている。

f:id:sshi:20060924063806p:image

主なデータ構造定義と関数を抜粋。

data HTTPRequest = HTTPRequest { params :: [(String, String)] }
data HTTPResponse = HTTPResponse {
    resContentType :: String,
    resBody :: String
}
instance Show HTTPResponse where
    show = httpResponseToString

runCGI :: (HTTPRequest -> IO HTTPResponse) -> IO ()
runCGI f = do hSetBinaryMode stdin True
              hSetBinaryMode stdout True
              input <- getContents
              env <- cgiEnvs
              res <- f (parseCGIRequest env input)
              putStr (show res)

Wikiエンジン コントローラー

http://www.loveruby.net/ja/stdhaskell/samples/lazylines/LazyLines.hs.html

runCGIに渡されるappMainが処理の実体。MVCそれぞれへの設定(データ保存するディレクトリテンプレートディレクトリ、このCGIを指すURLの情報)はContextに保存されている。

HTTPRequestをwikiRequestでさらにWiki用のリクエスト(WikiRequest)に変換してから、wikiSessionがコンテキストを受けて処理を行う。WikiRequestはView,Edit,Save,Recentの4種類。Response側にはWikiResponseというデータ構造はなくて、いきなりHTTPResponseを生成していることに注意。

data Context = Context Database TemplateRepository URLMapper

data WikiRequest = ViewRequest { name :: String }
                 | EditRequest { name :: String }
                 | SaveRequest { name :: String, content :: String }
                 | RecentRequest

appMain :: Context -> HTTPRequest -> IO HTTPResponse
appMain ctx = wikiSession ctx . wikiRequest

wikiRequest :: HTTPRequest -> WikiRequest

wikiSession :: Context -> WikiRequest -> IO HTTPResponse

wikiSessionはリクエスト(View,Edit,Save,Recent)をディスパッチする。いろんな処理が全てwikiSession内のwhere節につめこまれている。

  • respondTo ディスパッチの実体
    • view,edit,recentは、ほぼそれぞれの関数を呼ぶ。
    • saveは、savePageSource(Database.hs)を呼んでページを保存してから、softRedirectResponse.
  • viewPageResponse あるページをHTML表示
    • pageHTML
      • pageSouce(Database.hs)で名前をキーにページの内容(Wiki記法な文字列)を取得
      • compile(Syntax.hs)でHTML構造化
      • htmlToText(HTML.hs)でHTML構造データを文字列に変換
    • fill
      • fillTemplate(Template.hs)を使ってHTMLページ生成
  • editPageResponse あるページの編集画面表示
  • recentResponse 最近変更されたページ一覧表示
    • pageNamesWithMtime(Database.hs)を使ってmtime付きのページ一覧を取得
    • ごちゃごちゃ整形してから、recent用のテンプレートHTMLページ生成
  • softRedirectResponse ページ保存後のリダイレクトページを生成

haskellポイント

catch関数
あるアクションを実行中に例外があがった時の処理をつけたせる。
catch :: IO a -> (IOError -> IO a) -> IO a
-- こういうふうに使うと、respondeTO reqの実行がIOErrorになったときに、frontPageResponseが実行される。
catch (respondTo req) (\err -> frontPageResponse)
ReaderTモナド
wikiSessionの中ではwhereを使って複数の関数で値を共有しているが、こういうスタイルは好ましくなくて、ReaderTモナドを使うほうがよい、とのこと。

Wikiエンジン モデル

http://www.loveruby.net/ja/stdhaskell/samples/lazylines/Database.hs.html

論理的には名前とWiki記法な文字列の対応を管理。物理的には、1ディレクトリ内に、1ページを1ファイルで保存する。

データベース操作関数への引数には、Databaseという構造のデータが渡される。中身はファイルシステム上のパスファイルエンコーディング

data Database = Database { prefix :: String, encoding :: String }

主に使うのは読み込みと保存の二種類の操作。pageSourceとsavePageSource。

pageSourceは名前をファイルパスに変換してreadFileで読むだけ。

pageSource :: Database -> String -> IO String
pageSource db name = readFile (pagePath db name)
-- pagePathはconcatPath(PathUtils.hs)を使って名前からファイルパスを生成する。
pagePath db name = concatPath [prefix db, "pages", encodeName name]

savePageSourceは排他制御をやってるのでややこしい(p307~p308)

savePageSource :: Database -> String -> String -> IO ()
windows
ファイルを開くと自動的にwriteロックがかかるので、成功するまでwriteFileを繰り返すだけ
Linux
O_EXCLフラグ(排他アクセスフラグ?)付きで一時ファイルopen、出力を書きこんでおいてからrename

Database.hs中には、"#if WIN32 ... #elif POSIX ... #endif"を使ってでこの二つの処理が書かれている。

Wikiエンジン ビュー

http://www.loveruby.net/ja/stdhaskell/samples/lazylines/Template.hs.html

テンプレートライブラリWiki記法パーサからなる。Wiki記法パーサは次の章なので、ここではテンプレートライブラリの紹介。実体はTemplate.hs。

このテンプレートライブラリは、

の二つの機能しか持たない。

テンプレートの例(http://www.loveruby.net/ja/stdhaskell/ar/stdhaskell-samples.tar.gz で配布されているtemplate/view)

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=$encoding">
  <link rel="stylesheet" type="text/css" href="default.css">
  <title>$pageName</title>
</head>
<body>

.include menu

<h1>$pageName</h1>

$content

</body>
</html>

テンプレートの展開はfillTemplate関数で行う。テンプレートの場所、テンプレート名、変数名と値の組(alist)を与えると、結果のIO Stringが返ってくる。

fillTemplate :: TemplateRepository -> String -> [(String,String)]
             -> IO String

fillTemplate関数の中も二段階にわかれている。

  • まず テンプレート名に対応する文字列を取得する。(loadTemplate)
    • 同時に.includeの処理も行う(多重.includeもOK)
    • 最終的に$変数名が埋めこまれた文字列が取得される。
loadTemplate :: TemplateRepository -> String -> IO String
  • 次に$変数となっている部分を置換。(fill)
    • break,spanによる文字列分割(サーチ?)
      • breakで文字列の前から"$"のあるところを探す。breakは文字列を前からスキャンして「条件を満たさない部分」と「条件をはじめて満たした箇所とそれ以降」に分割する。
      • spanで$の後ろに続く変数名を切り出す。spanは文字列を前からスキャンして「条件を満たした部分」と 「はじめて条件を満たさなかった箇所とそれ以降」に分割する。
    • expandによる置換
      • lookupによるalistからの値を取得。fromMaybeを使って、対応する値がなければ"$変数名"に戻す。

補足

以降は、他のファイルの内容をざっくり補足

URLMapper

http://www.loveruby.net/ja/stdhaskell/samples/lazylines/URLMapper.hs.html

ページの名前からURLへの変換を行う。データ構造としては、基本となるURLとrewriteするかどうかのフラグとrewrite用suffixが格納されていて、pageURLというURL生成用の関数内でrewriteの値によって条件分岐して生成しわけている。閲覧用のURLを分離したい時に使う?

pageURL :: URLMapper -> String -> String
pageURL (URLMapper cgiurl rewrite suffix) name
    | rewrite   = urlencode name ++ suffix
    | otherwise = cgiurl ++ "?name=" ++ urlencode name

Config

http://www.loveruby.net/ja/stdhaskell/samples/lazylines/Config.hs.html

key = value のフォーマットconfigファイルから、[(key,value)]のデータを生成する。loadContextの中で利用されている、loadConfigはこんな定義。

loadConfig :: FilePath -> IO [(String, Config)]
loadConfig path = return . parseConfigFile =<< readFile path

FileUtils,PathUtils

http://www.loveruby.net/ja/stdhaskell/samples/lazylines/FileUtils.hs.html

http://www.loveruby.net/ja/stdhaskell/samples/lazylines/PathUtils.hs.html

ファイル操作をあつめたモジュールと,文字列からのパスへの文字列操作を集めたモジュール。FileUtilsのほうにはあるディレクトリファイル名列挙とかディレクトリ生成等の処理がある。PathUtilsのほうは、ディレクトリ名とファイル名からその環境に適切なファイルパスを生成したりする。

TextUtils

http://www.loveruby.net/ja/stdhaskell/samples/lazylines/TextUtils.hs.html

簡単な文字列の空白切りつめ関数(strip,lstrip,rstrip)と、空白文字列チェック(isBlank)が書いてある。

URLEncoding

http://www.loveruby.net/ja/stdhaskell/samples/lazylines/URLEncoding.hs.html

「GHC のバージョンによる escapeURIString 関数の違いを吸収」らしい。結局、urlencodeとurldecodeという関数が定義されている。

雑感

いろんな場所でファイルへのアクセスが発生するために、IOモナド汚染が一番上位まで伝搬している。(IO HTTPResponseになってるとこ)。haskell的にはIOモナドな部分だけを切り分けられると美しい気もするが、現実的なアプリケーション作るとこうなるのはしょうがないのかな。

 |