JavaScript UM Emulator

JavaScript上のPCエミュレータ http://bellard.org/jslinux/ に触発されて、

http://tanakh.jp/jsumix/

こんなものを作りました。
詳しくはこちら → http://www.boundvariable.org/
JavaScript意外と速くてびっくりなのです。
本家よりは全然すごくないですけど、
本家よりは遊べるんじゃないかと思います。

みんなで楽しくHacking!

Boost.勉強会 #4 で発表してきました

Boost.勉強会 #4で発表してきました。ICFP Programming Contest 2010 での Discriminating Hacker's 言語の件から、 C++をDISるというテーマで話す機会を頂いたので、せっかくなので、発表させていただくことにしました。

C++プログラマ、所謂闇の軍団の集う中でこのような発表をするのは、まさに飛んで火に入る夏の虫というやつで、戦々恐々としていたのですが、思惑とは逆に、全然DISれていないと私がDISられるというような結果となりました。

Haskellerの目線から、C++の気に入らない点を、主に型の話をメインに展開していったのですが、おおよそのC++erにとってはあまり賛否両論とは言えなかったようですね。なんでしょうか。動的型付け言語ユーザに静的型付けのメリットをとくとくととくような感じで。

もっと私自身の主観的な立場から、個人的に気に入らない点をあげあつらえば、いい感じに炎上してくれたのかなあと思います。const否定論やら、スマポ否定論やら、いろいろアイデアはありましたが、ちょっと日和ってしまったのと、全部話すには時間が足りないという感じでした。

C++関数型言語だと言われて久しい(?)ですが、私はパターンマッチとカリー化が関数型言語では重要と考えていますので、そこがやはり不満なのです。そこのところがもう少し掘り下げて喋れればよかったなあと、すこし残念ではありました。それはまたいつぞやの機会にブログにでも書きましょうか、あるいは、私の昨日あたりのtwitterを見ていただければ少しは想いが伝わるかもしれません。

Haskellのエラー処理とMonadCatchIOの落とし穴

(この記事はHaskell Advent Calendar jp 2010のために書かれました)

Haskellではエラー処理に例外が用いられます(MaybeモナドやErrorモナドも用いられますが、ここでは例外に焦点をあてます)。

例外インターフェースの話

Haskellにも、例外を扱うためにtry, catch, finallyなどが用意されています。他の多くの言語ではこれらは構文として用意されますが、HaskellではIOモナドを引数にとる関数になっています。

try :: Exception e => IO a -> IO (Either e a)
catch :: Exception e => IO a -> (e -> IO a) -> IO a
finally :: IO a -> IO b -> IO a

tryはIOアクションを引数にとり、それを実行した結果が正常に値を返したか、はたまた例外かを返します。catchは例外が起こった場合の処理を記述できます。finallyは例外が起こっても起こらなくても第2引数のアクションを実行します。

これら(ともっと他にある例外処理用関数)を組み合わせてHaskellではエラー処理を行います。ひときわ良くあるケースとして、リソースの獲得、リソースの使用、リソースの解放の一連のパターンがあります。たとえば、ファイルをオープンして、ファイルにアクセスして、用事が済んだらファイルをクローズする処理を考えてみます。

main = do
  h <- openFile "hoge" ReadMode
  ... ファイルにアクセス
  hClose h

ファイルのオープンとクローズが別々になっているので、クローズを忘れるということがあるかもしれません。これを抽象化してみます。

main = do
  withFile "hoge" ReadMode $ \h ->
    ... ファイルにアクセス

withFile filename mode m = do
  h <- openFile filename mode
  m h
  hClose h

これでwithFileを用いている限りは、ファイルのcloseし忘れということに煩わされる心配はなくなりました。ところが、この実装は不完全です。ファイルにアクセスする部分のコードが例外を発生させた場合、withFileの最後の行、hCloseが実行されずに終わってしまいます。そのため、正しく例外を処理するコードが必要になります。

