「ふつける」勉強会 資料用 このページをアンテナに追加 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モナドな部分だけを切り分けられると美しい気もするが、現実的なアプリケーション作るとこうなるのはしょうがないのかな。

2006-08-05

ふつける勉強会 第8回目 第九章 「型と型クラス」 (終了) ふつける勉強会 第8回目 第九章 「型と型クラス」 (終了) - 「ふつける」勉強会 資料用 を含むブックマーク はてなブックマーク - ふつける勉強会 第8回目 第九章 「型と型クラス」 (終了) - 「ふつける」勉強会 資料用 ふつける勉強会 第8回目 第九章 「型と型クラス」 (終了) - 「ふつける」勉強会 資料用 のブックマークコメント

第二部も折り返し。ここから急に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"}
  • 列挙型
    • "|"で区切って、"あれかこれか"を表現。Bool型も実体はTrueとFalseの二つのデータコンストラクタからなる(列挙型の)型定義である。
  • 共用体型
    • 上記二つの組みあわせ。列挙型で構造体型を並べた形式。
    • 再帰的な定義(左辺で定義している型を右辺で使う)のもあり。
型コンスタラクタとデータコンストラクタ

最後(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宣言は模擬できそうだが、列挙型のほうは無理。当然ながらデータコンストラクタがないので、データコンストラクタを使ったパターンマッチはできない。

newtypedata宣言に「列挙型になってなくて、かつフィールドも一個」という制約をつけたもの。data宣言で導入したデータ型と同じように扱える。ただし、実行時には新しい型が導入されたとは見做されないので軽い…(らしい)。

newtypeの使いどころは良くわからない。

型クラス

型クラスとは、「型」の「クラス」であって「型」とは別物。複数の型を表現したいが、任意の型では困る時に使う。型変数への制約として記述される。例えば、同値性の判断は任意の型で行えるわけではない。同値性判断可能な型は、Eq型クラスに属している。

*Main> :t (==)
(==) :: forall a. (Eq a) => a -> a -> Bool
-- (Eq a) => の部分が型クラスによる制約
型クラスの実体

型クラスの実体は、Javaでいうところのインターフェイス。その型クラスインスタンスである型が実装すべき関数(これをクラスメソッドと呼ぶ)の型の集合で定義される。実装は必須でないが、デフォルト実装も定義可能。

多重定義

ある型クラスにある型を属させる場合には、クラスメソッドの実装が必要。デフォルト定義されていたら省略可能。型ごとに実装を書きわけられるので多重定義が実現できる。

継承

継承もできます。

class宣言

型クラスを定義するには、class宣言を使う。(p241,p242参照)

instance宣言

ある型をある型クラスインスタンスであると定義する宣言。(p242参照)

デフォルト実装があるといっても全て省略できるわけでないことに注意。Eqの(==)と(/=)のように、片方をインスタンスで実装することを期待した循環したデフォルト実装もある。

deriving宣言

データ型を定義したときに、deriving宣言と型クラスをつけることによって、ある型クラスに属するというinstance宣言を省略できる。ただし、万能ではなく、使える型クラスに制限があり、クラスメソッド実装が自明な場合のみ。"deriving Show"は便利。

derivingの詳細

もし T が次のように定義された代数的データ型であるなら

data cx => T u1 ... uk 	= 	K1 t11 ... t1k1 | ...| Kn tn1 ... tnkn
		deriving (C1, ..., Cm)

(ここで m>=0 および括弧は m=1 の場合省略される)、 導出インスタンス宣言は、クラス C について、以下の条件がなりたてば、可能となる。

  1. C が EqOrd、Enum、 Bounded、Show あるいは Read のうちの どれか。
  2. cx' =>C tij が構成要素の型 tij のそれぞれについて保存されているような 文脈 cx' がある。
  3. C が Bounded である場合。このとき、この型は列挙型 (すべての構築子が無引数)であるか、または構築子が一つしかないかの どちらかでなければならない。
  4. C が Enum である場合。このとき、この型は列挙型 でなければならない。
  5. T u1 ... ukを C のインスタンスとしたプログラム内の別の個所で明示的な インスタンス宣言があってはならない。

The Haskell 98 Report: Derived Instances

上の条件を全て満す場合は、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.

主要な型クラス
    • Eq,Ord
    • Show
    • Read
    • Num,Integral,Fractional

練習問題

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"}]

*1名前空間が分離されているので同じ名前でも合法

2006-07-16

ふつける勉強会 第5回目 第六章 「基本的な値」 (終了) ふつける勉強会 第5回目 第六章 「基本的な値」 (終了) - 「ふつける」勉強会 資料用 を含むブックマーク はてなブックマーク - ふつける勉強会 第5回目 第六章 「基本的な値」 (終了) - 「ふつける」勉強会 資料用 ふつける勉強会 第5回目 第六章 「基本的な値」 (終了) - 「ふつける」勉強会 資料用 のブックマークコメント

リファレンス的な第2部で、このタイミングで基本的な値の説明。どわーっと値に関する話題(各型のリテラルの書き方と数値計算関数とか)があったあとに、catnという関数を作ってみてしめくくり。

まず、いろんな値と関連する関数の説明。

真偽値 Bool

  • not,(&&),(||)

副作用ある関数だと(||)や(&&)の実行順が気になるけど、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

文字、文字列 Char,String([Char])

(内部的にはUnicodeだがGHCでは入出力のエンコーディング変換が未実装。)

タプル "(a,b)"

要素の個数と順序固定。要素が一個のタプルはない。

3つめ以降の値を直接とりだす関数は発見できず。パターンマッチで取れということか。

  • ふたつの配列からタプルの配列にするzipと逆変換unzip。
    • zip :: [a]->[b]->[(a,b)],unzip :: [(a,b)] -> ([a],[b])
    • p.142の図がわかりやすい

ユニット "()"

要素が0個のタプル。

リスト "[a]"

  • : がcons。
    • 右結合する。 1:2:3:[ ] = 1:(2:(3:[ ]))
  • null 空リスト判定。
  • (++) いわゆるappend。[1,2] ++ [3,4] -> [1,2,3,4]

リスト数列表記

  • [1..10] -> [1,2,3,4,5]
  • [1,3..10] -> [1,3,5,7,9] 増分を見てくれる。
  • '..'の前に三つ以上の要素を','で並べるとパースエラー
  • ['a'..'e'] -> "abcde" 文字もいける。増分指定も可。(['a','c'..'z']とか)
  • 終端を省略すると無限リスト.
    • ['a','c'..] もいけた。
    • 文字だと無限じゃなくて、文字コードの最後まで列挙しているらしい。
    • length ['a'..] -> 1114015
  • Enumがまさにこのための型クラスのようだ。あ、でもFloatやDoubleの挙動がよくわからない

: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の入れ子相当のものを簡単に書けるようになっていて、はまると強力。

catnの実装

行番号付きでファイルの中身を表示するコマンド

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 を使って色をつけました。

2006-06-12

ふつける勉強会 第二回目 第三章 「型と高階関数」 (終了) ふつける勉強会 第二回目 第三章 「型と高階関数」 (終了) - 「ふつける」勉強会 資料用 を含むブックマーク はてなブックマーク - ふつける勉強会 第二回目 第三章 「型と高階関数」 (終了) - 「ふつける」勉強会 資料用 ふつける勉強会 第二回目 第三章 「型と高階関数」 (終了) - 「ふつける」勉強会 資料用 のブックマークコメント

二章でレイアウトルールや超基本的な演算に慣れた。三章では、まず型を説明してから、関数型言語の特徴である高階関数再帰関数の登場。また制御構文(パターンマッチやIF)もここででてくる。

ざっくりまとめてみました(第一部は掘り下げようとすると後の章を読めということになる気がする…)。

トピックを列挙

もろもろちゃんと説明してある。ひととおり読んで、p61の「これまでに紹介した関数の型」をばっと眺めて納得しよう。

以下細かい補足

  • printの型のうち"(Show a)=>"は型クラスによる制約。9章で詳しく紹介される。
  • IO ()はいままで「アクション」と呼ばれていたものの型。IOモナド
  • 関数の型の結合の方向には注意。
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]という結果になる

expand.hs

tab文字を置き換えるexpandコマンドの実装を、0、1、2とバージョンを上げながら各種構文を説明していく。読みこんだ文字列をmapで変換して出力という流れは全部同じ。以下、それぞれのバージョンの特徴を列挙。

最後にmapの定義を解説しつつ再帰のおさらい

mapの定義と、いわゆるconsセル概念説明。

map f [] = []
map f (x:xs) = f x : map f xs
  • リストへのパターンマッチが使われている。いわゆるcarとcdrにマッチ
  • (x:xs)は()つけないとパースエラーになるので注意
  • ":"は演算子。cons。
  • mapの処理の順序も説明されているが、ややごまかしがあるような(4段目で左から実行されるのは何故?)。正確な説明は5章かな。

直接再帰記述するより、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

この章と関係ないけど補足

次回