Wikiエンジンの開発

12.3 CGIの処理


本日は"isSpace"関数を見ていきます。


それにしても、こうやってゆっくり見ていくのもいいものですね。
一つ一つ丁寧に見る気になります。
この本が読み終わったら、他のオープンソースのコードを見ていくのも面白いかも知れませんね。


閑話休題。"isSpace"関数。
再度定義を掲載します(Charモジュール)。

isSpace c                =  c `elem` " \t\n\r\f\v\xA0"
        -- Only Latin-1 spaces recognized


ちなみに"Latin-1"とはISO8859-1のことです。
文字コード表はこちらにまとまっています。
たとえばisSpaceの引数が'\xA0'(0xa0のこと)だった場合に手動で簡約化してみます。

isSpace 'xA0'
'xA0' `elem` " \t\n\r\f\v\xA0"
elem 'xA0' " \t\n\r\f\v\xA0"


"elem"関数の定義はこちら(Preludeモジュール)。

foldr            :: (a -> b -> b) -> b -> [a] -> b
foldr f z []     =  z
foldr f z (x:xs) =  f x (foldr f z xs)

and, or          :: [Bool] -> Bool
or               =  foldr (||) False

any, all         :: (a -> Bool) -> [a] -> Bool
any p            =  or . map p

elem, notElem    :: (Eq a) => a -> [a] -> Bool
elem x           =  any (== x)


先にも書きましたが、"isSpace"関数も最終的には"foldr"関数に行き着きます。
では簡約化の続きです。

elem 'xA0' " \t\n\r\f\v\xA0"
any (== 'xA0') " \t\n\r\f\v\xA0"
(or . map (== 'xA0')) " \t\n\r\f\v\xA0"
(foldr (||) False) . map (== 'xA0') " \t\n\r\f\v\xA0"


まずは関数合成の右側を簡約化します。

(foldr (||) False) . [False, False, False, False, False, True]


ここからは力業です。

(||) False (foldr (||) False [False, False, False, False, True])
(||) False ((||) False (foldr (||) False [False, False, False, True]))
(||) False ((||) False ((||) False (foldr (||) False [False, False, True])))
(||) False ((||) False ((||) False ((||) False (foldr (||) False [False, True]))))
(||) False ((||) False ((||) False ((||) False ((||) False (foldr (||) False [True])))))
(||) False ((||) False ((||) False ((||) False ((||) False ((||) True (foldr (||) False []))))))
(||) False ((||) False ((||) False ((||) False ((||) False ((||) True (False))))))


ここから戻っていきます。

(||) False ((||) False ((||) False ((||) False ((||) False ((||) True (False))))))
(||) False ((||) False ((||) False ((||) False ((||) False (True)))))
(||) False ((||) False ((||) False ((||) False (True))))
(||) False ((||) False ((||) False (True)))
(||) False ((||) False (True))
(||) False (True)
True


まとめると…

or :: [Bool] -> Bool
-- 引数の[Bool]型のリストの各要素全てに対してOrを取る関数。
-- つまり一つでもTrueがあればTrueとする。


any :: (a -> Bool) -> [a] -> Bool
-- 第一引数の関数を第二引数のリストの各要素全てに対して適用し、その結果をOrする関数。
-- つまり判定が一つでも一致すればOKとする。

elem :: a -> [a] -> Bool
-- 第一引数の値が第二引数のリストに含まれるかどうかを判定する関数。

isSpace :: Char -> Bool
-- 引数の値がスペースかどうか判定する関数。


ここまで追っていくと何が目的だったか忘れてしまいます。
この時点で(頭の)スタックから目的を取り出すと、

  • TextUtil.hs
isBlank :: String -> Bool
isBlank = all isSpace


の解析でした。
前回も書いた通り、引数の文字列がスペースのみの行かどうかを判定する関数です。


さらにスタックから目的を取り出します。
次に取り出せるのは

    parseLinePlain line
        | isBlank line = []
        | otherwise    = let (k, ('=':v)) = break (== '=') . strip $ line
                         in [(strip k, strip v)]


今まではこの中の"isBlank"関数を調べていたわけですね。
"parseLinePain"関数は引数の文字列がスペースのみの行だと空リストを返すわけです。
つまり空行はスキップするわけですね。