withFile filename mode m = do
  h <- openFile filename mode
  m h `finally` hClose h

これで正しいコードができました。この様なリソースの確保、リソースの解法を例外安全に行う処理というのは至る所で必要になってくるので、bracketという便利な関数が用意されています。

bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c

第一引数がリソース獲得関数で、第2引数がリソース解法関数で、第3引数がリソースを使用する関数です。リソース解法関数は、例外が起こった場合も正しく実行されます。これを用いると、withFileは次のようにかけます。

withFile filename mode m =
  bracket (openFile filename mode)
          hClose
          m

MonadCatchIO

このようにしてHaskellでエラー処理を記述することができますが、一つ大きな問題点があります。それは、これらの関数がすべてIOモナドを用いたインターフェースになっていることです。Haskellで例外が発生するのはIOモナドだけではありません。一つはpureなコードから発生する場合ともう一つはIOをリフトしたモナドから発生する場合です。pureなコードから発生する場合は、evaluate :: a -> IO a という関数を介することにより、IOモナド経由で例外を処理することができます。後者はたとえば変換子版のモナドを用いている場合に頻発します。近年の多くのモナディックなライブラリでは変換子版が用意されていることが多く、liftIOを用いてどこでもIOができるようになっています。liftIOによってIOをリフトできるモナドはMonadIOクラスとして抽象化されています。

MonadIOに対して例外処理を追加し、例外処理を一般化したものがMonadCatchIOクラスです。Hackage上の、MonadCatchIO-mtlや、MonadCatch-transformersのいずれかで利用できます。これを用いると、IOモナドをリフトできるIOモナド以外のモナド(たとえば StateT Int IO など)に対してtry, catch, bracketなどができるようになります。

foo :: MonadCatchIO m => m ()
foo = bracket (putStrLn "begin")
              (\_ -> putStrLn "end")
              (\_ -> ... 凝った処理 ... )

エラーモナドに対するインスタンス

ところがこれには大きな罠が潜んでいます。MonadCatchIO-transformersのドキュメントにWarningとして記載されていますが、ここでそれを解説しておきたいと思います。

(MonadCatchIO m, Error e) => MonadCatchIO (ErrorT e m)

問題となっているMonadCatchIOのインスタンスはこれです。どうしてこれがいけないのかというと、このモナドには2つのエラー通知方法があります。一つはMonadCatchIOで扱える例外、もう一つはエラーモナドです。一方のエラー処理の方法では、当然ながら他方のエラーは検出できません。つまり、エラー処理が分散してしまうことになります。これはこれでうれしいことではないのですが、さらに良くないことに、このことは奇妙な、望ましくない現象を引き起こします。

例えば、次のようなコードを考えます。

import Data.Typeable
import Control.Exception as E
import Control.Monad.CatchIO as MCIO
import Control.Monad.Trans

data MyException = MyException deriving (Show, Typeable)
instance Exception MyException

iofail :: IO ()
iofail = do
  E.throwIO MyException

foo :: MonadCatchIO m => m () -> m ()
foo m = MCIO.bracket (liftIO $ putStrLn "abc")
                     (\_ -> liftIO $ putStrLn "def")
                    (\_ -> m)

main :: IO ()
main = do
  foo iofail

MyExceptionはユーザ定義の例外を定義しています。iofailで例外をIOモナドとして発生させています。fooにiofailを渡しているので、bracketの中で例外が発生しますが、終了処理のputStrLn "def"は実行されるはずです。実行結果は次のようになります。

abc
def
mcio.hs: MyException

この場合、MonadCatchIOは単なるIOとしてインスタンス化されます。正しく動いているように見えます。次に、ErrorT String IO としてfooを実行してみます。

errfail :: MonadCatchIO m => m ()
errfail = do
  MCIO.throw MyException

foo :: MonadCatchIO m => m () -> m ()
foo m = MCIO.bracket (liftIO $ putStrLn "abc")
                     (\_ -> liftIO $ putStrLn "def")
                     (\_ -> m)

