cabalでビルドをカスタマイズする
cabalファイルだけでは足りない事がある。どうしても単一のプログラムではできないこともあるし、TemplateHaskellでも生成できなくてやむなくソースコードを自動生成するスクリプトを書かざるを得ない事もあります。自動生成なんかやると何かに負けた気分になりますが、cabalでなければ普通にやっている事なので、何も問題は無い気がします。MakefileとかAntとか。
具体的には、
(1) Haskell製コード生成プログラムをビルド
(2) シェルスクリプト経由でコード生成プログラムを実行
(3) 本体のプログラムをビルド
という事をやりたい。
ところがcabalには直接的に外部のコマンドを実行してビルドする方法が無いみたいです。無いみたいなんですが、ビルドを細かく制御する方法はいくつか用意されています。
(A) Build-Type: Configureでautoconfで頑張る
(B) Build-Type: MakeでMakefileを書いて頑張る
(C) Build-Type: CustomでSetup.hsを書いて頑張る
はっきり言って(A)で何でもできるんですが、それなりに面倒くさい。(B)は楽そうだけど、全部のターゲット(http://hackage.haskell.org/package/Cabal-1.20.0.2/docs/Distribution-Make.html)を定義するのは結構面倒だし、そんなに色々出来る必要は無い。cabalの機能もそれなりに使いたいし。どうでもいいけど、Makeのときにcabal configureの時にconfigureファイルが無いとコケるのはバグかな。空のconfigureファイルがあればその後は問題なく通っちゃうし、configureスクリプト使ってないっぽいのでバグっぽい。誰も使ってないんだろうか。まあ(A)と(C)があれば使わないよな。
そんなわけで、余程の事がなければCustomで足りると思うのでCustomを使って行こうと思います。
ここで、こんな感じのプロジェクトを作ったと仮定します。この例はHaskellのCabalのマニュアルからのコピペ(http://www.haskell.org/ghc/docs/7.0.3/html/Cabal/authors.html)のBuild-TypeをCustomに変えただけのものです。
Name: TestPackage Version: 0.0 Cabal-Version: >= 1.2 License: BSD3 Author: Angela Author Synopsis: Small package with two programs Build-Type: Custom Executable program1 Build-Depends: HUnit Main-Is: Main.hs Hs-Source-Dirs: prog1 Executable program2 Main-Is: Main.hs Build-Depends: HUnit Hs-Source-Dirs: prog2 Other-Modules: Utils
今回はこれを、program1をビルドするためにprogram2とシェルスクリプト(build.sh)が必要だということにしよう。
これに加えて、cabal initした時に作られるSetup.hsがあります。
こんなの。
import Distribution.Simple main = defaultMain
このままの場合は、Build-Type: Simpleの時と全く同じ動きをします。
ちなみにimportの部分をDistribution.Makeに変えるとBuild-Type: Makeと同じ動きになります。
つまり、cabalコマンドを実行した時にこのmainから始まるプログラムが実行されるわけなので、例えば
main = return ()
などとやると何をしても何も起こらなくなります。いや、helpは大丈夫ですけど。
じゃあconfigureとかbuildとかtestの判別はどうやってるんだというと、単にdefaultMainの中でgetArgsしているだけなので、
import System.Environment main = getArgs >>= print
なんてやってみると、このSetup.hsというものがどれだけ何でもアリなのかがわかるんじゃないかな。ただ残念ながらcabalに定義されてないサブコマンドを作る事まではできません。
というわけで、getArgsの中身を見ながら処理を書いてもいいんですが、それだとMakefileと変わりません。ここではcabalが知り得る色々な情報を用意してくれるCabalパッケージのライブラリを使って処理を書くことができます。
Cabal: A framework for packaging Haskell software | Hackage
具体的には、例えばbuildサブコマンドの前に処理を挟みたい場合はこう
import Distribution.PackageDescription (HookedBuildInfo) import Distribution.Simple import Distribution.Simple.Setup (BuildFlags) main :: IO () main = defaultMainWithHooks simpleUserHooks{preBuildHook = hook} hook :: Args -> BuildFlags -> IO HookedBuildInfo hook args flags = ...
何ができるかは、おおむねUserHooks型のマニュアルを見ればわかるかと思います。
http://hackage.haskell.org/package/Cabal-1.20.0.2/docs/Distribution-Simple.html#t:UserHooks
これの中の、例えばbuildHookという、これはhookというよりはbuildコマンドを実行した時に行われる処理そのものなんですけど、そのbuildHook関数の定義がこうなってる。
buildHook :: PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()
この中で、
- PackageDescriptionは*.cabalファイルの中の全体に関わるNameとかDescriptionの部分
- LocalBuildInfoはExecutable節とかLibrary節みたいな部分
- UserHooksは定義したUserHooksそれ自身
- BuildFlagsはサブコマンドに渡したターゲットやオプション
になっています。
まるっきり、なんの工夫もなく、cabalの実行時に得られる情報そのものです。
そのものなので、例えばbuildコマンド実行時にinstallを実行したりもできます。
import Distribution.PackageDescription (HookedBuildInfo) import Distribution.Simple import Distribution.Simple.Setup (BuildFlags, defaultInstallFlags) main :: IO () main = defaultMainWithHooks simpleUserHooks{buildHook = hook} hook :: PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO () hook desc info hooks _flags = (instHook hooks) desc info hooks defaultInstallFlags
これでbuildだけを実行することはできなくなり、buildするとinstallを必ず実行するようになりますが、installコマンドを実行すると無限ループするので使い物にはなりません。
ここで最初の例に戻ります。
program1をビルドするためには(1)program2をビルドして(2)build.shを実行した後に(3)program1をビルドする必要があります。
まあ、これだけの情報があればこれくらいの処理は簡単に作れそうに見えます。
ところが、これが意外に面倒くさい。
hooksに渡される情報はあくまでもコマンドに渡される引数とcabalファイルの情報だけなので、これからビルドするターゲットがどれかという情報は含まれていない。program1をビルドしようとしているのかprogram2をビルドしようとしているのかの区別は存在しないのです。
なので、(3)program1をビルドする前に(1)program2をビルドして(2)build.shを実行する、みたいな順序付けは非常に面倒くさいのです。順序付けならcabalのBuild-Tools: program2でできますけど、プログラムの実行ができない。
ただ、build処理内でprogram1とprogram2の区別はされていないものの、一応何を実行しようとしているかは知ることができます、BuildFlagsのbuildArgsで。
http://hackage.haskell.org/package/Cabal-1.20.0.2/docs/Distribution-Simple-Setup.html#t:BuildFlags
このフィールドには、buildサブコマンドに渡されたターゲットが格納されています。
% cabal build program1
を実行すれば["program1"]が、
% cabal build program1 program2
を実行すれば["program1", "program2"]が、
% cabal build
を実行すれば[]が入ってきます。
お察しの通り最後のが厄介で、buildはターゲットを指定しないと全てのターゲットをビルドするんですが、その時は空リストが格納されています。でも落ち着いてPackageDescriptionのexecutablesから全ターゲットのリストが取れるので、それを使えばいいと思います。こんな感じで。
import Distribution.PackageDescription (HookedBuildInfo, executables, exeName) import Distribution.Simple main :: IO () main = defaultMainWithHooks simpleUserHooks{buildHook = \desc _ _ _ -> print (map exeName (executables desc))}
あとは、buildArgsの中身を見ながら、program1がビルドされるかどうかを判断してprogram2をビルドしたり、System.Processモジュールを使って外部コマンドを実行したりすればいいんじゃないかと思います。最後は力技です。面倒くさいです。
さらに面倒なことに、これら面倒な事をパッケージ化してライブラリとして公開したとしても、cabalにはSetup.hsをビルドするための依存ライブラリを記述する機能が無いので、そのライブラリが既にglobalかsandboxにインストールされていないと本体(program1とか)のビルドがコケる。
これなんとかならないのかな。もはやCabalに機能追加するしか無い気がする。
いやtargetのどれかのBuild-Dependsに書けば大丈夫なんですけど、targetに関係無い依存関係書きたくないよねぇ。私が使っているのは今のところprocessとdirectoryで、これらは本体でも使っているので問題は起きていませんけど。悩ましい。
haskell-relational-recordとMySQLの識別子の大文字小文字
haskell-relational-record(hrr)とMySQLを使ってプログラムを書いてみようと思った。
書き方自体はこのあたり。
https://github.com/khibino/haskell-relational-record
https://github.com/krdlab/haskell-relational-record-driver-mysql
元々haskell-relational-recordはPostgreSQLのために作られているので(訂正:IBM-DB2 + ODBCで動かしていたのが最初だそうです)、MySQLから使おうとするとちょっと詰まるのかもしれない。
まずdefineTableFromDBが動かなかったんですけど、それはちょっと原因がよくわからないので保留。
そもそもの問題が適当に作られたテーブルの形式だったりするわけなんですが、おおむねこういう感じになっている。
mysql> connect myapp Connection id: 126780 Current database: myapp mysql> desc USER; +--------------+--------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +--------------+--------------+------+-----+---------+-------+ | NAME | varchar(32) | NO | PRI | NULL | | | MAIL_ADDRESS | varchar(128) | YES | | NULL | | +--------------+--------------+------+-----+---------+-------+ 2 rows in set (0.00 sec)
MySQLではスキーマとデータベースの区別が無いというか、同じ物を指すっぽいので、データベース名もスキーマ名も同じ"myapp"になっている。そして、テーブル名とフィールド名を大文字のスネークケース。どこの常識だこれはという感じ。まあこれはそういうものなので仕方ないものとする。
これを元にテーブル定義を作るとこうなる。defineTableFromDBが動かないので自分で定義する。
{-# LANGUAGE TemplateHaskell, MultiParamTypeClasses, FlexibleInstances #-} module User where import Database.HDBC.Query.TH (defineTableDefault') import Database.Record.TH (derivingShow) import Language.Haskell.TH import DataSource defineTableDefault' "myapp" "USER" [ ("NAME", conT ''String) , ("MAIL_ADDRESS", conT ''String) ] [derivingShow]
でき上がるテーブルとクエリの定義はこうなる。
*Main> :i USER data USER = USER {nAME :: !String, mAILADDRESS :: !String} -- Defined at User.hs:10:1 instance Show USER -- Defined at User.hs:10:1 instance TableDerivable USER -- Defined at User.hs:10:1 instance ProductConstructor (String -> String -> USER) -- Defined at User.hs:10:1 *Main> uSER SELECT NAME, MAIL_ADDRESS FROM MYAPP.user
とてもきもちわるい。その上この時点で既に破綻している。
- データ型名が全部大文字で気持悪い
- フィールド名の'_'が消えて先頭だけが小文字になっていて気持悪い
- クエリの関数名が同様に"uSER"て何よ
- クエリの中身のデータベース名が大文字に、テーブル名が小文字になっている
この、最後のやつが困る。他はまあ気持悪いだけなのでいいのだけど。
どう困るかというと、このクエリを発行するとそんなテーブル無いよと言われる。MySQLはテーブル名の大文字と小文字を区別することがある。
MySQLのドキュメントにはこういう事が書いてある。
5.2.2. 識別子の大文字小文字の区別
MySQL において、データベースはデータディレクトリ内のディレクトリに対応しています。データベース内の各テーブルも、データベースディレクトリ内の少なくとも 1 つ(記憶エンジンによってはそれ以上)のファイルに対応しています。トリガーもファイルに対応しています。そのため、ベースとなるオペレーティングシステムで大文字と小文字が区別される場合、データベース名とテーブル名でも大文字と小文字が区別されます。
MySQL :: MySQL 5.1 リファレンスマニュアル (オンラインヘルプ) :: 5.2.2 識別子の大文字小文字の区別
つまりMySQLのデータがWindowsのファイルシステム上に置かれている場合はデータベース名やテーブル名の大文字小文字は区別しないけれど、UnixやLinuxの多くのファイルシステム上に置かれていた場合は区別しますという事。なんじゃそりゃという感じだけども、これも仕方ない。
これをなんとかしてhrrを使うためには、ソースをいじる必要がある。
問題はテーブル定義と同時に作成されるクエリ関数なわけなので、それを定義している場所を探すと、tableSQLという関数が見つかる。場所はここ。
haskell-relational-record/relational-query/src/Database/Relational/Query/TH.hs at master · khibino/haskell-relational-record · GitHub
tableSQL :: String -> String -> String tableSQL schema table = map toUpper schema ++ '.' : map toLower table
とてもわかりやすいですね。
これの"map toUpper"と"map toLower"を消してあげればいいのです。PostgreSQLでは基本的にテーブル名は小文字なのでこうなってるんだと思います。スキーマ名は忘れたけど大文字なんだっけ?
ということで、hrrはまだhackageに登録されていないので皆様各々githubからcloneして使っておられると思いますので、この辺りをいじる心理的障壁は比較的少ないんじゃないかと思います。やっちゃいましょう。branch作ってcommitしておくと本家が更新した時に追随が楽だと思います。いや検証してPull Request送れという話はもっともなんですが。
ともあれこれで一応クエリは利用可能になります。めでたしめでたし。
*Main> uSER SELECT NAME, MAIL_ADDRESS FROM myapp.USER
ところで実はMySQLはデータベース名とテーブル名についてはなんか面倒な仕様があるんですが、フィールド名の大文字小文字は区別しないので、テーブル定義のところはフィールド名を小文字のスネークケースにすると少しだけ幸せになります。
defineTableDefault' "myapp" "USER" [ ("name", conT ''String) , ("mail_address", conT ''String) ] [derivingShow]
ごらんよ、焼け石に水って感じだけども。これでも普通に使えます。
*Main> :i USER data USER = USER {name :: !String, mailAddress :: !String} -- Defined at User.hs:10:1 instance Show USER -- Defined at User.hs:10:1 instance TableDerivable USER -- Defined at User.hs:10:1 instance ProductConstructor (String -> String -> USER) -- Defined at User.hs:10:1 *Main> uSER SELECT name, mail_address FROM myapp.USER
UTCTimeをEqで検査する
UTCTimeが関わるテストをQuickCheckで書こうとしてハマった。
まずUTCTimeをランダム生成するために、UTCTimeをArbitraryのインスタンスにしようとした。
UTCTimeはDayとDiffTimeの組になっていて、DayとDiffTimeはIntegerから作れるし、IntegerはArbitraryのインスタンスなので、UTCTimeのインスタンス化はこんな風に書ける。
instance Arbitrary Day where arbitrary = ModifiedJulianDay <$> arbitrary instance Arbitrary DiffTime where arbitrary = secondsToDiffTime <$> arbitrary instance Arbitrary UTCTime where arbitrary = UTCTime <$> arbitrary <*> arbitrary
まあ別にDayとDiffTimeまでインスタンス化する必要は必ずしも無いんですけど。
で、UTCTimeに対してTextとの間で相互に変換する関数があるとする。こういうの。
fromText :: Text -> Maybe UTCTime toText :: UTCTime -> Text
fromTextは一応Maybeにしておきます。
これをQuickCheckでテストする。
main :: IO () main = hspec $ describe "test" prop "from/to text" prop_text prop_text :: Text -> Bool prop_text a = fromText (toText a) == Just a
このテストが通らないわけだ。
なぜか、prop_textの比較に使われているUTCTimeをshowで表示してみても全く同じなのに、(==)がFalseを返す。なんでや。
よく見るとDayとDiffTimeはIntegerから生成しているので、負の数の場合があり得る。
Dayの場合は0が1858-11-17になってるだけなのでいいんだけど、DiffTimeはUTCTimeの中では0-86400の間で、1日の中の経過秒数を表している。(最後の1秒はうるう秒らしい)
ただし、特にUTCTimeやDiffTimeを作る時にそういう値の制約がかかっているわけでもないので、0-86400の範囲外の整数値からも普通にDiffTimeやUTCTimeを作ることができてしまう。いやDiffTimeの時はいいんだけどUTCTimeを作る時は制約かけてくれよ。
その結果どうなるかというと、こういう感じになる。
Prelude Data.Time> UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 0) 1858-11-17 00:00:00 UTC Prelude Data.Time> UTCTime (ModifiedJulianDay 1) (secondsToDiffTime $ -86400) 1858-11-17 00:00:00 UTC
で、UTCTimeのEqのインスタンス化がこうなので
instance Eq UTCTime where (UTCTime da ta) == (UTCTime db tb) = (da == db) && (ta == tb)
まあ、showの結果が一致しててもこれじゃequalにならないよね。うん。
どうでもいいけどこれだったらderiving Eqでもよかったんじゃないかな。
ということなので、DiffTimeのArbitrary化はこうするとテストが通るようになる。
instance Arbitrary DiffTime where arbitrary = secondsToDiffTime <$> choose (0, 60*60*24 - 1)
"-1"は念のため。なくても一応動くんですけどね。
あと、実はDiffTimeは負の値にするとさっきの例のように日付が変わるんですが、増やすと日付が変わらずに時刻だけループする。けども1周しかループしない。
Prelude Data.Time> UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 86400) 1858-11-17 23:59:60 UTC Prelude Data.Time> UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 86402) 1858-11-17 23:59:62 UTC Prelude Data.Time> UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 86500) 1858-11-17 23:59:160 UTC Prelude Data.Time> UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 172800) 1858-11-17 23:59:86460 UTC
なんというか、UTCTimeのデータにDiffTimeを使いまわさない方が良かったんじゃないかなぁ。
DiffTimeに大きな値を突っ込んでも一応UTCTimeは作れるんですが、テストで使うと当たり前ですけどparseでコケますね。
Haskellで可変長引数
可変長引数を作りたかったわけではないんだけど、というか何がしたかったんだかよく覚えていないんだけど、こういうのを作った。
class Test a where test :: a -> Int test = const 0 instance Test a => Test (b -> a) where test f = 1 + test (f undefined) instance Test Int
Test型クラス。
> let c = undefined :: Int > test c 0 > let f = undefined :: Int -> Int > test f 1 > let g = undefined :: Int -> Int -> Int > test g 2
test関数に与えた関数の引数の数を数えられます。
ただし、与える関数の戻り値の型がTestのインスタンスになっていないと使えません。
これ、なんとなく面白くはあるんだけど、何の意味があるんだ。使い所もよくわかんなくなっちゃったなぁなんて思っていたんですが、printfがこの仕組で作られていました。
http://hackage.haskell.org/package/base-4.6.0.1/docs/Text-Printf.html
うわあああこれprintfじゃないか!
あとQuickCheckのpropというかTestableクラスも思いっきりこの仕組で実装されていました。
http://hackage.haskell.org/package/hspec-1.7.2.1/docs/Test-Hspec-QuickCheck.html
http://hackage.haskell.org/package/QuickCheck-2.6/docs/Test-QuickCheck-Property.html#t:Testable
誰だ使い所無いとか言ったやつ。
特にQuickCheckなんて最近も使ってるし、なんで疑問に思わなかったのか。これ作った人頭いいな。
まあそれはいいとして、
class Arg a where f :: c -> a
みたいな型クラスを定義して、
instance Arg a => Arg (b -> a) where f = ...
みたいなインスタンスを作ると可変長引数関数が作れるみたいです。
(b -> a)がArgのインスタンスなら(d -> b -> a)というか(d -> (b -> a))もArgのインスタンスになるので。
io-streams
そろそろio-streamsで遊んでみよう。いや遊んでたのは実のところ随分前なんですけど。
http://hackage.haskell.org/package/io-streams
io-streamsというのはHaskellのストリーム処理ライブラリの一つで、シンプルなのが売りなのかな。
ストリーム処理には私はよくconduitを使っていますけど、conduitと比べるとなんというか、本当にシンプルで使い勝手がいいです。
まずはHelloWorld的に、猫のようなもの。
import System.IO.Streams main :: IO () main = stdin `connect` stdout
ここで出てくるstdinとstdoutはSystem.IO.Streams.Handleに定義されてるもので、それぞれこんな感じになってる。
stdin :: InputStream ByteString
stdout :: OutputStream ByteString
で、connectはSystem.IO.Streamsに定義されてて、こんな感じ。
connect :: InputStream a -> OputputStream a -> IO ()
標準入力からの入力ストリーム(InputStream)を標準出力への出力ストリーム(OutputStream)に接続(connect)しているというだけ。
System.IO.Streams.HandleにあるhandleToInputStream/handleToOutputStreamを使って書き直すとこんな感じ。
import System.IO (stdin, stdout) import System.IO.Streams hiding (stdin, stdout) main :: IO () main = do is <- handleToInputStream stdin os <- handleToOutputStream stdout is `connect` os
ついでにHandleで書くとこんな感じ。
import System.IO main :: IO () main = do c <- hGetChar stdin hPutChar stdout c main
HandleToStreamと同じようにStreamToHandle系の関数もあるのでなんとなくストリームとIOハンドルは同じものなんだなぁというのがわかりますね。どうでもいいですけど。
ファイルから読むのはwithFileAsInput/withFileAsOutputというのがあって、withFileと同じような雰囲気で使えます。
% cat data.txt abc def ghc
みたいなファイルがあったとして
import System.IO.Streams main :: IO () main = withFileAsInput "data.txt" $ \is -> is `connect` stdout -- > "abc\ndef\nghc\n"
ファイルの中身を吐き出すだけのプログラムができる。中身はおそらくwithFileとhandleToInputStreamを組み合わせただけであろう。
System.IO.Streams.Listにはストリームをリストにしたりリストをストリームにしたりする処理が色々入っていて、おそらくよく使うのはtoListなんじゃないかなぁ。
import System.IO.Streams main :: IO () main = withFileAsInput "data.txt" $ \is -> do as <- toList is print as -- > ["abc\ndef\nghc\n"]
なんかあんまし嬉しくなかった。ストリームの一番ベースとなるデータ構造ByteStringには区切りが無いのでまあこうなるよね。
System.IO.Streams.ByteStringにはlinesという改行ごとにByteStringを区切る関数があって、まあData.List.linesとだいたい同じように動きます。
import Prelude hiding (lines) import System.IO.Streams main :: IO () main = withFileAsInput "data.txt" $ \is -> do ls <- lines is as <- toList ls print as -- > ["abc", "def", "ghc", ""]
最後改行で終わってるからなんかゴミ入ってますけど、これで意図通り。isはただのバイト列のストリームですが、lsはバイト列のリストのストリームになっています。わかりづらいですが。
InputStream/OutputStreamはMonadじゃないけど、linesとかtoListとかがIOなのでdoで連結できるし、printとかとも簡単に連携できて良い感じです。それはそうと今MBAのキーボードに水をこぼしてAのキーがすごくききづらくなりました。
bindで書くとちょっといい感じに。
main = withFileAsInput "data.txt" $ lines >=> toList >=> print -- > ["abc", "def", "ghc", ""]
別にconnectしなくてもデータ取り出せるのがなんかお手軽な気がします。ここではtoListがconnect相当をやってるんですけどね。
toList :: InputStream a -> IO [a]
モナドトランスフォーマーとか使わないでいいのが良いと思います。
InputStreamにはread/unRead/peekがある。conduitでいうところのawait/leftoverみたいなものです。conduitにpeek相当ってあったっけ?
{-# LANGUAGE OverloadedStrings #-} import Prelude hiding (lines, read) import Data.ByteString.Char8 () import System.IO.Streams main :: IO () main = withFileAsInput "data.txt" $ \is -> do ls <- lines is a1 <- read ls print a1 -- > Just "abc" unRead "xyz" ls a2 <- peek ls print a2 -- > Just "xyz" as <- toList ls print as -- > ["xyz", "def", "ghc", ""] a3 <- read ls print a3 -- > Nothing
readはストリームを消費するけどpeekは消費しない。unReadは本来は一度読んだデータを読まなかった事にする関数だけど、型が合ってれば別になんでも詰め直せる。で、toListはストリームを全部消費するのでその後に何か読もうとしても読めない。
一方、writeはデータをOutputStreamに書き出すだけです。conduitのyieldとは全然違う。
あと/dev/nullみたいなnullInput/nullOutputみたいなのもあります。nullOutputはまあいいとして、nullInputはunReadと組み合わせてスタックみたいに使えそうですね。いやそんなのリスト使えって感じですね。
データ変換、conduitでいうところのConduit相当のものは、単に
f :: InputStream a -> InputStream b
みたいな関数を作ってやればいいだけみたいなんですが、えっとこれどうやって作るんだ。みたいになりますが、おそらくそういう時のためにGeneratorというのがあります。GeneratorはInputStreamを作るための機能のようですが、こいつはMonadでMonadIOなのでInputStreamの変換にも使いやすそうです。おい、結局モナドトランスフォーマー使ってるじゃないか。
ためしに
data Member = Member ByteString deriving (Show) f :: InputStream ByteString -> IO (InputStream Member)
みたいな関数fを作ってみるとすると、
{-# LANGUAGE OverloadedStrings #-} import Control.Monad.IO.Class (liftIO) import Data.ByteString (ByteString) import Data.ByteString.Char8 () import Prelude hiding (lines, read) import System.IO.Streams data Member = Member ByteString deriving (Show) f :: InputStream ByteString -> IO (InputStream Member) f = fromGenerator . g g :: InputStream ByteString -> Generator Member () g is = do ma <- liftIO $ read is case ma of Nothing -> return () Just a -> do yield (Member a) g is main :: IO () main = withFileAsInput "data.txt" $ \is -> do ls <- lines is ms <- f ls toList ms >>= print -- > [Member "abc", Member "def", Member "ghc", Member ""]
gなんて関数が増えちゃいましたけど、だいたいこんな感じ? Generator部分(g関数)はあんましConduitと変わりませんね。一旦Generatorを返す関数を作らないと、データの繰り返し処理ができなくなる。というのもだいたいConduitと同じ。
ということでio-stream結構いいんじゃないでしょうか。
周辺ライブラリの充実度でconduitに見劣りするのもの、インタフェースが簡素で他との連携もやりやすいので、頑張ればなんとかなるんじゃないでしょうか。Networkとかattoparsecなんかは最初から入ってるから、頑張って作ろう。
とここまで書いたところでチュートリアルの存在に気づきました。Tutorialってモジュールがある……。
System.IO.Streams.Tutorial
今回作ったソースはここです。
exercises/streams at master · yunomu/exercises · GitHub
JavaコードをHaskellで書きなおすのに手こずった話
「続・アルゴリズムを学ぼう」が発売されて、校正をほんのちょっと手伝ったお礼かなんかで発売された本を頂きまして、ぼちぼち読んだりしています。
前作に比べて使いやすいというか、親しみやすいネタが多くて面白いですね。
「続・アルゴリズムを学ぼう」http://www.amazon.co.jp/dp/4048913948/
で、ちょっと暇つぶしに「続・アルゴリズムを学ぼう」に載ってるリバーシをHaskellで実装してみました。
https://github.com/yunomu/exercises/tree/master/reversi
この本に載っているプログラム例はJavaで書かれているので、要はJavaをHaskellで書きなおすという作業をしたような感じになりました。
前になんか勘違いしていた話
JavaとHaskellといえば、前に社内で"Types and Programming Languages"の読書会をやろうって話になった時にこんな事を言っている。
ここで言う型クラスというのはHaskellの型クラスのことで、見た目は割と似ていますよね。似てるんじゃないかな。型クラスってJavaでいうとこのinterfaceと似たような感じだよねと思っていたら型の専門家に「2~3ステップすればinterfaceにつながるのでまあ似てるといえば似てますね」みたいなことを言われてホゲェってなった
— Yusuke Nomura (@yunomu11) July 5, 2012
例えばEqとか、
class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool
ってなってて、いかにもインタフェースを定義している感じ。こういう振る舞いをするデータを作りたかったら、データ型を作ってこの型クラスのインスタンスにすればいいんじゃないかなと思う。
まあやってみると全然違うんですが。
Javaのinterfaceは型だけども、Haskellの型クラスは型ではないので、Javaのinterfaceみたいなノリで型クラスを使うと、徐々に無理が出てきて後悔することになる。
class ToInt a where toInt :: a -> Int instance ToInt Int where toInt = id instance ToInt Char where toInt = const (-1)
みたいな型クラスがあったとして
f :: ToInt a => [a] -> [Int] f = map . toInt a = f [2, 'a']
みたいなことはできない。リスト内の型が違うから。
b = [toInt 2, toInt 'a']
これはできる。まあ、そりゃできる。
こうしてみれば当たり前なんだけど、toInt関数の扱い方に気をつけなきゃいけないのでそれなりに面倒な場面が増えてくるというか、感覚的には「同じtoIntって関数が使えるんだから同じように扱えてもいいじゃん」って思うんだけど、シンボルというか名前が同じでもChar型のtoIntとInt型のtoIntは別物なので、別物として扱いましょう。
同じ型クラスならリストに入れられるようにしようみたいな話もどっかにあった気もしますが。
型クラスはやっぱりShowみたいに型によって処理を振り分けたい時に使うのが良さそうですね。
インタフェースが同じなら素直に同じ型にしましょう。
継承やコンストラクタやインタフェース定義
Haskellの型定義にはJavaの継承とかコンストラクタみたいなものはなくて、メソッドもなくて、どうするのこれってなります。
ならないなら幸せです。人やコードによってはならないんじゃないかな。
本ではプレイヤーをこんな風に定義してる。
public abstract class Player { protected final Turn turn; protected Player(Turn turn) { this.turn = turn; } public Turn getTurn() { return turn; } public abstract Position play(Board board); } public class HumanPlayer extends Player { ... }
抽象クラスの`Player`を定義して、それを継承してHumanPlayerとかSimpleAIPlayerとかを作ってる。
これはそのままだとHaskellに変換できないんだけど、よく考えれば具象クラスが要らないだけだったりする。コンストラクタの代わりに初期化関数を作る。
data Player = Player { playerTurn :: Turn , playerPlay :: Board -> Player -> IO (Maybe Position, Player) } initHumanPlayer :: Turn -> Player initHumanPlayer t = Player t humanPlay humanPlay :: Board -> Player -> IO (Maybe Position, Player) humanPlay = ...
Java風に言うと、テンプレートメソッドパターンがストラテジーパターンになってる。というかJavaでもこう書けよという気がしてくる。抽象クラスがいいのかストラテジーがいいのかはここでは微妙なところかもしれないけど。
戻り値の型
public abstract Position play(Board board);
Haskellのplayの型はこうなってる。
playerPlay :: Board -> Player -> IO (Maybe Position, Player)
このインタフェースはちょっと悩んだんですけど、
- Playerの内部状態を使うかもしれないから引数にPlayerが必要
- Playerの内部状態を更新するかもしれないから戻り値にPlayerが必要
- Positionが決まらない場合があるからMaybe Position(元プログラムではnullを返している)
- ユーザの入力や乱数を利用して手を決めるかもしれないからIO
という感じで決めました。
SimpleAIPlayerだとIOが要らないし、HumanPlayerだとMaybeが要らないし、もっと後の方になるまで戻り値のPlayerが要らなかったりするので、ここは先見性が試されるというか、3回くらい書き直しました。
IOが必要かどうか、値が存在しない場合があるかどうか、このあたりを最初から慎重に設計してなきゃいけないけども、このあたりは結構難しいところだったと思います。機能追加の度に共通部分を書き直す羽目になりました。よく考えれば自明なんですけど、Javaのメソッドは基本的に副作用があるというのは忘れがちな気がします。
そんなこんなで、暇だからAI作って遊び相手になってもらおうと思って書き始めたものの、案外面倒くさくて、ここまで書き終わる頃には新幹線が目的地に着いてしまったという。それはそれで良い暇つぶしにはなりましたけど。
lens
しーんじてーこーころがとーきーめーいたー瞬間をー
遠い昔のことですが、こんな記事を書きました。
record update - yunomuのブログ
recordのupdateめんどくさいよねという話。
で、コメント欄でふみさんに「lensあるよ」って言われました。
http://hackage.haskell.org/package/lens
あるよっていうか、存在は知っていたけど、こういう用途に使うものだったんですね。デカい図が貼ってあるけど何言ってるかわかんねぇ。
とりあえず使ってみました。
{-# LANGUAGE TemplateHaskell #-} module Main where import Control.Lens import Data.Text (Text) data Music = Music { _title :: Text , _count :: Int } deriving (Show) makeLenses ''Music
とりあえずこれで準備OK。lens使う時はレコードのフィールド名を'_'で始めないといけないっぽいです。
あとはmakeLensesのためのTemplateHaskell拡張。このとき、例えばこのMusicというデータの中で他のデータ型を参照している場合はこの部分より前でそのデータ型を定義しておく必要があるとか、titleとかcountとかいう関数が自動生成されるから注意するとか、TemplateHaskell的な制約が少々。
あとアンダースコアがそもそもダサいとか。その辺は、lensを使うとそもそも生のフィールド名を使わずに済みそうなので、割り切って見なかった方向で行くといいのかもしれない。
% ghci -XOverloadedStrings ... [1 of 1] Compiling Main ( Main.hs, interpreted ) Ok, modules loaded: Main. *Main> let music = Music "純愛レンズ" 0 *Main> music Music {_title = "\32020\24859\12524\12531\12474", _count = 0}
例題が微妙にマズかった。なんでTextで日本語にしたんだ。それはそうと、これだとレコードの更新はこんな風に書ける。
*Main> let a = Music "Mermaid festa vol.1" 0 *Main> a Music {_title = "Mermaid festa vol.1", _count = 0} *Main> a{_count = _count a + 1} Music {_title = "Mermaid festa vol.1", _count = 1} *Main> a&count %~ (+1) Music {_title = "Mermaid festa vol.1", _count = 1}
従来と比較して、まあ正直どっちがいいんだかはよくわからないけども、少なくとも`a`とか`_count`が2回出てこないのは良い。
でもこの書き方のいいところは他のデータ構造と一緒に使った時で
例えばMapでadjustを使うとき
*Main> import Data.Map *Main Data.Map> let m = insert 9 a empty *Main Data.Map> m fromList [(9,Music {_title = "Mermaid festa vol.1", _count = 0})] *Main Data.Map> adjust (\v -> v{_count = _count v + 1}) 9 m fromList [(9,Music {_title = "Mermaid festa vol.1", _count = 1})] *Main Data.Map> adjust (&count %~ (+1)) 9 m fromList [(9,Music {_title = "Mermaid festa vol.1", _count = 1})]
下の2つは同じ事をしているんだけど、要らないところが削れてだいぶ良い感じになったような気がする。構文というか、演算子が見やすいかどうかは意見が別れるところだろうけども。
という風に、Data.Map.adjustとかControl.Monad.State.modifyとかを多用する私みたいな人には割とおすすめなライブラリかもしれないです。
普通にgetterとかsetterっぽいのもあります。
*Main> a^.count 0 *Main> a^.title "Mermaid festa vol.1" *Main> a&title .~ "No brand girls" Music {_title = "No brand girls", _count = 0} *Main> a&title .~ "Love marginal" &count .~ 3 Music {_title = "Love marginal", _count = 3}
というか単に私がまだこの3つの操作しか覚えてないだけですが、もっといろんな使い方を知ると便利なライブラリになると思います。たぶん。
こんなかんじで更新を抽象化してる風な関数を定義したりとかもできますけど、
*Main> let upd rec field value = rec&field .~ value *Main> upd a title "sweet & sweet memories" Music {_title = "sweet & sweet memories", _count = 0}
[(FieldName, Value)]みたいな組のリストを渡して突っ込むみたいな事は、組のリストを作れない関係でできないので、作ってもあんまし意味が無い気がしますね。
でもこの3つの操作を覚えるだけでも結構気持ちよくやれる気がします。recordなんて要らんかったんや!
ソース
https://github.com/yunomu/exercises/blob/master/lens/Main.hs