|
|
||
このレジュメでは、Wikiってなに?CGIってなに?とかそういう話はばっさり割愛。LazyLinesのソースの理解のみにフォーカス。全部やる根性はなかったので、だいたい本書に従って主要な部分だけを解説する。ソース理解は、import関係を把握して全体図を頭にいれてから、各関数の型を追っていけば理解するのはそんなに難しくないと思う。静的型付き万歳。
http://www.loveruby.net/ja/stdhaskell/samples/ の 12、13章のところで参照可能。
LazyLinesのファイル間のimport関係を図示。矢印の根元のファイルが矢印の先のファイルをimportしている。
簡易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)
http://www.loveruby.net/ja/stdhaskell/samples/lazylines/CGI.hs.html
CGIフレームワークには、HTTPResuestからIO HTTPResponseへの関数を渡せば、標準入出力や環境変数の処理を肩代わりしてくれる。runCGIが簡易CGIフレームワークの実体。
runCGIはこんなことをしている。
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)
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節につめこまれている。
catch :: IO a -> (IOError -> IO a) -> IO a -- こういうふうに使うと、respondeTO reqの実行がIOErrorになったときに、frontPageResponseが実行される。 catch (respondTo req) (\err -> frontPageResponse)
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 ()
Database.hs中には、"#if WIN32 ... #elif POSIX ... #endif"を使ってでこの二つの処理が書かれている。
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 :: TemplateRepository -> String -> IO String
以降は、他のファイルの内容をざっくり補足
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
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
http://www.loveruby.net/ja/stdhaskell/samples/lazylines/FileUtils.hs.html
http://www.loveruby.net/ja/stdhaskell/samples/lazylines/PathUtils.hs.html
ファイル操作をあつめたモジュールと,文字列からのパスへの文字列操作を集めたモジュール。FileUtilsのほうにはあるディレクトリのファイル名列挙とかディレクトリ生成等の処理がある。PathUtilsのほうは、ディレクトリ名とファイル名からその環境に適切なファイルパスを生成したりする。
http://www.loveruby.net/ja/stdhaskell/samples/lazylines/TextUtils.hs.html
簡単な文字列の空白切りつめ関数(strip,lstrip,rstrip)と、空白文字列チェック(isBlank)が書いてある。
http://www.loveruby.net/ja/stdhaskell/samples/lazylines/URLEncoding.hs.html
「GHC のバージョンによる escapeURIString 関数の違いを吸収」らしい。結局、urlencodeとurldecodeという関数が定義されている。
いろんな場所でファイルへのアクセスが発生するために、IOモナド汚染が一番上位まで伝搬している。(IO HTTPResponseになってるとこ)。haskell的にはIOモナドな部分だけを切り分けられると美しい気もするが、現実的なアプリケーション作るとこうなるのはしょうがないのかな。