main :: IO ()
main = do
  r <- runErrorT $ foo errfail
  print (r :: Either String ())
abc
def
mcio.hs: MyException

これも正しく動いているように見えます。

これらはいずれも例外を投げていました。次に、もう一つのモナドの標準的なエラーであるfailを試してみます。

iofail :: IO ()
iofail = do
  fail "hoge"

foo :: MonadCatchIO m => m () -> m ()
foo m = MCIO.bracket (liftIO $ putStrLn "abc")
                     (\_ -> liftIO $ putStrLn "def")
                     (\_ -> m)

main :: IO ()
main = do
  foo iofail

まずはIOモナドです。

abc
def
mcio.hs: user error (hoge)

これは正しく動作します。次に、ErrorT String IO で試してみます。

errfail :: MonadCatchIO m => m ()
errfail = do
  fail "hoge"

foo :: MonadCatchIO m => m () -> m ()
foo m = MCIO.bracket (liftIO $ putStrLn "abc")
                     (\_ -> liftIO $ putStrLn "def")
                     (\_ -> m)

main :: IO ()
main = do
  r <- runErrorT $ foo errfail
  print (r :: Either String ())

さて、実行結果です。

abc
Left "hoge"

おや、さっきとは変わりました。failによるエラーがErrorモナドによるエラーの扱いになっているようですね。結果としてLeft "hoge"が返って来ています。そこはそれで良いのですが、問題なのは、defが出力されていないということです。これはどういうことなのでしょうか?

これはErrorTモナドのbindのセマンティクスおよびfailのセマンティクスに問題があります。ErrorTモナドでは、エラーが起こるとbindにおける右辺値、すなわち後続の計算がすべてショートカットされるようになっています。そして、failはErrorTモナドにおけるエラー値を返します。ゆえに、ErrorTにおいては、failが呼ばれた時点で残りの計算はすべてスキップされてしまいます。当然それにはリフトされているIOも含まれます。折角MonadCatchIOを用いて記述した例外処理も含まれます。確実にリソース解放処理を行わせるためにbracketを使っているのにこれでは大問題です。

どうすべきか

MonadCatchIOの例外処理が正しく動作するかどうかは、インスタンスにするモナドのセマンティクスに全面的に依存します。たとえば標準モナド変換子ライブラリですと、ErrorTとContTがこの問題を抱えているようで、ドキュメントにその旨が記載されています。モナドのセマンティクスに依存するので、もちろんそれ以外のモナドでもありうるかもしれません。例えば、Snap web frameworkにおけるSnapモナドでも同様の問題がありました(Snapモナドでは例外発生時でもbracketをすり抜けてしまっていました)。

どうすればいいんでしょうか。これに対する決定的な解決を私は知りません。自分がMonadCatchIOのインスタンスを作成する場合、もし可能なら、計算がショートカットしないようにすれば解決です。しかし、それは一般的には可能ではないでしょう。

この様な問題を抱えたMonadCatchIOのモナドを扱うにおいては、もっとも妥当な対策として、liftIOする前にすべての例外を捕まえてしまうようにするのがいいかと思われます(何のためのMonadCatchIOなのかという話になりますが…)。

少なくとも、この様な現象が発生し得るということを知っておくだけでもデバッグの役に立つかもしれません。私は最初この現象に遭遇した時に、必死にprintを挿んでコードを追っていましたが、突然コードパスが途切れて一体どうなっているのかとかなり悩んでしまいました。

関数型!侵略ノススメ☆

(この記事は Functional Ikamusume Advent Calendar jp 2010 の為に書かれました)

侵略!侵略!侵略!侵略!侵略!侵略!イカ娘

再帰しなイカ

main = putStrLn $ f 6 where
 f 0 = "イカ娘!"
 f n = "侵略!" ++ f (n-1)

古風に再帰しなイカ

main = putStrLn $ f 6 where
 f 0 = "イカ娘!"
 f (n+1) = "侵略!" ++ f n

