|
|
||
ふつうのHaskellプログラミング ふつうのプログラマのための関数型言語入門
このレジュメでは、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モナドな部分だけを切り分けられると美しい気もするが、現実的なアプリケーション作るとこうなるのはしょうがないのかな。
第二部も折り返し。ここから急にhaskell密度が濃くなっている感触。正直なとこ、この章の内容をちゃんとやろうとしたら一章分どころか二章あっても足りないと思う。今回のレジュメは後半息切れしています。
さて、この章では型と型クラスの話が登場。特に、ユーザーが型を定義できるdata宣言と、型クラスにまつわるclass,instance,deriving宣言が紹介される。
この話は前にもでてきたし、ここまで読んでくれば特にひっかかるところもないと思う。関数だけでなく式に型宣言がつけられるのはここで初めてでてきた?p.222の例、
luckyNumber = (7::Int) unluckyNumber = (13::Integer)
みたいに書けます。あと強いていうなら、haskellでは型の内部に変数を持つような「多相型」が使えますよ、というのがポイントか。あとは「静的に型推論します」という話が書いてある。
haskellで新しい型を定義する方法の紹介。
haskellでは代数的データ型を使って複合した型をユーザーが定義できる。「代数的データ型」というのはhaskellに限らない型理論界隈の用語で、haskellではdata宣言でそれが使える。
何が「代数的」かというと、型の和と積の組みあわせで複合的な型を構成していくところ。定義したデータ型は、型Aまたは型Bですよ、というのが型の和で、定義したデータ型は、型Aと型Bのフィールドを持ちますよ、というのが型の積。ORとANDというほうがしっくりくる?。(無理矢理)Cでいうと、型の和がenumで、型の積がstruct、あわせるとunionになる。多分、ML系言語なら同じような型定義ができたと思う。
本書では、構造体型、列挙型、そしてそれを組みあわせた共用体型がdata宣言の一般形です、というふうに順を追った説明になっているが、要はdata宣言を分解するとこれらの側面をもってますよ、ということ。
-- フィールドラベル付きの宣言でも data Anchor = Anchor {aUrl::String,aLabel::String} deriving Show -- ラベルなしでも値が生成可能。順番に注意。 *Main> Anchor "a" "b" Anchor {aUrl = "a", aLabel = "b"}
最後(p234)に、一般的なdata宣言がでてくる。今までの組みあわせ。一般的なdata宣言の構造を理解する上では、「和」の要素には、必ずデータコンストラクタがひとつだけ必要というのを意識するのがポイント。あと、型コンストラクタとデータコンストラクタは全く別物であることを意識しないと非常にわかりにくいので注意。
困ったことに、型コンストラクタとデータコンストラクタはdata宣言の両辺で似たような位置に置かれる上、同じ名前が使われる*1場合もある。理解が不十分だと、そういう例をみたときに混乱することうけあい。
ややこしい例としてはこんなの。
data Point a = Point a a
左辺のPointは「型」コンストラクタであり、例えば"Point Int"で「型」になる。右辺のPointは「データ」コンストラクタであり、例えば"Point 5 3"で「データ」になる。この二つのPointはたまたま同じ名前にしているだけで、意味するところは大きく違う。
haskellにはユーザーが型を定義する宣言はdata宣言の他にあと二つある。type宣言とnewtype宣言。
まず、data宣言というのは(見た目だけでなく)実行時に(内部的に)新たなデータ型を導入するものである、というのが大前提。要はデータ型を導入したことによるオーバヘッドがある。C++のクラス定義みたいなもの。一方、残りの二つは実行時には新たなデータ型は導入されない。表面的には新しいデータ型が導入されているようにみえるだけ。Cでいえばマクロ。
というのを踏まえて…
型に別名をつけるのがtype。いわゆるtypedef。タプルを使えば構造体型のdata宣言は模擬できそうだが、列挙型のほうは無理。当然ながらデータコンストラクタがないので、データコンストラクタを使ったパターンマッチはできない。
newtypeはdata宣言に「列挙型になってなくて、かつフィールドも一個」という制約をつけたもの。data宣言で導入したデータ型と同じように扱える。ただし、実行時には新しい型が導入されたとは見做されないので軽い…(らしい)。
newtypeの使いどころは良くわからない。
型クラスとは、「型」の「クラス」であって「型」とは別物。複数の型を表現したいが、任意の型では困る時に使う。型変数への制約として記述される。例えば、同値性の判断は任意の型で行えるわけではない。同値性判断可能な型は、Eqの型クラスに属している。
*Main> :t (==) (==) :: forall a. (Eq a) => a -> a -> Bool -- (Eq a) => の部分が型クラスによる制約
型クラスの実体は、Javaでいうところのインターフェイス。その型クラスのインスタンスである型が実装すべき関数(これをクラスメソッドと呼ぶ)の型の集合で定義される。実装は必須でないが、デフォルト実装も定義可能。
ある型クラスにある型を属させる場合には、クラスメソッドの実装が必要。デフォルト定義されていたら省略可能。型ごとに実装を書きわけられるので多重定義が実現できる。
継承もできます。
型クラスを定義するには、class宣言を使う。(p241,p242参照)
ある型をある型クラスのインスタンスであると定義する宣言。(p242参照)
デフォルト実装があるといっても全て省略できるわけでないことに注意。Eqの(==)と(/=)のように、片方をインスタンスで実装することを期待した循環したデフォルト実装もある。
データ型を定義したときに、deriving宣言と型クラスをつけることによって、ある型クラスに属するというinstance宣言を省略できる。ただし、万能ではなく、使える型クラスに制限があり、クラスメソッド実装が自明な場合のみ。"deriving Show"は便利。
もし T が次のように定義された代数的データ型であるなら
data cx => T u1 ... uk = K1 t11 ... t1k1 | ...| Kn tn1 ... tnkn deriving (C1, ..., Cm)(ここで m>=0 および括弧は m=1 の場合省略される)、 導出インスタンス宣言は、クラス C について、以下の条件がなりたてば、可能となる。
上の条件を全て満す場合は、derivingが使える。2の条件の書き方が良くわからなかったが、「derivingしたい型がEq,Ord,Show,Readであって、かつ新たに定義したデータ型の要素となる型がそれらの型クラスに属していればderivingできる。EnumとBoundedの場合はデータ型の形式に制約がある」というように理解した。個人的にはReadもいけるのが驚き。
data P = P Int deriving Show data C = C P deriving Show
みたいなderivingはOK.
1
data Line = L Int String deriving Show
2
*Main> (L 10 "hogehoge") L 10 "hogehoge"
3
data Line = L {number::Int,string::String} deriving Show
4
linelist = [L 2 "two",L 1 "one",L 3 "three",L 5 "five",L 4 "four"] *Main> linelist [L {number = 2, string = "two"},L {number = 1, string = "one"},L {number = 3, string = "three"},L {number = 5, string = "five"},L {number = 4, string = "four"}]
5
Lineを型クラスOrdのインスタンスにしてみた。この章の流れで模範解答(http://i.loveruby.net/ja/stdhaskell/samples/line.hs.html)がそうなってないのは不思議。
import List data Line = L {number::Int,string::String} deriving (Show,Eq) instance Ord Line where compare L {number=a} L {number=b} = compare a b {- -- この仕様ならderiving Ord一発でもOK -- ただし、data宣言のnumberとstringの位置が逆だとderiving Ordでは駄目 data Line = L {number::Int,string::String} deriving (Show,Eq,Ord) -} sortLines :: [Line] -> [Line] sortLines = List.sortBy compare -- sortLines = List.sort でも可。なので実質的にはderiving Ord書けばそれだけでよい。 linelist = [L 2 "two",L 1 "one",L 3 "three",L 5 "five",L 4 "four"] *Main> sortLines linelist [L {number = 1, string = "one"},L {number = 2, string = "two"},L {number = 3, string = "three"},L {number = 4, string = "four"},L {number = 5, string = "five"}]
リファレンス的な第2部で、このタイミングで基本的な値の説明。どわーっと値に関する話題(各型のリテラルの書き方と数値計算関数とか)があったあとに、catnという関数を作ってみてしめくくり。
まず、いろんな値と関連する関数の説明。
副作用ある関数だと(||)や(&&)の実行順が気になるけど、haskellだと関係ないか。
文脈によって型が解釈されうる。明示的に型指定も可能。数値演算一覧が(p.133)に。演算するときは型に注意。暗黙の変換はしてくれない。変換にはtoIntegerとかを使う。(p.134)に変換関数抜粋。
明示的に型を指定してみた。Intは32bitなのでちょうどあふれる。Integerなら大丈夫。
*Main> (2 ^ 32) :: Int 0 *Main> (2 ^ 32) :: Integer 4294967296
有理数型(Rational)や複素数型(Complex)を使えるようになるモジュールもある。
と思いきや、Rational型はPreludeにあった。
Prelude> (toRational 1) 1%1 Prelude> :t (toRational 1) (toRational 1) :: Rational Prelude> let a = (toRational 3) Prelude> let b = (toRational 4) Prelude> a / b 3%4
(内部的にはUnicodeだがGHCでは入出力のエンコーディング変換が未実装。)
要素の個数と順序固定。要素が一個のタプルはない。
3つめ以降の値を直接とりだす関数は発見できず。パターンマッチで取れということか。
要素が0個のタプル。
:info Enumの結果
class Enum a where succ :: a -> a pred :: a -> a toEnum :: Int -> a -- 数値と値の対応 fromEnum :: a -> Int enumFrom :: a -> [a] enumFromThen :: a -> a -> [a] -- 増分指定 enumFromTo :: a -> a -> [a] -- 終端指定 enumFromThenTo :: a -> a -> a -> [a] -- 増分および終端指定
BoolやOrdering(比較結果を返す型)もEnumに属しているので…
*Main> [(False)..] [False,True] *Main> [(LT)..] [LT,EQ,GT]
ということは…自分で定義した型もEnumの型クラスに属させておけば数列記法が使える。なかなか楽しい。
data MyEnum = A | B | C | D | E | F |G deriving (Show,Enum) -- Enumのインスタンスとして定義 -- 以下、GHCiで読みこんだ後テスト *Main> [(A)..(E)] -- ()でくるまないとパースエラー [A,B,C,D,E] *Main> [(B)..] [B,C,D,E,F,G] *Main> [(A),(C)..(F)] [A,C,E] *Main> [(A),(C)..] [A,C,E,G]
p.148より
[abs x | x <- xs]
…「xsの各要素xについて(abs x)を集める」
要は、
をまとめた記法。上の例では、ひとつのリストを指定していて条件式もないが、クイックソートの例に条件式が、さらにその次に複数リスト指定した組みあわせ列挙の例あり。
全部組みあわせた例を書くとこんなかんじ。
*Main> [(x,y) | x <- [1..10],y <- [1..10] , x * 2 == y] [(1,2),(2,4),(3,6),(4,8),(5,10)]
結局、mapとfilterとconcatがひとつにまとめられているだけだが、mapの入れ子相当のものを簡単に書けるようになっていて、はまると強力。
zipLineNumberで行番号と行ペアのタプルをつくって、それぞれ(の行番号部分を)formatで整形して、最後にまとめて表示。
resolv f (x:xs) = textify x ++ resolve f xs getenv key env = fromMaybe "" $ lookup key env readTemplate id = readFile $ prefix repo ++ "/" ++ id -- に()をおぎなうと以下に resolv f (x:xs) = (textify x) ++ (resolve f xs) getenv key env = fromMaybe "" (lookup key env) readTemplate id = readFile ((prefix repo) ++ ("/" ++ id))
(++)は右結合 (infixr 5)
ちなみに、今回のソースの表示には、http://hatena.g.hatena.ne.jp/hatenagroup/20060616/1150453529 を使って色をつけました。
二章でレイアウトルールや超基本的な演算に慣れた。三章では、まず型を説明してから、関数型言語の特徴である高階関数と再帰関数の登場。また制御構文(パターンマッチやIF)もここででてくる。
ざっくりまとめてみました(第一部は掘り下げようとすると後の章を読めということになる気がする…)。
トピックを列挙
もろもろちゃんと説明してある。ひととおり読んで、p61の「これまでに紹介した関数の型」をばっと眺めて納得しよう。
以下細かい補足
take :: Int -> [a] -> [a] take :: Int ->([a] -> [a]) -- 括弧を補うとこうなる。 take ::(Int -> [a])-> [a] -- これは間違い
ちなみに、「Haskellの文字は内部的にUnicodeなのに、GHCではエンコーディング変換が未実装」ということが、ここに書いてあった。
map登場。
square n = n * n map :: (a -> b) -> [a] -> [b] map square [1,2,3] --> [1,4,9]という結果になる
tab文字を置き換えるexpandコマンドの実装を、0、1、2とバージョンを上げながら各種構文を説明していく。読みこんだ文字列をmapで変換して出力という流れは全部同じ。以下、それぞれのバージョンの特徴を列挙。
map f [] = [] map f (x:xs) = f x : map f xs
直接再帰を記述するより、map等々の高階関数を使うほうが良いスタイルなので、みんな高階関数使いましょう!というまとめ。
一番良く使うのはmapだろうな。あとはfold系とfilterやiterateがメジャーかな(自信なし)。
--普通版
swapa = do
str <- getContents
putStr $ map swapa' str
swapa' 'a' = 'A'
swapa' 'A' = 'a'
swapa' c = c
-- 短め版
swapa_ = getContents >>= putStr . (map swapa')
where swapa' 'a' = 'A'
swapa' 'A' = 'a'
swapa' c = c