ようやく"parseLinePlain"の心臓部に入ります。
…と思ったら、ここには次の二つの関数がありますね。

  • break
  • strip


次はこの辺りから見ていこうかな。


…と思ったのですが、"break"関数は以前調べていますね。
第一引数の関数がTrueを返すところでリストを分離して、タプルにする関数です。
となると、"parseLinePain"関数の不明点は"strip"関数だけになりました。


"strip"関数は次の定義です。

  • TextUtil.hs
strip :: String -> String
strip  = rstrip . lstrip

lstrip :: String -> String
lstrip = dropWhile isSpace

rstrip :: String -> String
rstrip = reverse . lstrip . reverse


まずは予想をたてます。
おそらく文字列の中にあるスペース以外の文字列の前後にあるスペースを取り除く関数じゃないかと。
つまりこんな感じ。

strip "     abc def     "
"abc def"


これはあくまで予想です。
まぁ、こういうイメージがあると解析しやすいですから。
ではいつものように簡約化していきます。
引数は上の例と同じとします。

strip "     abc def     "
(rstrip . lstrip) "     abc def     "
((reverse . lstrip . reverse) . (dropWhile isSpace)) "     abc def     "
((reverse . (dropWhile isSpace) . reverse)) . (dropwWhile isSpace)) "     abc def     "


とりあえずここで止めます。
"dropWhile"関数の定義はこんな感じ(Preludeモジュール)。

dropWhile               :: (a -> Bool) -> [a] -> [a]
dropWhile p []          =  []
dropWhile p xs@(x:xs')
            | p x       =  dropWhile p xs'
            | otherwise =  xs


いやぁ、Haskell再帰が多いですね。もう脳トレ状態です(笑)。
繰り返し構文がないですからね。


でもとってもシンプルですね。
第二引数(a型)に対して判定関数((a -> Bool)型)を適用して、
Trueの間はどんどん捨てていって(drop)、
Falseになった時点で、それ以降のリストを返します。
まぁ、関数名が定義を物語っていますね。


では"strip"関数の続き。
まずは関数合成の一番右側を適用します。

dropwWhile isSpace "     abc def     "


もうそのままですね。
第二引数の文字列を先頭から見ていき、スペースじゃなくなった時点で終了。
その時点でのリストを返します。
つまり

"abc def     "


となります。つまり他の部分も合わせると

(reverse . (dropWhile isSpace) . reverse) "abc def     "


となります。上の関数合成の右側から展開してきます。
"reverse"関数は本書でも出てきましたが、リストを逆順にします。

(reverse . (dropWhile isSpace)) "     fed cba"


部分的用された"dropWhile isSpace"関数は前に出てきた通り、
先頭に現れるスペースを全て削除するものでした。
結果としてはこんな感じになります。

reverse "fed cba"
"abc def"


やっぱり予想の通りでした。
"strip"関数は前後のスペースを全部取り除く関数です。


これで"parseLinePlain"関数の全貌がつかめます。
もう一度定義を見てみます。

    parseLinePlain line
        | isBlank line = []
        | otherwise    = let (k, ('=':v)) = break (== '=') . strip $ line
                         in [(strip k, strip v)]


let式の中で"k"と"v"に束縛しています。
"k"は引数の文字列にある'='の左側、"v"が右側になります。
たとえば

"     HOST     =       com.example.helloworld   "


という文字列に"parseLinePlain"関数を適用すると、

k : "HOST     "
v : "       com.example.helloworld"


という感じで束縛されます。
でもこれだと無駄なスペースが取れきれていませんよね。
この最後のスペースを取り除くのがinの中です。

[(strip k, strip v)]
[("HOST", "com.example.helloworld")]


となります。
何もリストにしなくてもいいじゃん、と思うのですが
これは(多分)スペースのみの空行の場合、空リストを返すためだと思います。
この後、"concatMap"関数で、

[[(String, String)]]




[(String, String)]


となり、次の関数"categorize"に渡されます。
なお、この時点で空リストは削除されます。
つまり"categorize"関数の引数は"./config"の中の

ラベル1=1
ラベル2=2
ラベル3=3
...


のラベルと値のタプルのリストが渡るわけなのです。

[(ラベル1, 値1), (ラベル2, 値2), (ラベル3, 値3), ...]


というデータですね。


今日はここまで!
明日は"categorize"関数です。