左派じゃなイカ

main = putStrLn $ foldl (\a _ -> "侵略!"++a) "イカ娘!" [1..6]

右派じゃなイカ

main = putStrLn $ foldr (\_ a -> "侵略!"++a) "イカ娘!" [1..6]

右派に見せかけた左派じゃないか?

main = putStrLn $ foldr (\_ g n -> g ("侵略!"++n)) id [1..6] "イカ娘!"

const教じゃなイカ

main = putStrLn $ foldr (const ("侵略!"++)) "イカ娘!" [1..6]

ポイントフリーじゃなイカ

ika = foldr (const ("侵略!"++)) "イカ娘!" . enumFromTo 1
main = putStrLn $ ika 6

繰り返さなイカ

main = putStrLn $ until ((>=18+4) . length) ("侵略!"++) "イカ娘!"

メモ化しなイカ

ikas = "イカ娘!" : map ("侵略!"++) ikas
main = putStrLn $ ikas !! 6

メモ化しなイカ(その2)?

main = putStrLn $ iterate ("侵略!"++) "イカ娘!" !! 6

継続渡さなイカ

ikac k 0 = k "イカ娘!"
ikac k n = ikac (k . ("侵略!"++)) (n-1)
main = putStrLn $ ikac id 6

不動点じゃなイカ

import Control.Monad.Fix
main = putStrLn $ take 18 (fix $ \侵略s -> "侵略!"++侵略s) ++ "イカ娘!"

もっと不動点じゃなイカ

import Control.Monad.Fix
main = putStrLn $ (fix $ \f n -> if n==0 then "イカ娘!" else "侵略!" ++ f (n-1)) 6

モナド使わなイカ

main = putStrLn $ (do [1..6];"侵略!")++"イカ娘!"

Haskellと言ったらリスト内包表記じゃなイカ

main = putStrLn $ foldr (.) (const "イカ娘!") [("侵略!"++)|_<-[1..6]] $ ""

手続き型やらなイカ

for b e m
  | b == e = return()
  | otherwise = do
    m b
    for (b+1) e m

main = do
  for 0 6 $ \i -> do
    putStr "侵略!"
  putStrLn "イカ娘!"

熟練した手続き型Haskellerはこう書くでゲソ!

main = forM_ [1..6] (const $ putStr "侵略!") >> putStrLn "イカ娘!"

手続き型に侵略されたでゲソ!

import Control.Monad
import Data.IORef
import System.IO.Unsafe

イカ = unsafePerformIO $ newIORef "イカ娘!"

main = do
  forM_ [0..6] $ \_ -> do
    modifyIORef イカ ("侵略!"++)
  putStrLn =<< readIORef イカ

エンコードしなイカ

import Numeric
main=putStrLn$showIntAtBase 6(toEnum.fromInteger.(`mod`10^5).div 652812306412459124522040530053.(10^).(*5))25099952825734985""

ちょうど140文字でゲソ!

普通に書かなイカ

main = putStrLn $ concat (replicate 6 "侵略!") ++ "イカ娘!"

普通に書かなイカ(その2)?

main = putStrLn $ take 18 (cycle "侵略!") ++ "イカ娘!"

ゴルフしなイカ

main=putStrLn$([1..6]>>"侵略!")++"イカ娘!"

これでいいんじゃなイカ

main = putStrLn "侵略!侵略!侵略!侵略!侵略!侵略!イカ娘!"

※この記事は以下のページの関数型イカ娘風パロディでゲソ!
http://www.willamette.edu/~fruehr/haskell/evolution.html

ICFP Programming Contest 2010 優勝

   Pure Pure Code ++
Language: C++, Haskell, Python
... are the programming
languages of choice for
discriminating hackers.

