Juliaのデストラクター
Juliaのマニュアルを読むとコンストラクターの記述はちゃんとあるのですが、デストラクターに関しての記述が見当たりません。ググっても情報が出てこないようなので、ここにその方法を残しておきます。
JuliaにはC++の~ClassName()
のようにデストラクター専用の関数はありませんが、コンストラクターの中でオブジェクトがGCされたときの処理を記述できます。
やり方は簡単で、Base.finalizer()
関数がまさにオブジェクトが破壊された時の処理を登録するための関数ですので、これを使うわけです。
finalizer(x, function)
はx
が到達不可能になったときにfunction(x)
を呼ぶ仕組みになっているため、function
に資源を解放するような処理を置いておけば良いことになります。
何かDBなどとの接続を管理する型Connection
の例で考えてみると、以下のように何かmallocなどの直後にfinalizer()
で後処理を登録すればいいことになります。
type Connection conn::Ptr{Void} open::Bool function Connection() this = new(c_malloc(sizeof_connection_info), false) finalizer(this.conn, connection_finalizer) err = ccall( (:connection_init, "libsomedb"), Cint, (Ptr{Void},), this.conn ) if err != 0 error("failed to create a new connection") end this.open = true this en
Juliaのソースで実際に使われている例を見てみましょう。
正規表現のライブラリで確保したメモリへのポインタをfinalizer()
で開放しています。
type Regex pattern::ByteString options::Uint32 regex::Ptr{Void} extra::Ptr{Void} function Regex(pattern::String, options::Integer) pattern = bytestring(pattern) options = uint32(options) if (options & ~PCRE.OPTIONS_MASK) != 0 error("invalid regex options: $options") end re = compile(new(pattern, options, C_NULL, C_NULL)) finalizer(re, function(re::Regex) re.extra != C_NULL && PCRE.free_study(re.extra) re.regex != C_NULL && PCRE.free(re.regex) end) re end end
https://github.com/JuliaLang/julia/blob/0f54ca52e350c89d821069cbdc349f1e49e43cbc/base/regex.jl#L7-27
注意する点としては、
finalizer()
はimmutableな型には適用できない- コンストラクターでは最後に自分自身を返すことを忘れない
ことが挙げられます。
参考
matplotlib入門
matplotlibはPythonでグラフを描画するときなどに使われる標準的なライブラリです。 画像ファイルを作るばかりでなく、簡単なアニメーションやインタラクティブなグラフを作ることも可能です。 実際の例はmatplotlibサイトのギャラリーで見ることができます。
matplotlibは本家のサイトやどこかのブログにあるチュートリアルや例を描画してみるぶんには簡単なのですが、 実際に自分でプロットするとなると基礎的な概念を理解していないと使いにくいライブラリでもあります。 また、基礎的な概念を理解していないとドキュメントを参照する際にもどこを見て、どう実用すればいいのかわかりません。 そこで、この記事ではそのあたりのmatplotlibの基礎を解説していきます。 なお、Python自体の知識はある程度仮定していますが、matplotlib自体の実装に関わるようなことまでは解説しませんので Pythonの関数やクラス、モジュールなどがわかれば十分でしょう。 描画するデータの作成にはnumpyを利用しています。
なお、この記事はまだ書いてる途中です。今後もっと網羅的かつ手短にmatplotlibを把握できる記事にしていくつもりです。
続きを読むHaskell Tips (文字列編)
この記事はHaskell Advent 2012のための記事です。(遅れてすいません(ヽ´ω`))
Haskellで実際にプログラムを書く上では、様々なパッケージにある型や関数を利用するのが不可避になります。 そういった便利ツールのうち文字列まわりについて調べたところを、Haskell最近始めたって人に紹介するのがこの記事の目的です。Haskell詳しい方には新しい発見は無いかもしれません。ごめんなさい。
この記事では、主に文字列に焦点を当てていますが、そのうちまた別のテーマでも書こうと思っています。
なお、前提としてるバージョンはHaskell Platform 2012.4.0.0と、それに付随するGHCやパッケージです。 OSも、Mac OS XやLinuxなどUNIX系の環境を想定しています。Windowsは持ってませんのでわかりません。ごめんなさい。
文字列
Haskellでは文字列の表現も色々あって、Haskellを始めたばかりの人はきっと混乱するでしょう。 自分も大混乱しましたよ。
Haskellで文字列を扱うのによく使われる型は大きく分けて以下の3つです。
- String
- ByteString
- Strict
- Lazy
- Text
- Strict
- Lazy
ByteString型はbytestringパッケージで、Textはtextパッケージから提供されてます。 どちらもHaskell Platformに同封されてます。
ひとまず確認して行きましょう。
String
あんまり説明は要らないと思いますが、Stringはただの文字型Charのリストです。 なのでlengthやmapなどのリスト操作関数はそのまんま使えて便利ですし、自然な発想です。
Stringでは一文字で5ワードも占めるので、 メモリを喰いますし、速度もイマイチです。
でもStringはマルチバイト文字を扱えます。 以下のようなものはランタイムのロケールを考慮して、正しく出力されるはずです。
putStrLn "鬼"
そうそう、入出力のエンコーディングなどのロケール情報は、GHC.IO.Encodingモジュールの
getLocaleEncoding
やgetLocaleFileSystemEncoding
で取得できます。
import GHC.IO.Encoding sample1 :: IO () sample1 = print =<< getLocaleEncoding
出力
UTF-8
他にもGHC.IO.Encodingにはエンコーディングを設定する機能などがあるようなので、この機能も必要があるかもしれません。
ByteString
「すごいHaskell」や「Read World Haskell」などを読まれた方はおなじみかと思います。bytestringパッケージで提供されているByteString型と関数で、効率良く文字列(というよりバイト列)を扱うことができます。
ByteStringが実際に収めているデータはWord8(8bitの符号なし整数)型なので、それより大きい要素を収めようとすると問題があります。マルチバイト文字など8bitに収まらないデータがそうです。
ByteStringには、実は同名の2つの型があります。 ひとつはData.ByteString.Internalモジュールで定義されている正格評価のByteStringで、 もうひとつはData.ByteString.Lazy.Internalモジュールにある遅延評価のByteStringです。 内部的には、Lazyな方のByteStringは、StrictのByteStringをリストにしているだけです。
-- LazyなByteStringの定義 import qualified Data.ByteString.Internal as S data ByteString = Empty | Chunk {-# UNPACK #-} !S.ByteString ByteString
正格と遅延の型を字面だけ見て組み合わせようとしても、型エラーが出るので気をつけましょう(`・ω・´) 外部のライブラリを使うときは、ByteStringとだけ書いてあってもどちらか分からないので、ソースを読むなり、HTML版のHaddockのリンク先を確認するなりしないといけませんね。
じゃあ、これらの型の相互変換はどうするのかというと、bytestringパッケージのバージョン0.10.0.1では、
toStrict
とfromStrict
が、LazyとStrictを相互変換するための関数が定義されています。
それ以前のバージョンでは、これらの関数は自作することになります。
このへんの話は以下のリンクを参考にして下さい。
- Convert a Lazy ByteString to a strict ByteString
- Proposal: Add {to,from}Strict conversion functions between strict and lazy ByteStrings
他に、Data.ByteStringにはモジュール名の末尾にChar8がつくData.ByteString.Char8とData.ByteString.Lazy.Char8もありますが、これらはByteStringが内部でもつWord8のデータとCharの相互変換をしてくれる関数を間にかませているだけなので、本質的には何も変わりません。変換の分のオーバーヘッドはありそうですが。
このへんの挙動をちょっと見てみましょう。
下のコードの頭にあるOverloadedStrings
は、文字列リテラルをIsString
型クラスのインスタンスへと自動的に変換してくれます。
ですので、文字列リテラルに関しては、bytestringやtextパッケージで用意されているpack
関数を明示的に呼ぶ必要はありません。
コメントでは文字に対応する文字符号化形式のコードを書いています。
{-# LANGUAGE OverloadedStrings #-} import qualified Data.ByteString.Char8 as B8 sample :: IO () sample = do -- '鬼': -- UTF-16: 9B3C -- UTF-8: E9ACBC -- '<': -- ASCII: 3C B8.putStrLn (B8.singleton '鬼') -- '平': -- UTF-16: 5E73 -- UTF-8: E5B9B3 -- 's': -- ASCII: 73 B8.putStrLn (B8.singleton '平') B8.putStrLn (B8.pack "鬼平") B8.putStrLn "鬼平" -- OverloadedStringsがなければ、この書き方は型エラー
出力
<
s
<s
<s
漢字などマルチバイト文字は、Char型としての文字リテラルではUTF-16でエンコードされている様子です。 しかし、ByteStringは内部的にはWord8(8bit)単位でしか扱えないので、 ByteStringに変換する際に、CharのデータはWord8(8bit)に切り詰められてるのが分かると思います。
文字列リテラルで指定されたマルチバイト文字を無理やりByteStringで扱おうとすると、特にエラーも出さず間違った結果を返すので注意が必要ですね。 なので、ByteStringは文字列というよりは、生のバイト列を収めるデータ型と思ったほうが良いと思います。
IOを介すなどして外部から与えられたマルチバイト文字は、 エンコーディングなど考慮されずにそのままバイト列として扱えます。 なので、以下のようにユーザーの入力を受け取る場合は、入力がマルチバイト文字を含んでいても問題ありません。入力をそのまま吐いてるだけです。
sample :: IO () sample = do putStrLn "What is your name?" putStr "> " name <- B8.getLine B8.putStrLn $ "Hello, " `B8.append` name putStrLn $ (show $ B8.length name) ++ "-byte"
出力
What is your name?
> 鬼平
Hello, 鬼平
6-byte
バイト数がUTF-8でエンコードしたときにあたる6バイトになっちゃってますね。 これでは文字数は正しく数えられませんし、文字境界も分からないのでgrepのようなことも出来ません。
Text
じゃぁこういったマルチバイト文字をバイト列でなく、ちゃんと文字列として扱うにはどうすればいいのかと言うと、textというパッケージがありまして、こいつを使うというわけです。
ちゃんと文字列として扱うとは、文字列の長さが正しく数えられるとか、特定の文字を探せるとかそういう話です。
textパッケージでは、文字列操作のためのモジュールとIOのためのモジュールが別のモジュール(Data.Text.IO)にあります。下のように、同じT
という別名をつけてimportすると楽ちんです。
{-# LANGUAGE OverloadedStrings #-} import qualified Data.Text as T import qualified Data.Text.IO as T sample :: IO () sample = do T.putStrLn (T.singleton '鬼') T.putStrLn (T.singleton '平') T.putStrLn (T.pack "鬼平") T.putStrLn "鬼平" -- これもOverloadedStringsが必要
出力
鬼
平
鬼平
鬼平
良い感じですね(´ω`) Data.Text.IOではロケールを考慮してくれています。
先ほどのByteStringのときと同じサンプルも、Textなら大丈夫です。
sample :: IO () sample = do putStrLn "What is your name?" putStr "> " name <- T.getLine T.putStrLn $ "Hello, " `T.append` name putStrLn $ (show $ T.length name) ++ "-letter"
出力
What is your name?
> 鬼平
Hello, 鬼平
2-letter
ちゃんと文字数を数えています。バイト数じゃなくって。 でも、Textのlength関数の計算量はO(n)です(´・ω・`)
Data.Text.IOには他にファイルやHandleとText型のデータをやり取りする関数も用意されてます。
TextはByteStringとの相互変換も可能ですし、Lazyなバージョンもあります。
相互変換に必要な関数はData.Text.Encoding
モジュールにあります。
利用できるエンコーディングは以下の3つです。
下の例では、マルチバイト文字をByteStringで表現して、decode系の関数がそのByteStringをバリバリ喰ってTextにガガッと変換してくれます。
ちなみに、任意のバイト列を作成したい時は、
以下の様に文字列リテラルの中でバックスラッシュ()と16進数を表すx
で指定することができます。
10進数では、x
は要りません。
{-# LANGUAGE OverloadedStrings #-} import qualified Data.ByteString as B import qualified Data.Text as T import qualified Data.Text.IO as T import qualified Data.Text.Encoding as T import qualified Data.Text.Encoding.Error as T sample :: IO () sample = do -- '鬼': -- UTF-8: E9ACBC -- UTF-16: 9B3C let oniUTF8 = "\xE9\xAC\xBC" :: B.ByteString oniUTF16BE = "\x9B\x3C" :: B.ByteString -- ビッグエンディアン oniUTF16LE = "\x3C\x9B" :: B.ByteString -- リトリエンディアン T.putStrLn $ T.decodeUtf8 oniUTF8 -- Output: 鬼 T.putStrLn $ T.decodeUtf16BE oniUTF16BE -- Output: 鬼 T.putStrLn $ T.decodeUtf16LE oniUTF16LE -- Output: 鬼 -- エンディアンの間違い!! T.putStrLn $ T.decodeUtf16LE oniUTF16BE -- Output: 㲛
UTF-8ではバイトオーダーの指定は不要なのですが、 UTF-16では必要で、バイトオーダーを間違えると別の文字として認識されていしまいます。 外部から受け取ったUTF-16でエンコードされているはずのByteStringなどを、Text型に変換する際はバイトオーダーに気をつけましょう。
あと、外部から受け取ったデータなどをText型に落としこむ際には気をつけなければならないことがあります。
以下の例は例外を投げてしまいます。
T.decodeUtf16BE "\xD8\x00"
出力
"*** Exception: Cannot decode input: Data.Text.Encoding.Fusion.streamUtf16BE: Invalid UTF-16BE stream
これは、Text型が内部的にはUTF-16を利用していることが原因です。
UnicodeではU+D800~U+DBFFとU+DC00~U+DFFFの範囲の16bitをペア(サロゲートペア)にして、16bitだけでは表現しきれない文字を表現しています。
この範囲はUTF-16では表現しきれないので、decodeUtf16BE
などでは例外を投げます。
外部から与えられたByteStringをデコードするときは、この挙動に注意して例外を拾うか、
decodeUtf16BEWith
関数など、デコードのエラーが生じた時の挙動を明示する関数を使うといいでしょう。
lenientDecodeを使うと、デコードできないものはコードポイントU+FFFDに置き換えられます。
T.putStrLn $ T.decodeUtf16BEWith T.lenientDecode "\xD8\x00"
他にもUTF-8に限ればutf8-stringというパッケージがあるようですが、使ったことはありませんので分かりません(ヽ´ω`)
blaze-builder
チャンクのサイズが...
ByteStringの説明のところで、LazyなByteStringはStrictなByteStringのリストだと説明しました。 だとすると、細かく作ったStrictなByteStringをたくさんつなげばLazyなByteStringを作る場合、結局細かいByteStringが連なったリストが出来上がりそうです。
下の例では、5バイトのByteStringを10個連結して、50バイトのLazyなByteStringを作っています。 それをまたチャンクに分解して、それぞれの長さを調べると、確かに5バイトのチャンクが10個連なってます。
{-# LANGUAGE OverloadedStrings #-} import Control.Monad import Data.Monoid import Blaze.ByteString.Builder as B import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Char8 as BS chunkSizes :: BL.ByteString -> [Int] chunkSizes = map BS.length . BL.toChunks concatByteString :: Int -> BS.ByteString -> BL.ByteString concatByteString n = BL.fromChunks . replicate n main = print . chunkSizes $ concatByteString 10 "xxxxx"
出力
[5,5,5,5,5,5,5,5,5,5]
5バイトずつのチャンクになって、これじゃァCharのリストだったStringと大して変わらんってことで、 ひとつひとつのチャンクサイズを広げて、うまいことLazyなバイトストリングを作ってくれるのがblaze-builderです。
blaze-builderではBuilder
型を介して文字列の結合をO(1)で行い、LazyなByteStringを吐き出すときに大きなチャンクを提供してくれます。
blaze-builderで文字列連結をするため、blazeConcatByteString
関数を加えた例が以下のコードです。
{-# LANGUAGE OverloadedStrings #-} import Control.Monad import Data.Monoid import Blaze.ByteString.Builder as B import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Char8 as BS byteN :: Int -> BS.ByteString byteN n = BS.concat $ replicate n "x" chunkSizes :: BL.ByteString -> [Int] chunkSizes = map BS.length . BL.toChunks concatByteString :: Int -> BS.ByteString -> BL.ByteString concatByteString n = BL.fromChunks . replicate n blazeConcatByteString :: Int -> BS.ByteString -> BL.ByteString blazeConcatByteString n = B.toLazyByteString . mconcat . map B.fromByteString . replicate n main = forM_ [1, 10, 100, 1000, 5000, 10000] $ \x -> do let bs = byteN x putStrLn $ show x ++ "-byte:" putStr "no-blaze " print . chunkSizes $ concatByteString 10 bs putStr "blaze " print . chunkSizes $ blazeConcatByteString 10 bs putStr "\n"
出力
1-byte:
no-blaze [1,1,1,1,1,1,1,1,1,1]
blaze [10]
10-byte:
no-blaze [10,10,10,10,10,10,10,10,10,10]
blaze [100]
100-byte:
no-blaze [100,100,100,100,100,100,100,100,100,100]
blaze [1000]
1000-byte:
no-blaze [1000,1000,1000,1000,1000,1000,1000,1000,1000,1000]
blaze [4080,5920]
5000-byte:
no-blaze [5000,5000,5000,5000,5000,5000,5000,5000,5000,5000]
blaze [4080,32752,13168]
10000-byte:
no-blaze [10000,10000,10000,10000,10000,10000,10000,10000,10000,10000]
blaze [10000,10000,10000,10000,10000,10000,10000,10000,10000,10000]
blaze-builderを使ってつなげたほうが、チャンクのサイズが大きくなって、連結リストの長さが短くなってますね。 こうしてチャンクのサイズを大きくすることで、キャッシュの利用を効率化したり、入出力に関わるシステムコールのオーバーヘッドを低減しているようです。
詳しいバッファ利用の戦略などについては、blaze-builderのhaddockや作者のブログ記事を参考にして下さい。
これを利用して、HTMLやSVGなどを高速に構築するblaze-htmlやblaze-svgといったパッケージがあります。 WebアプリケーションフレームワークのYesodなどでも使われていますね。
ベンチマーク
最後に、blaze-builderがどれくらい効くのが、ベンチマークをカジュアルに取ってみましょう!
ベンチマークにはcriterionを使ってます。Haskellのベンチマークが簡単に取れて、ブラウザで見られる出力も得られます。
blazeを使用しない元々の連結操作と、blazeを利用した連結との両方の方針で文字列片をつなげて、ファイルに出力しています。 ついでに、Textに関してもByteStringと同様にベンチマークを取ってみました。
{-# LANGUAGE OverloadedStrings #-} import Data.Monoid import Blaze.ByteString.Builder as B import Blaze.ByteString.Builder.Char.Utf8 as BT import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Char8 as BS import qualified Data.Text as T import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.IO as TL import qualified Data.Text.Encoding as T import qualified Data.Text.Lazy.Builder as T import Criterion.Main import System.IO (openTempFile) import System.Directory (removeFile) -- N-byteのByteStringを作る byteN :: Int -> BS.ByteString byteN n = BS.concat $ replicate n "x" concatByteString :: Int -> BS.ByteString -> BL.ByteString concatByteString n = BL.fromChunks . replicate n blazeConcatByteString :: Int -> BS.ByteString -> BL.ByteString blazeConcatByteString n = B.toLazyByteString . mconcat . map B.fromByteString . replicate n -- N-文字のTextを作る textN :: Int -> T.Text textN n = T.concat $ replicate n "鬼" concatText :: Int -> T.Text -> TL.Text concatText n = TL.fromChunks . replicate n blazeConcatText :: Int -> T.Text -> TL.Text blazeConcatText n = T.toLazyText . mconcat . map T.fromText . replicate n main :: IO () main = do -- 出力用の一時ファイル (tmpfile, tmph) <- openTempFile "/tmp" "bench_blaze" -- 1, 10, 50の大きさの単語を、10000個つなげる let samples = [1,5,10,50] wc = 10000 defaultMain $ [ bgroup ("[bytestring] " ++ show x ++ "-byte words") [ bench "no-blaze" $ BL.hPut tmph $ concatByteString wc (byteN x) , bench "blaze" $ BL.hPut tmph $ blazeConcatByteString wc (byteN x) ] | x <- samples ] ++ [ bgroup ("[text] " ++ show x ++ "-letter words") [ bench "no-blaze" $ TL.hPutStr tmph $ concatText wc (textN x) , bench "blaze" $ TL.hPutStr tmph $ blazeConcatText wc (textN x) ] | x <- samples ] removeFile tmpfile
上半分がByteStringで、下半分がTextです。
ちっちゃい文字列片ほど、効果が大きいようですね。
ベンチマークの実行は以下の様な感じです。
% ghc -O2 bench_blaze.hs % ./bench_blaze -o bench_blaze.html
出力は分かりやすさのため一部削ってます。
出力
warming up
estimating clock resolution...
mean is 1.488974 us (640001 iterations)
found 4828 outliers among 639999 samples (0.8%)
3539 (0.6%) high severe
estimating cost of a clock call...
mean is 58.44394 ns (8 iterations)
found 1 outliers among 8 samples (12.5%)
1 (12.5%) high severe
benchmarking [bytestring] 1-byte words/no-blaze
mean: 1.221033 ms, lb 1.214946 ms, ub 1.228679 ms, ci 0.950
std dev: 34.94729 us, lb 29.31052 us, ub 43.71415 us, ci 0.950
benchmarking [bytestring] 1-byte words/blaze
mean: 136.0696 us, lb 125.3679 us, ub 144.9379 us, ci 0.950
std dev: 49.58111 us, lb 39.05003 us, ub 60.22993 us, ci 0.950
benchmarking [bytestring] 5-byte words/no-blaze
mean: 1.321834 ms, lb 1.310183 ms, ub 1.337639 ms, ci 0.950
std dev: 68.97484 us, lb 54.09187 us, ub 95.68489 us, ci 0.950
benchmarking [bytestring] 5-byte words/blaze
mean: 743.6934 us, lb 709.6834 us, ub 775.6366 us, ci 0.950
std dev: 169.3327 us, lb 132.2535 us, ub 223.0841 us, ci 0.950
benchmarking [bytestring] 10-byte words/no-blaze
mean: 1.396986 ms, lb 1.387786 ms, ub 1.408378 ms, ci 0.950
std dev: 52.24961 us, lb 42.96288 us, ub 63.49543 us, ci 0.950
benchmarking [bytestring] 10-byte words/blaze
mean: 1.448711 ms, lb 1.383338 ms, ub 1.513390 ms, ci 0.950
std dev: 331.6004 us, lb 294.9020 us, ub 414.8301 us, ci 0.950
benchmarking [bytestring] 50-byte words/no-blaze
mean: 6.478227 ms, lb 5.424632 ms, ub 7.761446 ms, ci 0.950
std dev: 5.944880 ms, lb 5.028813 ms, ub 7.670362 ms, ci 0.950
benchmarking [bytestring] 50-byte words/blaze
mean: 7.454674 ms, lb 7.041077 ms, ub 8.173508 ms, ci 0.950
std dev: 2.719382 ms, lb 1.760055 ms, ub 5.017425 ms, ci 0.950
benchmarking [text] 1-letter words/no-blaze
mean: 3.269654 ms, lb 3.259670 ms, ub 3.282536 ms, ci 0.950
std dev: 58.06780 us, lb 46.70660 us, ub 75.66644 us, ci 0.950
benchmarking [text] 1-letter words/blaze
mean: 306.1035 us, lb 304.2076 us, ub 308.4018 us, ci 0.950
std dev: 10.65997 us, lb 9.045305 us, ub 12.66282 us, ci 0.950
benchmarking [text] 5-letter words/no-blaze
mean: 4.930164 ms, lb 4.542772 ms, ub 6.752361 ms, ci 0.950
std dev: 3.662762 ms, lb 177.2833 us, ub 8.698922 ms, ci 0.950
benchmarking [text] 5-letter words/blaze
mean: 1.524795 ms, lb 1.515778 ms, ub 1.535984 ms, ci 0.950
std dev: 51.46477 us, lb 43.33059 us, ub 64.69094 us, ci 0.950
benchmarking [text] 10-letter words/no-blaze
mean: 5.897089 ms, lb 5.879782 ms, ub 5.917257 ms, ci 0.950
std dev: 95.94202 us, lb 82.75690 us, ub 114.9103 us, ci 0.950
benchmarking [text] 10-letter words/blaze
mean: 3.982748 ms, lb 3.591212 ms, ub 4.620310 ms, ci 0.950
std dev: 2.510847 ms, lb 1.805720 ms, ub 3.784512 ms, ci 0.950
benchmarking [text] 50-letter words/no-blaze
mean: 21.43716 ms, lb 19.98548 ms, ub 27.01497 ms, ci 0.950
std dev: 13.20911 ms, lb 2.446692 ms, ub 31.00659 ms, ci 0.950
benchmarking [text] 50-letter words/blaze
mean: 21.46007 ms, lb 20.58942 ms, ub 22.80350 ms, ci 0.950
std dev: 5.445815 ms, lb 3.780456 ms, ub 8.059161 ms, ci 0.950
ところで、最新のbytestring-0.10.2.0のパッケージを見ると、Builder関係のパッケージがbytestringから提供されてますね。 ドキュメントも、サンプルコードなどがあり充実してます。 textでは既にBuilderが入ってきてます。
まとめ
3種の文字列型と連結ライブラリのblaze-builderを見てきました。
- Stringはマルチバイト文字も扱えてカジュアルに使えるけど長い文字列には向かない。
- ByteStringは文字列というよりバイト列で、各種データをシリアライズするには便利。blaze-builderもあるし。
- TextはStringのようにマルチバイト文字も扱えて、ByteStringのように効率のいい処理ができて嬉しい。
これらの実装もHaskellで書かれているので、ソースを読んでみればきっと面白い発見があると思いますよ(・ω<)。
何か間違いなどありましたら、Twitter宛@brain_appleかコメント欄などで教えて頂けるとありがたいです。
モナドトランスフォーマー・ステップ・バイ・ステップ(Monad Transformers Step By Step)
著者のMartin Grabmüller氏に許可をいただきましたので、 Haskellモナドトランスフォーマーのチュートリアルを翻訳したものを公開します。
タイポや誤訳、プログラムのミス等ありましたら、 Twitter宛@bicycle1885かコメント欄までご連絡頂けるとありがたいです。
元のテキストやプログラムは以下のリンクから得られます。
Monad Transformers Step by Step
- [2012/12/19] 誤りを多数訂正しました。id:qtamakiさん、ありがとうございます。
- [2014/6/19] 誤りを2点訂正しました。id:daimatzさん、id:hitotakuchanさん、ありがとうございます。
Monad Transformers Step by Step
Martin Grabmüller Oct 16 2006
概要(Abstract)
このチュートリアルでは、Haskellのプログラムに徐々に機能を追加していくためには、どのようにモナドトランスフォーマー(Monad Transformer)を利用したらよいかを説明します。
これはモナドトランスフォーマー自体の「実装」に関するものではありません。 エレガントでスッキリしていて、なおかつ強力なHaskellプログラムを書くために、いかにモナドトランスフォーマーを「利用」するかについてのチュートリアルです。
単純な式を評価する関数から始め、モナドスタイルに変えてから、モナドトランスフォーマーを構築しつつ、エラー処理、環境渡し、状態、ログ、入出力といった機能を加えていきます。
1 イントロダクション(Introduction)
このチュートリアルでは、Haskellのモナドトランスフォーマーへステップ・バイ・ステップで紹介していきます。
モナドは、プログラムを組み上げていく上で、柔軟で拡張性を持つ非常にエレガントな手法です。 モナドはHaskellのような遅延評価関数型言語ではとても興味深いもので、純粋関数型のプログラムに副作用を統合させることができます。 さらに、モナドを使ってプログラムを構築していくことで、ちょっとした定義があれば、色々なアルゴリズムの必要な記録処理や裏側の処理の多くを隠すことができ、注目してるアルゴリズムに集中することができます。
モナドトランスフォーマーはモナドを使ったプログラミングをさらに便利にしてくれます。 違ったモナドや型やモナドを連結する関数のライブラリを提供してくれるので、必要なモナドトランスフォーマーを組み合わせて特製のモナドを作ることができます。 例えば、状態とエラー処理を備えたモナドが欲しいなら、StateTとErrorTふたつのモナドトランスフォーマーを選んできて組み合わせればよいのです。 このチュートリアルの目的は、シンプルな関数から始め、それをステップ・バイ・ステップで機能を拡張すべく色々とモナドを操って広げていく様を優しく紹介することです。 このチュートリアルはモナドトランスフォーマーの概念の裏に潜んだ理論についてのものではありませんし、実装についてのものでもありません(うまいことモナドトランスフォーマーを使うのに必要なものは除く)。
ここでは、Haskellのモナドクラスとかdo記法などといった、モナドを使ったプログラミングの機能や基礎については知っていることを想定しています。他のことは途中で追々説明していきます。
ここにあるHaskellのプログラムは現在のHaskell98標準のものではない機能を使っています。Control.Monad.Errorなどは標準ライブラリのモジュールではありません。 こうしたモジュールにある階層的なモジュール名も細かい実装もHaskell98の範疇ではありません。 しかしながら、こうした拡張は現在のGHC[1]ではちゃんとサポートされています。 プログラムはGHCのバージョン7.4.1で確認しています。
モナドトランスフォーマーのモジュールはMark P.Jonesの論文[2]にインスパイアされています。その中には、とても読みやすいMonadプログラミングのイントロダクションはありますが、このチュートリアルほどは実践的ではありません。
この文章は文芸的Haskell(Literate Haskell)のスクリプトからAndres Lo ̈hのlhs2TeXプリプロセッサを使って変換されています。 そのテキストはGHCで実行可能です。 文芸的Haskellのソースファイル Transformers.lhs は著者のウェブサイトhttp://www.grabmueller.de/martin/www/pub/Transformers.en.htmlにあります。
コンピュータを前にしてこのチュートリアルを読むのがベストでしょう。 Haskellの標準ライブラリやモナドトランスフォーマーのライブラリにある、いろいろな関数の説明を調べたり、型を調べたりできるようにです。 このチュートリアルを印刷して、ウェブブラウザでオンラインのライブラリのドキュメントを開いて、Transformersモジュール(あとで説明します)をロードしてghciで対話的に:type(もしくは:t)で型を確認しながら実行してみるのが一番でしょう。
1.1 プログラム例(Example Program)
実行する例として、シンプルなプログラミング言語のインタープリタをこのチュートリアルを通して使います。 すべてのコードはTransformersというモジュールに収められ、次のようなコードが頭にあります。
module Transformers where import Control.Monad.Identity import Control.Monad.Error import Control.Monad.Reader import Control.Monad.State import Control.Monad.Writer import Data.Maybe import qualified Data.Map as Map
Control.Monadで始まるインポートされたモジュールは、その中で定義されたモナドトランスフォーマーを使うときだけ必要となります。 Data.MaybeモジュールはMaybe a型の任意の値を扱うのに便利な関数を定義していて、Data.Mapモジュールは有限のマップを定義します。 これらは、環境(変数-値のマッピング)を小さなインタープリタの中で定義するのに使われます。
次のデータ型がその言語の中でプログラムをモデリングをするのに使われます。
type Name = String -- variable names (変数名) data Exp = Lit Integer -- expressions (式) | Var Name | Plus Exp Exp | Abs Name Exp | App Exp Exp deriving (Show) data Value = IntVal Integer -- values (値) | FunVal Env Name Exp deriving (Show) type Env = Map.Map Name Value -- mapping from names to values (変数名から値へのマッピング)
Name型は標準のString型のただの別名です。 普遍的な文字列でなく、変数名についての話であることを明確にする時に使われます。 Expデータ型は、整数リテラル・変数・加算・λ式(抽象)・関数適用へのヴァリアントを持っています。 評価されるプログラムはExpデータ型からできていて、結果はValue型です。 Valueは整数か関数(クロージャ)です。 FunValの構成要素であるEnvは、λ抽象が評価される環境です。
モナドトランスフォーマーを用いる具体例は、上に示した小さな言語のインタープリタなので、まずは評価関数をつくることから始めます。 この関数はモナドを使っておらず、ある種の「参照実装」となっています。 インタープリタ関数eval0は、単純です。
eval0 :: Env -> Exp -> Value eval0 env (Lit i) = IntVal i eval0 env (Var n) = fromJust (Map.lookup n env) eval0 env (Plus e1 e2) = let IntVal i1 = eval0 env e1 IntVal i2 = eval0 env e2 in IntVal (i1 + i2) eval0 env (Abs n e) = FunVal env n e eval0 env (App e1 e2) = let val1 = eval0 env e1 val2 = eval0 env e2 in case val1 of FunVal env' n body -> eval0 (Map.insert n val2 env') body
整数リテラル(Lit)は、Value型に包まれてそのまま自分自身に評価されます。 変数(Var)は、その環境で束縛されている値に評価されます。 fromJust関数はMap.lookup関数がMaybeを返すので必要です。 この関数では、どこにもラムダ式で束縛されていない変数が使われてしまうと、エラーメッセージと共にプログラムは停止します。 加算(Plus)は単純に2つのオペランドを評価して、その和を返します。 加算のオペランドがが片一方でも数値にならなかったら、let式のパターンマッチングは失敗し、やはりエラーメッセージと共にプログラムは終了してしまいます。 抽象(Abs)は、評価される環境にを捉えて関数値に評価されます。 関数適用(App)は、最初に関数と引数を評価し、加算と同じように処理されます。 一番目の式は関数値に評価されなければならず、環境に従ってその内部が評価されます。 ここで使われている関数値を分解しているcase式は、また別のエラーを吐くかもしれません。 このチュートリアルの後の方では、エラーモナドを使ってエラーを扱い、エラーの扱いをもっと強力にしています。
eval0の定義はもう少し短くもできます。例えば、Appの時のlet式は冗長かもしれません。 しかし、ここでの定義は、後で定義されるモナド版にするのが簡単になります。
12 + ((λx -> x)(4+2))
この式は、このインタープリタを試すのにも使えますし、すぐに残りも定義していきます。
exampleExp = Lit 12 `Plus` (App (Abs "x" (Var "x")) (Lit 4 `Plus` Lit 2))
例えば、
> eval0 Map.empty exampleExp
とghciに入力すれば、答えは
IntVal 18
となります。
2 モナドトランスフォーマー(Monad Trasformers)
モナドトランスフォーマーを使う目的は、状態・エラー・環境といった計算の側面をコントロールすることです。 すでにあるプログラムをモナドを使って書きなおすのは少々面倒臭いですが、一度やってしまうとモナドが絡む部分を加えたり、削ったり、変更したりするのは比較的簡単です。
この節では、1.1節のプログラムをモナドを使って書き直し、データ型や関数定義を様々なモナドトランスフォーマーの型や関数を使って徐々に拡張していきます。
2.1 モナドスタイルに(Converting to Monadic Style)
モナドトランスフォーマーを使うには、関数をモナドスタイルで表現しなければなりません。 それはつまり、逐次処理をdo記法を使ったモナド操作で書き、関数の結果を示すためにreturn関数を使う必要があるということです。
まず、評価装置が定義されるモナドを定義します。 下のEval1 aをIdentity a型の別名として定義します。Identityは、Control.Monad.Identityからインポートされてモナドで、想像しうる限り一番単純なモナドかもしれません。 Identityは、標準的なreturnや(>>=)をモナド操作の構築のために定義していて、runIdentity関数をそのような操作を実行するために追加で定義しています。 それ以外にはIdentityモナドは何もしません。 ある意味で、このモナドを「基礎」として使い、周りを別のモナドで包むこともできます。 可読性のため、ただrunIdentityを呼ぶためのrunEval1も定義しています。 runEval1はrunIdentityを呼び出すだけです。
type Eval1 a = Identity a runEval1 :: Eval1 a -> a runEval1 ev = runIdentity ev
Eval1モナドをもとにeval0関数をeval1として書きなおします。
eval1 :: Env -> Exp -> Eval1 Value eval1 env (Lit i) = return $ IntVal i eval1 env (Var n) = maybe (fail ("undefined variable: " ++ n)) return $ Map.lookup n env eval1 env (Plus e1 e2) = do IntVal i1 <- eval1 env e1 IntVal i2 <- eval1 env e2 return $ IntVal (i1 + i2) eval1 env (Abs n e) = return $ FunVal env n e eval1 env (App e1 e2) = do val1 <- eval1 env e1 val2 <- eval1 env e2 case val1 of FunVal env' n body -> eval1 (Map.insert n val2 env') body
まず注目すべきことは、LitとAbsの場合に結果を表すためにreturn関数を使っていることです。 (($)演算子は結合順位が低い関数適用で、主に丸括弧()を省くに使われます。) 次に、Varの場合にはfromJustはもう必要ありません。 これは、Map.lookupはどんなモナドの中でもfail関数を呼ぶだけで動くようになっていて、ここではこれがうまいこと当てはまるからです。 (Identityモナドのfail関数は例外を投げる一方、Maybeモナドのfail関数はNothingを返すので、異なるエラーメッセージが現れます。)
PlusやAppの場合は内部の式をdo記法で評価し、結果を変数に束縛しています。 Plusでは結果はreturnを使って返されますが、Appでは関数値は上のeval0関数のようにさらに識別されます。
このインタープリタを試すには、eval1をサンプルの式exampleExpに適用して得られたモナドのアクションを評価する必要があります。 これは前に定義したrunEval1を呼べば大丈夫です。
runEval1 (eval1 Map.empty exampleExp)
とすれば、
IntVal 18
となります。
要約すれば、return関数を使って関数の結果を返すことと、do記法や(>>=)や(>>)関数をつかってモナドアクションの逐次を行うことの2つからモナドへの変換は成り立っています。
NOTE
eval1の型は、以下のように一般化できます。
eval1 :: Monad m => Env -> Exp -> m Value
これは、returnと、do記法に隠された(>>=)以外には、どんなモナド操作も行っていないためです。 こうすればどんなモナドの文脈でもeval1を使うことができ、
runEval1 (eval1 Map.empty exampleExp)
とする代わりにghciで
eval1 Map.empty exampleExp
と書けます。 これは、IOモナドの内部で式を実行します。インタープリタは内部的にprint関数を使っていますが、この関数はIOモナドの内部でしか使えないためです。 これは嬉しいこともありますが、特定のモナドに固有の操作を使うことが多いでしょうし、特定のモナドに操作は縛られてしまいます。
2.2 エラー処理を加える(Adding Error Handling)
今まで見てきたように、現状の評価関数は部分的(partial)です。 例えば束縛されていない変数や型エラーがある式など、入力によってはエラーメッセージと共に止まってしまいます。 (訳注: 部分関数については、Partial Function Considered Harmfulが参考になります)
ローカルのモナドトランスフォーマーライブラリにあるErrorTモナドトランスフォーマーを使えば、Eval1モナドを基にしてEval2へ拡張できます。
type Eval2 a = ErrorT String Identity a
ErrorTのString型引数は例外の型で、エラーの状態を示すために使われる値です。 ここでは簡単のためにStringを使いますが、実際の実装ではコンパイラだとソースコードの位置だとか、ウェブアプリケーションだとタイムスタンプだとかがあると良いかもしれません。
Eval2モナド内での計算を実行する関数は、2つ異なる点があります。 ひとつ目は評価の結果がEither String a型で、Left sだとエラーが起きたことをエラーメッセージsで示し、Right rだと評価が成功したことを結果rで表します。 二つ目は、runErrorT関数を与えられた計算に対して呼び出し、Identityの計算が返され、今度はそれがrunIdentityを使って評価されるということです。
runEval2 :: Eval2 a -> Either String a runEval2 ev = runIdentity (runErrorT ev)
さてもうeval1関数の型を単純に書き換えることができて、次のバージョン(eval2a)を示します。
eval2a :: Env -> Exp -> Eval2 Value eval2a env (Lit i) = return $ IntVal i eval2a env (Var n) = maybe (fail ("undefined variable: " ++ n)) return $ Map.lookup n env eval2a env (Plus e1 e2) = do IntVal i1 <- eval2a env e1 IntVal i2 <- eval2a env e2 return $ IntVal (i1 + i2) eval2a env (Abs n e) = return $ FunVal env n e eval2a env (App e1 e2) = do val1 <- eval2a env e1 val2 <- eval2a env e2 case val1 of FunVal env' n body -> eval2a (Map.insert n val2 env') body
このバージョンは上で定義されたrunEval2関数を使って実行できます。 この関数をサンプルの式に適用すると、結果はRight構築子に包まれている点のみが変わっています。
runEval2 (eval2a Map.empty exampleExp) => Right (IntVal 18)
しかし残念なことに、正しくない式が与えられるとErrorTトランスフォーマーのエラー報告は使われません。 もっと有用なエラーメッセージを吐くためには改良が必要です。
eval2b :: Env -> Exp -> Eval2 Value eval2b env (Lit i) = return $ IntVal i eval2b env (Var n) = maybe (fail ("undefined variable: " ++ n)) return $ Map.lookup n env eval2b env (Plus e1 e2) = do e1' <- eval2b env e1 e2' <- eval2b env e2 case (e1', e2') of (IntVal i1, IntVal i2) -> return $ IntVal (i1 + i2) _ -> throwError "type error" eval2b env (Abs n e) = return $ FunVal env n e eval2b env (App e1 e2) = do val1 <- eval2b env e1 val2 <- eval2b env e2 case val1 of FunVal env' n body -> eval2b (Map.insert n val2 env') body _ -> throwError "type error"
これで、正しくない式を評価しようとすると、Left構築子に包まれたエラーメッセージが出ます。 そして、評価結果に対してパターンマッチングをすることで、通常の結果とエラーの結果を区別することができます。
runEval2 (eval2b Map.empty (Plus (Lit 1) (Abs "x" (Var "x")))) => Left "type error"
(訳注: 原文では runEval2 (eval2a Map.empty (Plus (Lit 1) (Abs "x" (Var "x"))))
となっているが、著者の意図は恐らくeval2aでなくeval2b)
しかしちょっと待ってください! Map.lookupについてはどうでしょう? Nothingを返すかどうかを確かめて、適切なエラーメッセージを生成するべきではないでしょうか? 前述したとおり、Map.lookupは任意のモナドで結果を返しますし、Control.Monad.Errorモジュールはすぐに動くように必要な定義を提供しています。
runEval2 (eval2b Map.empty (Var "x")) => Left "Data.Map.lookup: Key not found"
(訳注: containers-0.5.2.1
の実際のData.Map.lookupの定義は、Maybeモナドを返すようになっている。なので、上記のエラーメッセージは、実際には Left "undefined variable: x"
となる)
eval2b関数をもうちょっとよく調べてみれば、do式の内部のモナド結合ではパターンマッチングが失敗したときにfail関数を呼び出すということをうまく使うと、もっと短く(良く?)できることがわかります。 そして、今まで見てきたように、fail関数は思ったとおりに動いてくれます。
eval2c :: Env -> Exp -> Eval2 Value eval2c env (Lit i) = return $ IntVal i eval2c env (Var n) = maybe (fail ("undefined variable: " ++ n)) return $ Map.lookup n env eval2c env (Plus e1 e2) = do IntVal i1 <- eval2c env e1 IntVal i2 <- eval2c env e2 return $ IntVal (i1 + i2) eval2c env (Abs n e) = return $ FunVal env n e eval2c env (App e1 e2) = do FunVal env' n body <- eval2c env e1 val2 <- eval2c env e2 eval2c (Map.insert n val2 env') body
この関数の難点は、エラーメッセージが"pattern match failure"(パターンマッチング失敗)としか言わないことで、どうしてパターンマッチングが失敗したのかについて具体的な情報は何もないということです。 ですので、もっと良いエラーメッセージが欲しいなら、自分でthrowErrorをする方が良いです。 これがエラー処理評価をする最終バージョンです。
eval2 :: Env -> Exp -> Eval2 Value eval2 env (Lit i) = return $ IntVal i eval2 env (Var n) = case Map.lookup n env of Nothing -> throwError ("unbound variable: " ++ n) Just val -> return val eval2 env (Plus e1 e2) = do e1' <- eval2 env e1 e2' <- eval2 env e2 case (e1', e2') of (IntVal i1, IntVal i2) -> return $ IntVal (i1 + i2) _ -> throwError "type error in addition" eval2 env (Abs n e) = return $ FunVal env n e eval2 env (App e1 e2) = do val1 <- eval2 env e1 val2 <- eval2 env e2 case val1 of FunVal env' n body -> eval2 (Map.insert n val2 env') body _ -> throwError "type error in application"
NOTE
Control.Monad.ErrorモジュールはthrowErrorで生じたエラーを捕まえるように、別の関数も提供しています。 それが、catchError :: m a -> (e -> m a) -> m aで、任意のエラーモナドに使えます。 局所的なエラー処理にもエラーを上位に渡すことにも使えます。
2.3 環境を隠す(Hiding the Environment)
評価関数の定義をもっといいものにする一つの方法は、すべての関数定義と呼び出しから環境を隠すことです。 環境が展開される場所は一カ所(関数適用)だけ、実際に使われる場所は二ヶ所(変数とラムダ式)だけなので、他の場所では環境を隠せばコードの量を減らすことができます。 これはリーダーモナドを実装するためにReaderTモナドトランスフォーマーを加えれば実現できます。 Readerモナドは、値をその下のすべての計算に渡します。 この値は内側の計算から読むことができ、入れ子になった計算の中では改変することもできます。 状態(State)モナド(2.4節で紹介されます)と異なり、カプセル化された計算は上位の計算で使われた値を改変することができません。
以前のモナド単にをReaderT構築子で包むことから始めます。
type Eval3 a = ReaderT Env (ErrorT String Identity) a
実行関数runEval3は、初期環境を与える必要があるので少し変更しなければなりません。 評価関数から環境引数を削ることが目的です。
runEval3 :: Env -> Eval3 a -> Either String a runEval3 env ev = runIdentity (runErrorT (runReaderT ev env)) eval3 :: Exp -> Eval3 Value eval3 (Lit i) = return $ IntVal i eval3 (Var n) = do env <- ask case Map.lookup n env of Nothing -> throwError ("unbound variable: " ++ n) Just val -> return val eval3 (Plus e1 e2) = do e1' <- eval3 e1 e2' <- eval3 e2 case (e1', e2') of (IntVal i1, IntVal i2) -> return $ IntVal (i1 + i2) _ -> throwError "type error in addition" eval3 (Abs n e) = do env <- ask return $ FunVal env n e eval3 (App e1 e2) = do val1 <- eval3 e1 val2 <- eval3 e2 case val1 of FunVal env' n body -> local (const (Map.insert n val2 env')) (eval3 body) _ -> throwError "type error in application"
サンプルを実行するなら、以下のように評価してください。
runEval3 Map.empty (eval3 exampleExp)
現在の環境が必要になる場所では、Readerモナドの隠れた状態からask関数を使って引き出します。 関数適用の場合では、local関数が再帰的呼び出しの環境を変更するのに使われています。 localは(r -> r) -> m a -> m a型を持っていて、現在の環境から、第二引数にある入れ子になった計算で使われる環境へのマップをする関数を渡す必要があります。 この場合では、入れ子になった環境は現在の環境に直接依存していないので、const関数に渡すだけです。
NOTE
askに加え、asks関数が予め定義されていて環境から値へとマッピングする関数を取ります。 これは、レコードセレクター関数(record selector function)にasksを適用して環境のそれぞれの構成要素を取り出すのに使うこともできます。 (訳注:レコードセレクター関数とは、データ型をrecord syntaxで定義した際にコンパイラが自動的に作るフィールドの値を取得する関数のことです。)
2.4 状態を加える(Adding State)
重要なモナドの応用例として、純粋関数型のプログラムに変更可能(mutable)な状態を可能にすることが挙げられます。 これはStateモナドを用いればよく、初期状態の設定・現在の状態の問い合わせ・変更といった操作を提供してくれています。
例として、このインタープリタにプロファイリング機能を付け足したいとしましょう。 最も内側のモナド(Identity)をStateT構築子で包んで新しいモナドを定義します。 (StateモナドとErrorモナドに関しては、後で見るように構築子の順番が問題になります。) 例では単純な整数値を状態として保持しますが、どんなデータ型の値でも可能です。 通常は、目下の仕事に必要十分な状態を、記録として保持することになります。
type Eval4 a = ReaderT Env (ErrorT String (StateT Integer Identity)) a
runEval4関数の返り値の型は変わります。 最終的な状態が評価結果(エラーか計算値)と一緒に返されるからです。 さらに、初期状態を追加の引数で渡し、例えば別の計算の最後の状態から再度計算を始めるなどができるように、柔軟性を持たせています。
runEval4 :: Env -> Integer -> Eval4 a -> (Either String a, Integer) runEval4 env st ev = runIdentity (runStateT (runErrorT (runReaderT ev env)) st)
単純な例として、評価のステップ数を数えるだけにしたいと思います。 つまり、eval4関数を呼んだ回数です。 状態の変更は、すべて小さな補助的関数tickの中で起き、隠された状態を計算から取り出し、カウンターを増やして、状態を戻します。 この先の節で再利用するつもりなので、もちろんtickの型はEval4 ()ではありません。 ですので、中でtickが使われるモナドは状態モナド(訳注:この場合MonadState型クラスのこと)で、そのモナドの中で操作される状態は(+)演算子を使えるように数値にすると言うにとどめます。
tick :: (Num s, MonadState s m) => m () tick = do st <- get put (st + 1)
それぞれの場合でtick関数の呼び出しを加えれば、適用の回数を数えることができます。
eval4 :: Exp -> Eval4 Value eval4 (Lit i) = do tick return $ IntVal i eval4 (Var n) = do tick env <- ask case Map.lookup n env of Nothing -> throwError ("unbound variable: " ++ n) Just val -> return val eval4 (Plus e1 e2) = do tick e1' <- eval4 e1 e2' <- eval4 e2 case (e1', e2') of (IntVal i1, IntVal i2) -> return $ IntVal (i1 + i2) _ -> throwError "type error in addition" eval4 (Abs n e) = do tick env <- ask return $ FunVal env n e eval4 (App e1 e2) = do tick val1 <- eval4 e1 val2 <- eval4 e2 case val1 of FunVal env' n body -> local (const (Map.insert n val2 env')) (eval4 body) _ -> throwError "type error in application"
サンプルを評価すると、
(Right (IntVal 18), 8)
と出ます。これは評価が成功して整数18を返し、簡約が8ステップだったということを表しています。
NOTE
Eval4モナドの型を次のように変える(StateTとErrorTを入れ替えた)と、モナドの解釈が変わります。
type Eval4' a = ReaderT Env (StateT Integer (ErrorT String Identity)) a
結果(エラーか否か)と状態を返す代わりに、エラーか結果と最終的な状態かを返します。 これは対応する実行関数の型からも分かるでしょう。
runEval4' :: Env -> Integer -> Eval4' a -> (Either String (a, Integer)) runEval4' env st ev = runIdentity (runErrorT (runStateT (runReaderT ev env) st))
最終的な状態には関与しないので、Readerモナドトランスフォーマーの位置は問題にはなりません。
NOTE
Stateモナドはgets関数も提供しており、結果を返す前に状態に対して関数を適用します。 内部状態に関数を適用することによって状態を変更するmodify関数もあります。
2.5 ログを加える(Adding Logging)
ここで述べる道具箱にある最後のモナドトランスフォーマーはWriteTです。 ある意味ReaderTの対になるものです。WriterTが提供する関数は、渡された値を用いるものではなく、計算結果に値を追加するものだからです。
type Eval5 a = ReaderT Env (ErrorT String (WriterT [String] (StateT Integer Identity))) a
StateTの場合と同様に、結果を出力するものなのでWriterTはErrorTと干渉します。 ErrorTとWriterTの順番によって、エラーが起きたときに、結果が(訳注:Writerモナドの)値を返すかどうかが変わります。 書き出される値、は文字列のリストです。 WriterTモナドトランスフォーマーのドキュメントを読むと、書きだされた値の型はモノイド(Monoid)型クラスのメンバーに制限されることがわかるでしょう。 このことは、このクラスのメソッドは内部的に初期値を構成したり、書き出される値を合成したりするのに使われるため必要なことです。
実行関数は先ほどと同じように拡張されます。
runEval5 :: Env -> Integer -> Eval5 a -> ((Either String a, [String]), Integer) runEval5 env st ev = runIdentity (runStateT (runWriterT (runErrorT (runReaderT ev env))) st)
評価関数では、評価中に遭遇した変数名を書き出すことによって、Writerモナドの使い方を説明しています。
eval5 :: Exp -> Eval5 Value eval5 (Lit i) = do tick return $ IntVal i eval5 (Var n) = do tick tell [n] env <- ask case Map.lookup n env of Nothing -> throwError ("unbound variable: " ++ n) Just val -> return val eval5 (Plus e1 e2) = do tick e1' <- eval5 e1 e2' <- eval5 e2 case (e1', e2') of (IntVal i1, IntVal i2) -> return $ IntVal (i1 + i2) _ -> throwError "type error in addition" eval5 (Abs n e) = do tick env <- ask return $ FunVal env n e eval5 (App e1 e2) = do tick val1 <- eval5 e1 val2 <- eval5 e2 case val1 of FunVal env' n body -> local (const (Map.insert n val2 env')) (eval5 body) _ -> throwError "type error in application"
2.6 IOはどうすんの?(What about I/O?)
これまでのところ、ある重要な面を考慮していません。入力と出力です。 どのようにしてこれまでに開発してきたモナドの定義に、入出力を導入すればいいでしょうか。 IOモナドトランスフォーマーを定義することはできません。 なぜなら、HaskellのIO操作の実行は他の関数やモナドに勝手に入れ子にしてはならず、IOモナドでのみ可能になっているからです。 幸運なことに、モナドトランスフォーマーのライブラリは、私たちが組み上げてきたものに簡単にIO操作を導入する下地を提供してくれています。 単にIdentityの代わりにIOを使えばいいのです! これは、Identityが基になるモナドで、今まで見てきたように、このモナドのアクションを評価するrunIdentity関数は、いつでも一番最後に適用されていたからです。
type Eval6 a = ReaderT Env (ErrorT String (WriterT [String] (StateT Integer IO))) a
runEval6が返す型は、IO構築子によって包まれています。 Eval6の計算を実行することは直接的に結果を出すのではなく、結果を得るには実行しなければならないIO計算を返します。
runEval6 :: Env -> Integer -> Eval6 a -> IO ((Either String a, [String]), Integer) runEval6 env st ev = runStateT (runWriterT (runErrorT (runReaderT ev env))) st
eval6関数では、ちょっとしたひと手間でIO操作を行えます。 それには、liftIO関数を使って操作を呼び出す必要があり、これでIO計算を現在実行中のモナドに持ち上げることができます。 例として、整数定数が評価される度にプリントすることにします。(これが良い方法だとは思いませんが、要点を説明できますし、良いデバッグのテクニックになることもあります。)
eval6 :: Exp -> Eval6 Value eval6 (Lit i) = do tick liftIO $ print i return $ IntVal i eval6 (Var n) = do tick tell [n] env <- ask case Map.lookup n env of Nothing -> throwError ("unbound variable: " ++ n) Just val -> return val eval6 (Plus e1 e2) = do tick e1' <- eval6 e1 e2' <- eval6 e2 case (e1', e2') of (IntVal i1, IntVal i2) -> return $ IntVal (i1 + i2) _ -> throwError "type error in addition" eval6 (Abs n e) = do tick env <- ask return $ FunVal env n e eval6 (App e1 e2) = do tick val1 <- eval6 e1 val2 <- eval6 e2 case val1 of FunVal env' n body -> local (const (Map.insert n val2 env')) (eval6 body) _ -> throwError "type error in application"
3 結論(Conclusion)
モナドトランスフォーマーは関数型プログラマーの持てる強力なツールです。 このチュートリアルでは、現在のHaskellの実装で利用できるモナドトランスフォーマーと、簡単なインタープリタを作るのにそれらをどうやって組み合わせるかを紹介しました。
ここでは、現在実装されているモナドトランスフォーマーを全てはカバーしていません。(例えば継続やリストのモナドトランスフォーマー) もっと情報を得るために、Haskellのウェブサイトにあるライブラリのドキュメントを読むこともおすすめします。
モナドトランスフォーマーを使うことで、今のアプリケーションでやりたいことを、一枚岩でお手製のモナドにまとめ上げ、様々なアプリケーションに特化したモナドをいとも簡単に作り出すことができます。
Happy hacking in Haskell!!
References
- [1] GHC Developers. Glasgow Haskell Compiler Homepage. Available from: http://www. haskell.org/ghc, 2008. Last visited: 2008-10-07.
- [2] Mark P. Jones. Functional programming with overloading and higher-order polymorphism. In First International Spring School on Advanced Functional Programming Techniques, vol- ume 925 of Lecture Notes in Computer Science, pages 97–136, B ̊astad, Sweden, May 1995. Springer-Verlag.
第6回 スタートHaskell2 で発表してきました
2012年の11月18日日曜日に神保町で行われたスタートHaskell2の第6回で、モナドについて発表してきました。
当日の朝まで資料作って、お布団との癒着により家を出るのがギリギリになって、バス停で財布を忘れてきたことに気がついて取りに戻ったのでギリギリアウトになり、最近買い換えたiPhone5のマップのせいで神保町駅の出口を間違えバッチリアウトになるなどして、@hirataraさんにわざわざビル出入口の自動ドアを開けてもらうなどアレな感じながらも発表してきました。
当日の資料はSlideShareのモナドがいっぱい!にあります。 そして発表中に気がついたのですが、資料にいくつか間違いがあったので訂正します(´д⊂)‥
スライド15枚目[Applicative復習]の(<*>)
の定義が間違ってますね。
-- 誤 (<*>) :: f (a -> b) -> a -> b -- 正 (<*>) :: f (a -> b) -> f a -> f b
あとは67枚目[「選択」のmplus(Maybe)]でもやらかしてます。 上から2番目の実行例が間違ってます。
-- 誤 > Just 1 `mplus` Nothing Nothing -- 正 > Just 1 `mplus` Nothing Just 1
たぶん以上二つです。混乱した型ごめんなさい。
あと@aomoriringoさんのFunctor, Applicative, Monadのイメージ、とっても良いと思います。
FunctorとApplicativeとMonadのイメージってこんな印象を受けたんですがどうなんだろう。 #start_haskell URL
2012-11-18 15:10:00 via web
また、当日の様子などは@koieさんがまとめてくださってるので、そちらをご覧下さいませ。
ブログ: koie blog : いってきた: 第6回 スタートHaskell2
Togetter: 第6回スタートHaskell2 - Togetter
と、いろいろありましたが、後の懇親会でもなんかよく分かんない話(?)ができ、発表に関しても分かりやすかったと言ってくださった方々もいて、keynote買ってがんばって資料を用意した甲斐があったなぁと思った次第で有ります。 でも発表の練習など出来なくて、結構スライドが前後しつつ話してたので、あの辺り何とかしないといけません。 またひとつモナドチュートリアルを増やす形になりましたが、誰かの助けになれば幸いです。
Haskellのパッケージ管理について調べてみた
Haskellやって1年ちょっと経つわけですが、Haskellで使うpackageの管理についてよく知らなかったので色々調べてみました。
対象としては最近Haskellを始めた方やpackage管理についてよく知らないという方向けです、 packageを利用する側からの説明なので、作るにはどうしたらいいかは書いてません(`・ω・´)ゞ
そして、やたら長いのです。
ちなみにghc-7.0.3で、OSはLinux Mintです。
packageって?
packageはHaskellのライブラリを構成する一つのまとまりで、packageはいくつかのmoduleをまとめています。
moduleはだいたい.hs拡張子のHaskellプログラム1ファイルと対応していて、module Foo.Bar where
とファイルの上の方に書いてあるFoo.Barがモジュール名です。
Haskell Platformに含まれる標準的なpackageはココを見てみましょう。 え、Haskell Platformを入れてない? 入れましょう(゚Д゚)
右側にある haskell98-2.0.0.1
とかってヤツですね(・・)
packageはそのパッケージから利用できるmoduleがあって、あるパッケージのモジュールを使いたくなったらプログラムからimportするわけです。
どんなモジュールが使えるかは、次のようにghc-pkg
コマンドを使うことで調べられます。
textパッケージから利用できるモジュールを調べてみると、
% ghc-pkg field text exposed-modules
exposed-modules: Data.Text Data.Text.Array Data.Text.Encoding
Data.Text.Encoding.Error Data.Text.Foreign Data.Text.IO
Data.Text.Internal Data.Text.Lazy Data.Text.Lazy.Builder
Data.Text.Lazy.Builder.Int Data.Text.Lazy.Builder.RealFloat
Data.Text.Lazy.Encoding Data.Text.Lazy.IO Data.Text.Lazy.Internal
Data.Text.Lazy.Read Data.Text.Read
となりますね。同じパッケージでも複数のバージョンがあるとそれも出てきます。
ghc-pkg
コマンドは他にもいろいろ機能がありますが、それも追って紹介します。
cabalって?
Haskellのパッケージのメタ情報や管理などをしている一つのエコシステムです。
主な役者は、
- Cabalパッケージ
- cabal-installパッケージ
- .cabalファイル
- cabalコマンド
などです。
各パッケージのメタ情報は<package名>.cabal
なるファイルに収められていて、ココに依存関係やらバージョンやらビルド方法など様々な情報が収められています。
Hackageにあるパッケージ群にはこのファイルが付属していて、こいつを使って管理されているようです。
Haskellのパッケージ管理は主にcabal
コマンドを通して行います。
cabal
コマンドはcabal-installパッケージが提供するCabalパッケージへのインターフェースです。
え?よくわからん?私もですъ(゚Д゚)
ちなみにcabal-installパッケージの依存先を見てみると、
HTTPとかunixとかnetworkとかのパッケージに依存しててイカにも外部と通信してパッケージを取ってインストールしますよ〜ってな感じですね! もちろんCabalパッケージにも依存しているようです。
Cabalパッケージの依存関係はシンプルです。
まぁHaskell Platformを入れていればcabal
コマンドは使えるはずなので、特に問題ないはずです。
cabalを使えばHackageからパッケージをインストールしたり、自分の作ったパッケージをビルドしたりHackageへアップロードするのが簡単になります。助かります。
使い方
Hackageから特定のパッケージをインストールしてくるには、cabal install
コマンドを使います。
が、そのまえに、Hackageにある最新のパッケージの情報とローカルの情報を同期させるため、
% cabal update
をしましょう。
それから、例えばconduitパッケージをインストールしたいなら、
% cabal install conduit
と打てば、conduitの最新版がインストールされます。 インストールするバージョンの指定もできます。
% cabal install conduit-0.5.2.4
各パッケージの情報は、
% cabal info conduit
とすればパッケージの説明や、現在インストールされているバージョンなどが見られます。 利用可能なモジュール名もここで表示されますね:-)
自分でHaskellのプロジェクトを作るには、
% cabal init
を使えば、対話的に自分のパッケージ管理のための.cabalファイルが生成されます。
自分のパッケージの依存関係を解決してインストールするには、.cabalファイルのあるディレクトリで
% cabal install
とすればOKです。Hackageから自動的に依存しているパッケージを集めてビルドしてくれます。
依存している必要なパッケージが一度入ってしまえば、次からはcabal build
をするだけです。
しかしこのcabal
コマンドはなぜだかインストールしたパッケージを削除する機能が無いので、その辺使い勝手が良いcab
を使うと良いかもしれませんねъ(゚Д゚)
cab: A maintenance command of Haskell cabal packages
他にも使うパッケージをプロジェクトで共有したくないよ〜って時はcabal-devが便利です。 パッケージの依存関係が他と干渉してメタメタになることはたまによくある(?)ことなので、こういうのも便利ですね!
他にもcabal-installの代替や補佐役のパッケージは色々ありますよ。
packageはどこに?
インストールして利用できるようになったはずのパッケージはどこにあるんでしょう?
これは、% ghc-pkg list
で一覧で出てきます。
% ghc-pkg list
/var/lib/ghc-7.0.3/package.conf.d
Cabal-1.10.1.0
GLUT-2.1.2.1
HTTP-4000.1.1
HUnit-1.2.2.3
OpenGL-2.2.3.0
QuickCheck-2.4.1.1
....
/home/kenta/.ghc/x86_64-linux-7.0.3/package.conf.d
Cabal-1.14.0
HUnit-1.2.4.2
ListLike-3.1.6
MissingH-1.1.1.0
MonadCatchIO-transformers-0.3.0.0
PSQueue-1.1
QuickCheck-2.4.2
SHA-1.5.0.1
SafeSemaphore-0.7.0
attoparsec-conduit-0.4.0.1
....
適当に% cabal install
しまくってるとズラズラ〜っとたくさん出てきます。こんな感じでインストールされたパッケージの名前とバージョンが表示されますね。
上に表示された/var/lib/ghc-7.0.3/package.conf.d
は、システムにインストールされたパッケージの情報を持っているディレクトリです。Haskell Platformをインストール先を指定せずインストールするとこんな感じのところに入ると思います。
その下の/home/kenta/.ghc/x86_64-linux-7.0.3/package.conf.d
は、ユーザーの領域にインストールされたパッケージですね。% cabal install
でインストールしたパッケージは普通こっちの管理下に入ります。なので普通に% cabal install
するにはsudoは要らないわけです:-)
それでは、package.conf.d/ディレクトリの中を見てみましょう。
% ls /var/lib/ghc-7.0.3/package.conf.d
Cabal-1.10.1.0-e951c182da4a22a7b82c0f2e4be13b7b.conf
GLUT-2.1.2.1.conf
HTTP-4000.1.1.conf
HUnit-1.2.2.3.conf
OpenGL-2.2.3.0.conf
QuickCheck-2.4.1.1.conf
X11-1.5.0.0.conf
X11-xft-0.3.conf
array-0.3.0.2-143060371bda4ff52c270d1067551fe8.conf
base-4.3.1.0-91c3839608ff4d3ec95f734c5ae4f31c.conf
bin-package-db-0.0.0.0-03f52950478226c0334a7d7e83d56e17.conf
....
おぉ、パッケージの設定ファイルみたいなのが、たくさんありますね!
package.conf.d
ってのはpackage configuration directoryの略でしょうかね。
一つ設定ファイルの中身を見てみましょう。中身は人間が読めるようになっています。 (長いので、一部省略)
% cat zlib-0.5.3.1.conf
name: zlib
version: 0.5.3.1
id: zlib-0.5.3.1-7e19941cbd00147a79723e25160ffc8b
license: BSD3
copyright: (c) 2006-2008 Duncan Coutts
(....)
exposed-modules: Codec.Compression.GZip Codec.Compression.Zlib
Codec.Compression.Zlib.Raw Codec.Compression.Zlib.Internal
hidden-modules: Codec.Compression.Zlib.Stream
import-dirs: /usr/lib/haskell-packages/ghc/lib/zlib-0.5.3.1/ghc-7.0.3
library-dirs: /usr/lib/haskell-packages/ghc/lib/zlib-0.5.3.1/ghc-7.0.3
hs-libraries: HSzlib-0.5.3.1
extra-libraries: z
extra-ghci-libraries:
include-dirs:
includes: zlib.h
depends: base-4.3.1.0-91c3839608ff4d3ec95f734c5ae4f31c
bytestring-0.9.1.10-6aa1efbfa95d1689fc03d61e7c4b27c4
(....)
なるほど、「フィールド名: 内容」の形式で、バージョンやどんなモジュールが利用可能か、依存関係なども書いてありますね。
先ほど紹介した% ghc-pkg field <package名> exposed-modules
というヤツは、実はこのファイルのフィールドを指定して、それを表示させているだけでした。
つまり、% ghc-pkg field <package名> <フィールド名>
となってます。
ですので、% ghc-pkg field zlib depends
とやれば、
% ghc-pkg field zlib depends
depends: base-4.3.1.0-91c3839608ff4d3ec95f734c5ae4f31c
bytestring-0.9.1.10-6aa1efbfa95d1689fc03d61e7c4b27c4
と、どのフィールドでもフィールド名を指定してやれば見られるわけです。
実際には、.confファイルをキャッシュしているpackage.cacheファイルを見に行っています。straceがあれば、
% strace ghc-pkg field zlib depends 2>&1 1>/dev/null | grep open
などでわかりますね。
@shelarcyさんに実際のコードの位置を教えて頂きました。ありがとうございます。( ;∀;)
readPackageConfig :: DynFlags -> FilePath -> IO [PackageConfig] readPackageConfig dflags conf_file = do isdir <- doesDirectoryExist conf_file proto_pkg_configs <- if isdir then do let filename = conf_file </> "package.cache" debugTraceMsg dflags 2 (text "Using binary package database:" <+> text filename) conf <- readBinPackageDB filename return (map installedPackageInfoToPackageConfig conf) # (省略)
確かにpackage.cacheを読みに行ってますね!
そして、パッケージの実体がどこにあるのかというと、import-dirs
やlibrary-dirs
フィールドに書いてあるようです。
そこをfind
で覗いてみると。。。
% find /usr/lib/haskell-packages/ghc/lib/zlib-0.5.3.1/ghc-7.0.3/ -type f -printf '%P\n'
HSzlib-0.5.3.1.o
Codec/Compression/Zlib/Raw.hi
Codec/Compression/Zlib/Stream.hi
Codec/Compression/Zlib/Internal.hi
Codec/Compression/Zlib.hi
Codec/Compression/GZip.hi
libHSzlib-0.5.3.1.a
おぉ、オブジェクトファイル(.o)やらインターフェースファイル(.hi)もありますね!
実際ghciで試してみると、
% ghci
GHCi, version 7.0.3: http://www.haskell.org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> :m Codec.Compression.GZip Data.ByteString.Lazy.Char8
Prelude Codec.Compression.GZip Data.ByteString.Lazy.Char8> unpack . decompress . compress . pack $ "Compress me!"
Loading package bytestring-0.9.1.10 ... linking ... done.
Loading package zlib-0.5.3.1 ... linking ... done.
"Compress me!"
最後から2番目の行にある通り、ちゃんとzlibパッケージをロードして、リンクしてるらしいことがわかりますね。 *1
ココまではシステム(global)に入ったパッケージについて述べて来ましたが、ユーザー領域でも大体一緒のようです。
インストールされたパッケージは、先程調べたように/home/kenta/.ghc/x86_64-linux-7.0.3/package.conf.d
にありました。
内容も一緒ですが、ここにはcabal
で入れたものの情報が入ります。
コンパイル済みの実体は、~/.cabal/lib
以下に同じように収まってますね。
~/.cabal/bin
には、cabal install
などで入れたプログラムの実行ファイルがあります。cabal install
でインストールしたプログラムを使うなら、ココへのパスを通しておきましょう:-)
先ほど紹介したcabal-dev
やcab
などの実行ファイルも~/.cabal/bin
にありました。
それで、今までhackage.haskell.orgのレポジトリしか使ってませんでしたが、実はミラーがあります。 本家hackage.haskell.orgはちょくちょく落ちるので、ミラーを使ってみたりするのも良いかもしれません。
- hdiff.luite.com
- hackage.haskell.biz
はすぐ見つけられたのですが、他はよくわかりません(`・ω・´)ゞ
cabal
コマンドでこれらのミラーを使うには、~/.cabal/config
ファイルのremote-remo
を編集すれば、リポジトリを切り替えられます。
最初は本家の
remote-repo: hackage.haskell.org:http://hackage.haskell.org/packages/archive
になっているので、この行をコメントアウト(--)して、
-- remote-repo: hackage.haskell.org:http://hackage.haskell.org/packages/archive
remote-repo: hackage.haskell.biz:http://hackage.haskell.biz
とすればOKです。
試しに% cabal update
をしてみると、
% cabal update
Downloading the latest package list from hackage.haskell.biz
ちゃんとミラーに取りに行ってますね!
ちなみに、取ってきたパッケージの情報は、~/.cabal/packages/<リポジトリのホスト名>/00-index.tar
に収められています。
本家レポジトリなら、~/.cabal/packages/hackage.haskell.ord/00-index.tar
ですね。
このtarファイルの中身は、% tar -tf 00-index.tar
をしてみると、
% tar -tf 00-index.tar | head
4Blocks/0.1/4Blocks.cabal
4Blocks/0.2/4Blocks.cabal
AC-Angle/1.0/AC-Angle.cabal
AC-Boolean/1.0.0/AC-Boolean.cabal
AC-Boolean/1.1.0/AC-Boolean.cabal
AC-BuildPlatform/1.0.0/AC-BuildPlatform.cabal
AC-BuildPlatform/1.1.0/AC-BuildPlatform.cabal
AC-Colour/1.1.1/AC-Colour.cabal
AC-Colour/1.1.2/AC-Colour.cabal
AC-Colour/1.1.3/AC-Colour.cabal
.cabalファイルがただひたすら入ってます。凄く...たくさんです...///
長くなってきたので一旦ここまで
なんだかやたらと長くなってきたので一旦ここまでにします。
そのうちある程度まとまったらまた書きます(^m^;)
YAPC::Asia 2012でスタッフしてきた(2回目)
YAPC::Asia 2012で去年に引き続き今年もスタッフしてきましたΣ(゚∀゚ノ)ノ
去年まではずっと大岡山の東工大でやってきたようですが、今年は私の本拠地であります東大本郷キャンパスでした。
YAPC::Asiaとは私にとってLarry Wallやネットでしか見たことないPerl Mongersを遠くから眺めてニヤニヤするイベントなわけで、今年もたくさんの人たちを見に行きました。
前夜祭・1日目・1日目と主にメインホールでスタッフしつつ特に面白かったトークを少しご紹介します。
Masaaki Goshima - Perlと出会い、Perlを作る - YouTube
perlを深く知るために、perlの処理系を作ってしまうというお話です。
処理系を作って高速化する上での苦労や工夫点なんかを紹介していて、半年後ぐらいにはmixiで動かしたいとおっしゃってました。
@dankogaiさんや@miyagawaさんも質問されていて、注目度は高いです *1
スポンサードセッション LT - 株式会社スカイアーク - YouTube
スポンサードセッションのLTですが面白い!!北の大地の普通の民家で会社をやっているわけで。。。
TAKESAKO LT - Perl 1+1 耐久テスト - YouTube
@takesakoさんのLT、サーバサイドの処理系のガチンコ対決であります。安定の面白さです。
私の見た中で一番の盛り上がりを見せておりました。
最強なのはもちろんアイツです。わかります。
3日間、いつの間にか終わっていましたけどとにかく楽しかった。
きっと、来年も参加します!できれば、トークしたい!!
YAPC::Asiaは、原点です。
*1:o(´∀`)o