今年のICFP Programming Contestにて優勝しました。(コンテスト中の様子は http://d.hatena.ne.jp/tanakh/20100702#p1 こちらにあります)

一次ソース(http://www.icfpcontest.org/2010/)はまだ来ていませんが、今年のICFP@ボルチモアにて表彰されてきました。こちら(http://twitpic.com/2swi5c)に証拠写真がアップロードされています。

Our Score: 13597.354
Our Solved: 3451
Our Cars: 72
>=5 users: 15 unsolved
3-4 users: 262 solved, 36 unsolved
2 users: 496 solved, 12 unsolved
Monopoly: 253 unsolved
91% of the Market are belong to us

最終的なスコアはこのような感じでした。特筆すべきは2 usersの占有率の高さで、508個中496個をうちのチームが解いています。しかも、リファレンス解答よりもサイズが小さいものも半分ぐらいあり、ここだけで250近い定期収入がありました。データが出てきたときにはちょっと信じられなくて何度か確かめたのですが、どうやら間違いはなさそうでした。どうしてこうなったのかは当の本人にもよくわかりませんが、コツコツソルバを改良していたのがよかったのかなと思います。http://d.hatena.ne.jp/wata_orz/20100622/1277229671 おそらくこのような被害者をたくさん作ってしまったんじゃないかと思います。でも、責任をとって優勝してきました!

本当に本当に、とても嬉しいです。同時に、やっと勝てた、という安堵の気持ちがあります。2004年に初参加して以来毎年参加していて、今年で7度目の参加です。初参加でいきなり3位になり(http://d.hatena.ne.jp/tanakh/20040926#p1)、次の年で同じく3位ながらも入賞を果たした(http://d.hatena.ne.jp/tanakh/20050930#p1)ときには、なんだかよくわからないけど、そのうち1位は取れるものだと思っていました。ところがその次の年は人数制限に泣き、さらに次の年は5位に入ったものの、それ以降は年々順位は右肩下がりで、もう勝てないんじゃないかと年々衰え行く脳みそを嘆いたりしていました。

しかし、諦めるものではありませんね。ずっと頑張っていれば報われることもあるようです。今年はチームメイトに恵まれました。これだけのメンバが6人も集まれば、文殊をも凌駕します!改めてチームの皆様に感謝します。ぴゅあぴゅあこーどの一員になれて本当によかったです。

さて、我々がDiscriminating Hackersと認められたと同時に、我々の使ったプログラミング言語にはDiscriminating Hackersの選ぶ言語として、今年一年間無制限に宣伝する権利が与えられます。何を選ぶか大変に悩みました。さすがに6人もいるとなかなか合意が得られません。というより、私以外に特に選びたい言語がある人がいないようです。私は断然Haskellを推しますが、さすがに私しか使っていない言語を選ぶのも気が引けます。はてはて、何時まで経っても決まらず、一時は間を取って(?)"crontab"にするという案も挙がりましたが、果たしてcrontabは"プログラミング言語"なのか、というそもそもの議論からして、やはり自粛すべしということになりました。crontabがDiscriminating Hackersの言語になっても喜ぶ人だれもいないですし。

結局言語選びは表彰式当日まで縺れました。最終的には、"ひとつに選べる言語はない"。強いて挙げるなら、使った言語の中で、まともにプログラミングに用いられた"C++, Haskell, Python"の3つがそれである、との結論に至りました。これらの順番には意味がないことを注記しておきます。単に辞書順です。

C++は焼きなましソルバに用いられました。焼きなましソルバに求められるのは他の全てを差し置いて速度です。速度以外に何も必要ないのでC++で書かれました。普段、Haskellでも高性能なプログラムを書けると主張している私ですが、この選択は間違っていません。他ならぬ私が書いたものです。C++で高速なプログラムを書くのは圧倒的に簡単です。普段ならともかく、時間の限られている中では、チューニングの手間がかからないC++がベストです。

Haskellは回路記述に用いられました。Haskellが使われたのは、単に私が書いたからという理由以外にはないかもしれませんが、Lava(http://hackage.haskell.org/package/xilinx-lava)などを挙げるまでもなく、このような用途にHaskellが適していることに異論はないでしょう。開始〜夜明けまでに回路ジェネレータが動いたのはHaskellのおかげだと思っています。その他、Haskellは一部の特殊ソルバなど、私専用スクリプティング言語として働いてくれました。

Pythonはその他ほぼすべての用途に用いられました。大きいのは、CGIと特殊ソルバでしょうか。CGIは制約式の可視化及び、行列表記によるサブミット、ログ蓄積とかなり重要なインフラでした。用意されたCGIでは、解いた問題や作った車すら分からなくなる仕様でしたので、このようなインフラは縁の下の力持ちとしてしっかりとチームを支えてくれました。特殊ソルバは焼きなましソルバで解けないタイプの問題のかなりの部分を潰してくれました。まさにどれが欠けても今回の優勝はなかったでしょう。

という訳で、"C++, Haskell, Python"です。これらの言語に関して、今年一杯はくれぐれも不用意な発言は避けていただくようにお願いいたします。尤も、今回のICFPコンテストにおいて、1番多くのチームに使用された言語はHaskellで、その次がC++で、その次がPythonですので、我々の選択は最も多くの人々を幸せにするものだと自負しております。ただし!私にはその権利があると思いますので、これからもC++の悪口を言いまくると思います。ワハハハは。

さて、ICFPなのです。コンテストの入賞者はICFPに招待されます。ICFPというのはその名の通り、関数プログラミングに関する国際学会です。その分野では最高峰に位置していると思います。私が研究の道に進んでいたらもしかしたら論文の方で参加していたかもしれません。そうなっていないのは、私の怠惰によるところではありますが、別の入口からとはいえ、改めてその中を覗いてみると、面白そうで面白そうで悔しくなってきます。英語が全く聞き取れないのと、そもそも内容が理解出来ないのとで、殆ど分からなかったのが悲しかったです。予習をしておけばよかったと悔やみました。でも、悔やんでも仕方が無いので、復習はしっかりしようと思います。

ICFPに参加できて本当によかったです。ともすれば日々安穏と暮らすことを良しとしようとする私の中に、知的好奇心のかけらと闘争心がまだ残っていることを確認できました。世の中にはこんなに面白くて難しいことが埋れているのに、ゆっくりなんてしていられない。もっと勉強したい。そして、いつか機会があれば論文の方で通してみたいという野望も持ち続けておこうと思いました。

併設イベントのHaskell SymposiumとHaskell Implementors Workshopにも参加しました。Haskell SymposiumはHaskellに関する研究ばかりで、私にも分かりやすいものが多かったです。Haskell Implementors WorkshopはHaskellを取り巻く現状や、ツールチェインの整備の話など、まさにHaskellの今がここにあるといった感じでとても面白かったです。Haskell界隈で有名な人が勢ぞろいしていてすごかったです。Hackageの話など、発表が終わった後も延々と議論が続いて、自分も英語が不自由なく扱えたなら、意見の一つや二つ言えるんだなあと思うと、これまた悔しさと悲しさがこみ上げてきたりして。

当時の様子はkinabaさんのまとめ(http://togetter.com/li/55345)にちょこっと載せていただいているので、よろしければご参照ください。私のはともかく、ICFP本編のkinabaさんのまとめが大変参考になります。

はてさて、そういうわけで今回は本当にいい経験をさせてもらいました。チームの皆さんに改めて感謝。このような楽しいコンテストを開催していただいた、主催者の方々にも感謝。願わくば、来年も優勝せんことを。そうそう、来年のICFPは東京開催です。皆様是非是非奮ってご参加を。

One-liner in Haskell

Haskellを現場言語にするために、こんなものを作ってみました。

hoe: Haskell One-liner Evaluator
(名前には深い意味はありません。)

Haskellワンライナーをやろうという誰得なツールです。誰得ですが、ワンライナーでも、型があると便利なんではなかろうか、型を元にユーザの望みの動作が大体決定できるんではなかろうか、という発想を元に作られました。

Haskellワンライナーは、ghc -e でも評価できますが、これは (Show a) => a か、 (Show a) => IO a な型しか評価できません。hoeでは、String -> String など、もっと色々な型を評価できます。そして、その型に応じていい感じの動作が自動的に選択されます。

例えば、idを入力すると、入力がそのまま出力されます。

$ cat tmp
Hello, Haskell World!
$ hoe 'id' tmp
Hello, Haskell World!

そうです。もうcatを作るのに、
http://d.hatena.ne.jp/nobsun/20100820/1282270092
こんなプログラムを書く必要は無いんです。

文字処理

Char -> Char なら、文字列全体に与えられた関数を適用します。

$ cat tmp | hoe 'toUpper'
HELLO, HASKELL WORLD!

大変単純明快です。

[a] -> [a] ほか

基本的にhoeは、行指向の振る舞いをします。
[a] -> [a] は、[String] -> [String] にも String -> String にもマッチしますが、このような場合は [String] -> [String] が選択されます。例えば、take 2 なら、先頭から2行を取ってくる処理になります。

$ cat tmp
Hello,
Haskell
World!
$ cat tmp | hoe 'take 2'
$ hoe 'take 2' tmp
Hello,
Haskell

また、sort なら、行をソートする処理になります。

$ cat tmp | hoe 'sort'
Haskell
Hello,
World!

大変シンプル。

String -> String の関数を書いた場合も、行ごとの処理になります。

$ cat tmp | hoe '("> "++)'
> Hello,
> Haskell
> World!

行番号

Int -> String -> String などの関数を書けば、第一引数に行番号が入ります。

$ cat tmp | hoe '\ln s -> show ln ++ "> " ++ s'
1> Hello,
2> Haskell
3> World!

先頭に行番号を書いたりするのも、このとおり。

(Int, String) を返せば、第一引数でソートした結果が出力になります。

$ cat tmp | hoe '\_ s -> (length s, s)'
Hello,
World!
Haskell

これは、行を長さでソートします。

関数のlift/join

行ごとではなく、文字ごとに処理したい場合もあります。

たとえば、drop 2 と書くと、先頭2行を落とす処理になりますが、各行2文字落とす処理が書きたいとしましょう。これは

$ cat tmp | hoe 'map $ drop 2'
llo,
skell
rld!

のように、mapを用いればできますが、これはめんどくさい。そこで、与えられた関数を [String] -> [String] ではなく、 String -> String と解釈する --join (-j) オプションを用意しました(現在の実装は極めていい加減なので、型によってはうまく動かない時もあるかもしれませんし、joinじゃなくてliftするかもしれません)。これを用いると、

$ cat tmp | hoe -j 'drop 2'
llo,
skell
rld!

と書けます。

モジュール自動import

hoeは、独断と偏見で選別したよく使われるライブラリを、自動的にimportしています。上での例でのtoUpperが装飾なしで使えていたのはこのおかげです。

なので、例えば行の長さでのソートは

$ cat tmp | hoe 'sortBy $ comparing length'
Hello,
World!
Haskell

このようにも書けます。

inplaceモード

  • i を付けると、与えたファイルを書き換えます。

例えば、

$ hoe -i 'toUpper' *

とやると、すべてのファイルを大文字にします。-iオプションに文字列を渡すと、

$ hoe -i.bak 'toUpper' *

元のファイルに.bakを付けたファイル名でバックアップが作成されます。安心です。

値評価モード

当然ですが ghc -e でできる単なる (Show a) => a や、(Show a) => IO a の評価もできます。

$ hoe '2^100'
1267650600228229401496703205376
$ hoe 'pi*50^2'
7853.981633974483

電卓としても使えますね。

$ hoe 'forM [1..10] $ \i -> writeFile ("tmp."++show i) ""'

ゴミファイルの生成もお手の物。

あとがき

という訳で、誰得ツールを作ったお話でした。
超適当な実装で、まだまだ全然機能がありませんが、
ご意見ご感想などあればどしどしご送信下さい。