Data.Traversable

みなさんいかがお過ごしでしょうか。きれいな象さん本が出た昨今、皆様におかれましても急速にHaskell熱が高まっておられるものと思います。え?高まってない?KY*1


さて、象さん本を読んだことで僕もFunctor, Applicative, Monoid, Monadといった型クラスがなんとなく分かりました。正直Applicativeは未だにちゃんと分かってません。そんな僕ですが今回はとても便利な型クラスを紹介したいと思います。それはData.Traversableです。


ドキュメンテーションとしての型

そもそも僕がTraversableを知ったのは、「こんな型の関数が欲しいな〜」と思って、自分で実装する前に試しにhoogleで型を検索してみたらそのものずばりの型があった、という経緯です。その時のクエリはたしかt a -> (a -> m b) -> m (t b)でした。「型はドキュメントたりうる」という名ゼリフを実感しました。この型こそ、Traversableの基本操作である関数traverseだったのです。

class (Functor t, Foldable t) => Traversable t where
  traverse :: Applicative f => (a -> f b) -> t a -> f (t b)

ウゲー、Applicativeが出てきました。よくわからないのでMonadに読み替えましょう。それでも良くわからないので、IOモナドを具体例に取りましょう。tは自分がトラバースしたいデータ構造です。かりに木だとしましょう。

   traverse :: -- トラバースは 
   (a -> IO b) -- 「aを取ってbを返すプログラム」を元に、
-> Tree a      -- 『「aを要素にもつ木」を取って
-> IO (Tree b) --  bを要素にもつ木を返すプログラム』を作ってくれる。

ということのようです。要素を扱うモナドが既にあって、その要素からなる複雑なデータ構造があるとき、データ構造をくまなく舐めてくれるモナドが一発で作れる。そうそうこれがやりたかったんです!

Traversableココが凄い!

以下のようにPlayTree.hsを作って、インタプリタから読み込んでみます。

> cat PlayTree.hs
{-# OPTIONS -Wall #-}
import Control.Applicative
import Data.Foldable
import Data.Functor
import Data.Monoid
import Data.Traversable
import Prelude hiding (mapM,sequence,fmap,foldr, foldl,foldl1,foldr1)

data Tree a = Empty | Node a (Tree a) (Tree a) 

instance (Show a) => Show (Tree a) where
  show Empty = ""
  show (Node x left right) = "(" ++ show x ++ show left ++ show right ++ ")"

instance Functor Tree where
  fmap = fmapDefault
instance Foldable Tree where
  foldMap = foldMapDefault

instance Traversable Tree where
  traverse _ Empty               = pure Empty
  traverse up (Node x left right) = 
    Node <$> up x <*> traverse up left <*> traverse up right

> ghci PlayTree.hs
 ...
Ok, modules loaded: Main.
 *Main> 

これでTraversableなTreeが出来ました!

すごい1:定義すべきはtraverseだけ

自作のデータ構造をTraversableにするために必要なのはただ一つ。traverseという関数を定義する事です。では、上のtraverseをどうやって作ったのかを説明しましょう。

  traverse :: Applicative f => (a -> f b) -> t a -> f (t b)

全然わからないけどApplicative?っていうの?を使った何か?をどうにかして組み立てないといけません。材料はおそらくこの辺です。

 *Main> :t Empty
Empty :: Tree a
 *Main> :t Node
Node :: a -> Tree a -> Tree a -> Tree a
 *Main> :t pure
pure :: Applicative f => a -> f a
 *Main> :t (<$>)
(<$>) :: Functor f => (a -> b) -> f a -> f b
 *Main> :t (<*>)
(<*>) :: Applicative f => f (a -> b) -> f a -> f b

と、とにかくtraverseの第一引数は関数 up :: a -> f b で、第二引数はTreeのはずです。

instance Traversable Tree where
  traverse up Empty               = undefined
  traverse up (Node x left right) = undefined

Emptyはもうこれ以上崩しようがないので、とりあえずpureっておけばいいでしょう。型はf (Tree b)なので合ってます。

  traverse up Empty               = pure Empty

問題はNodeです。象さん本によるとApplicativeってのは、ピュアな関数をfの上の"関数"に変化させるもので、そのとき元々の呼び出し方「関数 引数 引数 引数」だったものは「関数<$>引数<*>引数<*>引数」という形になるはずです。そこで次の型を調べてみます。

 *Main> :t (\x y z -> Node <$> x <*> y <*> z)
(\x y z -> Node <$> x <*> y <*> z)
  :: Applicative f => f a -> f (Tree a) -> f (Tree a) -> f (Tree a)

というわけでノードをトラバースする式の右辺には "Node <$> x <*> y <*> z"という式を書けば良く、x,y,zの型はそれぞれ f b , f (Tree b) , f (Tree b) であると分かります。

  traverse up (Node x left right) = undefined

左辺には x :: a , left :: Tree a , right :: Tree b が来ています。こいつらは up :: a -> f b と、traverse up :: Tree a -> f (Tree b) を使って必要な型に変えられます!こうしてtraverseを作ることができました。

instance Traversable Tree where
  traverse _ Empty               = pure Empty
  traverse up (Node x left right) = 
    Node <$> up x <*> traverse up left <*> traverse up right

そしてこれは正しく動作します!

・・・うむ、茶番に付き合ってもらってすまない。最初に書いたときはData.TraverseのhaddockにあるTreeのサンプルをいじっただけで、ここまで深く考えたわけじゃないです。

すごい2:どんなモナドでも扱える

Traversableのインスタンスにしただけで、このツリーには数々の強力な機能が備わりました。試してみましょう。架空の一族、sazae家の家系図を作ってみます。

 *Main> let t = Node "sazae" (Node "katsuo" Empty Empty) (Node "wakame" Empty Empty)
 *Main> t
("sazae"("katsuo")("wakame"))

まずはこの木をIOモナドに舐めさせてみましょう。

 *Main> :t mapM (putStrLn) t
mapM (putStrLn) t :: IO (Tree ())
 *Main> mapM (putStrLn) t
sazae
katsuo
wakame
(()(())(()))

木の要素がputStrLnされて、Tree ()が返ってきています。Tree ()を返されたところであまり有用ではありませんが、IOなので「ディレクトリ構造を再帰的に舐めた上で見つかったファイルの中身をディレクトリ木構造に入れて返すIOモナド」なんてのが美しく書けそうですね。

つづいて他のモナドに舐めさせてみます。IO以外のモナドといえば非決定計算です。sazae家の各キャラが男性"M" なのか女性"F" なのかコンスタントに疑い続ける、という姿勢を貫いてみましょう。

 *Main>  mapM (const ["M", "F"]) t
[("M"("M")("M")),("M"("M")("F")),("M"("F")("M")),("M"("F")("F")),("F"("M")("M")),("F"("M")("F")),("F"("F")("M")),("F"("F")("F"))]

予想通り、sazae家のあり得る性別の組み合わせが全列挙されました。リストモナドにはいつも驚かされます。

すごい3:いまなら無料でFunctorとFoldableがついてくる!

Traversableのクラス定義をあらためて見ると、(Functor t, Foldable t) => というコンテキストがついてます。

class (Functor t, Foldable t) => Traversable t where

ウゲー、Traversabe t をインスタンスするにはまず Functor t, Foldable t を書かないといけないの?そんなことはないんです!

Data.Traversableには fmapDefaultfoldMapDefaultというのが用意されており、これを使うとFunctorとFoldableのインスタンスは機械的に作れます。

instance Functor Tree where
  fmap = fmapDefault
instance Foldable Tree where
  foldMap = foldMapDefault

Traversable tにするにはまず(Functor t, Foldable t)であること必要だが、(Functor t, Foldable t)であることはTraversable tであることを利用して定義されている。こんなことがどうして可能になるのでしょう。恐らく円環の理の導きによるものだと思われます。Haskellすごいですね。

とにかくこれでTreeはFunctorでもありFoldableでもあるということになりました。Functorなので、木構造を保ったままfmapできます。

 *Main> let age s = if s=="sazae" then 24 else if s=="katsuo" then 11 else 9
 *Main> fmap age t
(24(11)(9))

さらにFoldableですので様々なフォールドが使えます。フォールドは、traverseで指定された順序でツリーの要素を1つづつ綴じ込んでいく、という操作になります。Foldには左派と右派がおり、綴じる方向が違います。

 *Main> foldl (\sum str -> sum + age str) 0 t
44
 *Main> foldr (\str sum -> sum + age str) 0 t
44
 *Main> putStrLn $ foldl1 (\x y -> " <"++x++" "++y++"> ") t
 < <sazae katsuo>  wakame> 
 *Main> putStrLn $ foldr1 (\x y -> " <"++x++" "++y++"> ") t
 <sazae  <katsuo wakame> > 

さらにfoldMapという関数もあり、これは木の各要素からMonoidを作り出したあげくMonoidの結合演算ですべてを結合する、というものです。

 *Main> :t foldMap
foldMap :: (Monoid m, Foldable t) => (a -> m) -> t a -> m

そんな説明ではさっぱりわかりませんね。例えばリストはMonoidで結合演算は++です。例えばツリーの要素をすべて単一要素のリストに変えたうえで結合すると

 *Main>  foldMap (\a->[a]) t
["sazae","katsuo","wakame"]
 *Main>  foldMap (:[]) t
["sazae","katsuo","wakame"]

このようにツリー全体がリストに変化します。一瞬何が起きたか分からないので、各キャラが猫を連れている、という状況を表現してみます。

 *Main>  foldMap (\x -> [x, x++"'s cat"]) t
["sazae","sazae's cat","katsuo","katsuo's cat","wakame","wakame's cat"]

さらに文字列もリストなので、ぶっちゃけそのまま連結することもできます。

 *Main>  foldMap id t
"sazaekatsuowakame"

リストばかりがMonoidではありません。たとえばProductというMonoidは結合すると掛け算を行います。

 *Main>  foldMap (Product . age) t
Product {getProduct = 2376}

sazae家全員の年齢の積が分かりました。


いまスーパークラスのデフォルトインスタンスを定義する機能というのが議論されているそうで(via http://favstar.fm/users/shelarcy/status/45649867862839296)、これが組み込まれれば、あの4行すら不必要になって更に便利になると思われます。

まとめ

自作のデータ構造を Data.Traversable にすれば

  • 「要素を扱うモナド」「その要素からなるデータ構造」があるとき、「モナドにデータ構造をくまなく舐めさせる」ことができます。
    • モナドが凄いので、これで自作のデータ構造に凄まじいモナドの力が備わり凄いことになります。
  • さらにそのデータ構造に対するfmapとfoldが無料でついてきます。
  • 今は分からなくてもいい。俺だって分からん。型を信じろ。俺が信じるお前が信じる型を信じろ。


本日のサンプルはここに置いてあります。 https://github.com/nushio3/Data.Traversable-example
「本物のプログラマは〜」にも掲載予定だそうで、楽しみにしてます!http://big.freett.com/shelarcy/articles_status.pdf

追記

それGHC拡張でderiveできますid:mr_konnさんありがとう!しかしこのフラグ達、GHC拡張の一覧にないですね。どこに定義されてるんだろう。Cabalはそういう言語拡張があるのを知っているみたいだけど。

{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable #-}
{-# OPTIONS -Wall #-}
import Data.Foldable
import Data.Functor
import Data.Traversable
import Prelude hiding (mapM,sequence,fmap,foldr, foldl,foldl1,foldr1)

data Tree a = Empty | Node a (Tree a) (Tree a) 
            deriving (Functor, Foldable, Traversable)
              
instance (Show a) => Show (Tree a) where
  show Empty = ""
  show (Node x left right) = "(" ++ show x ++ show left ++ show right ++ ")"

*1:learn you a hasKell for great good Yome!の略

Dvorak配列の左右入れ替えたら日本語入力しやすくなった

(この記事はDvorak Advent Calendarの一環として書かれました)
いきなりですが、僕はキーボードをこんな配列で使っています。

そう、kinesisです。101キーボードに焼き直すとこんな感じだと思います。

RAKDAO配列

という名前をつけています。まあ自分専用配列なのですが、日本語も英語もプログラムも打つ人向けなどに役に立てばと、このたび公開することにしました。RAKDAOは、Dvorak配列をベースに、以下の点で改良を加えたものです。

  • Dvorakを左右反転させることで、日本語の左右感覚に近づける
  • さらに右面を上下反転させ、記号をほぼQWERTY位置に
  • DvorakJPに基づく、「か」行、拗音、撥音、二重母音の入力効率化
  • Kinesis Contoured Keyboardの採用)

命名の由来は 

Dvorakを | 左右反転 | 右面を上下反転 |  日本語入力が                            
DVO RAK  | RAK DVO  | RAK DAO        |  楽だお(^ω^)

しょうもないですね。以下、変更した理由を述べてみます。

左右反転

Kinesis買って数週間。せっかく大枚はたいて買ったんだからQWERTYじゃ勿体ないと、Dvorakに移行したんですが、なんか日本語打ってて違和感があるんですよね。何故かと自省するに、日本語をローマ字で打つとき「子音→母音、左→右」という感覚があって、Dvorakだと母音が左側にあるんでそれに逆行してるんですよ。今見たらちょうどnagizeroさんのエントリにも同じことが書いてあります。さらに左右分離型の日本語入力用配列を見るとたいてい「子音=左、母音=右」となっていることからも、多くの人がこの感覚を持っているようです。

チョイとASEDR-MJIOPは逆ですね。ついでにいうとDvorakの設計理念の1つに英文で多い子音が右手ホームポジションで打てるように、ってのがあるそうですが、日本語だと母音の方が多いですよね。べ、べつに正当化したいわけじゃないんだからね!個人の感覚だと思うよ。あと左利きの人ごめん。

Rakdaoでは利き手よりも、左右感覚を重視していて、頻度の高いスペースが左に、エンターが右にあるのも、文章内ではそういう位置関係だからです。あと、エンターの方が強く押すキーだから、ってのもあるかな。

右面を上下反転

生きて行く上でQWERTYが打てなくなると不便ですので、ノートPCのほうはQWERTYにしてます。これはほとんど問題なく両方覚えました。で、記号があまりに別位置だと辛いので、左右入れ替えたついでに句読点あたりを元と同じ位置にしました。某オブジェクト指向言語で多用するピリオド、某テンプレート言語とかが多用する不等号C言語やらでデリミタになっているセミコロン・コロン。そいつらが一番打ちにくい左手上段から、それなりに優先度の高い、元の位置近くに戻ってきたときは、相当しっくり来ました。

一方、中かっことかが長距離動いてますが、これはもともとあるべき位置にもはや何も無いので、打ち間違えるってことは無いです。たまにlsのことをqaと打ったりはします。

DvorakJP

本Advent-Calendarでも何回か言及されている定番、DvorakJPを使わせてもらっています。「か」行を左手にあるCで入力、拗音は左手2キーの組み合わせで入力、二重母音や母音+「ん」を右手の日本語入力には使わないキーでサポート。これでいよいよ左右、左右のリズムでローマ字入力ができます。便利です。

Kinesis

常識を覆すエルゴノミックデザイン、全キーがハードウェア機能でリマップ可能、ついでに全キー同時押し可能という凄い奴です。なんといっても、普段タッチパッドの上か不必要に長いスペースバーの上でニートになってる親指が、修飾キーを押しまくってくれます。英文入力の時は親指がスペースと改行をがんがん入力してくれるので非常に快適であり、日本語入力時は親指が変換と確定をがんがん入力してくれるのでやっぱり非常に快適です。カーソルキーが遠くて小さいという悩みも、カーソルがすぐそばまで来てくれたので解決です。

リマップしたらキートップを入れ替えたくなりますが、なんとキーが1つ1つ曲面配置に合わせ微妙に異なった形をしているので入れ替えできません。したがってもともと刻印されているQWERTYはたんなる模様となり、タッチタイピングにより押したキーとは全然違うものが入力されます。パスワードとか盗まれない。この自慢できるアイテム度は無刻印とかの比じゃありません。しかもいまKinesis社のサイトを見たら、何かラインナップが変わってたのとドルが安いのとで、値段が某墨色とかとほとんど変わらなくなってるじゃなイカ!これはぜひお買い得であると思われます。いますぐKinesis社通販サイトへGo!

*送料は別途お客様のご負担となります

まとめ

まあ、QWERTYじゃなくて何にするかってときに調べたらいろんな日本語入力があるもんで、中には2〜3ストロークであらゆる文字入力可能でも暗号表覚えてね、なんてのもありましたが、こちとらプログラマはlazyなのでやってられないですよね。いまさら仮名の位置覚え直すのすらめんどいしローマ字入力でいいよ。プログラマ向けに記号頻度調べて再配置とかもありましたが、面倒いからこれもいいや。Ctrlを押すとQWERTYに戻るというのもありましたが以下同文。実はRAKDAOだとZCVは近所にあるんで案外行けます。


いろいろ書きましたが、要は好きにすればいいこと。もしこの記事が、Dvorakで同じような違和感があった人の助けになれば幸いです。

ghcのwarningについて

Haskell Advent Calendarに拙文を書いたことで、いけがみさん, やまもとさんなどから教えてもらったので、ghcのwarningについて調べてみました。

まず、warningの一覧はghcのドキュメントの中にあって、和訳も整備されていますフラグ早見表というのもあります。

次に、ホームに.ghciファイルを書いておくとghciの起動時にコマンドを実行できるということを知りました。.ghciの使い方も和訳されてます。じゃあghcにもデフォルトでフラグ渡したりできないのかな、と思いましたが、.ghcファイルというのは無いようなので、これはまあMakefileなり何なりから渡してください、ということなのでしょう。

さて、warning一覧に書いてあるwarningを出したり消したりしてみて遊んでいたのですが、-fwarn-incomplete-record-updates のところで詰ってしまいました。これは以下のように、存在しないかもしれないレコードを更新しようとするかもしれないコードに対して警告を発するというものです。

data Foo = Foo { x :: Int }
         | Bar

f :: Foo -> Foo
f foo = foo { x = 6 }

"このオプションはとてもうるさいことがあり、プログラムのバグを示していないことがしばしばあるので、デフォルトでは有効にされていない。"とされ、-Wallでも有効にされないので、気にしなくてもいいのかもしれませんが、それでもWarningである以上、消すことができるはずですよね。それでまあ

data Foo = Foo {record::Int} 
         | Bar deriving (Show)

main::IO ()
main = do
  print y where
    x = Foo 40
    y = f x
    f :: Foo -> Foo
    f foo = case foo of 
              Foo _ -> foo{record=6}
              Bar -> foo

こうかな、などとやって見ましたが、消えませんでしたね・・・こうなってくるととても気になります。どなたか、上のプログラムを ghc --make -Wall -Werror -fwarn-incomplete-record-updates でもコンパイルできるようにする方法、わかりますか?まあ、気にしなくてもいいのかもしれませんが、それでもWarningである以上、消すことができるはずですよね・・・。なんだか偏執狂的になってきたので、正気度チェックをしたほうがいいかもしれませんね。

加速しなイカ?

Chapter 1. Motivations

あれは、今から86万4000秒前の出来事だったか・・・

xxxx様
新規利用申請を承認しました。
アカウント名は 10IKA101 です。

(つд⊂)ゴシゴシ→(;゚ Д゚) …!?
10IKA101

神は言っている・・・
Functional Ikamusume Advent Calendarに参加するのです、と・・・

Chapter 2. Introduction of Haskell to TSUBAME 2.0

というわけで今日はLINPACK日本最速のスパコン(c.f. http://www.top500.org/list/2010/11/100)を侵略するでゲソ!
まずは橋頭堡を築くでゲソ。さしもの関数型イカ娘もGHCが入らなければ何も出来ないでゲソ。

10IKA101@t2a006172:~> cat /etc/issue
Welcome to SUSE Linux Enterprise Server 11 SP1  (x86_64) - Kernel \r (\l).

SUSEとは渋いじゃなイカ!筆者はOpenSUSEを愛用しているでゲソ。定番Linuxディストロが自作マシンでは全部動かなかった時もOpenSUSEは動いてくれたでゲソ。ただしHaskellのサポートは比較的弱いでゲソ・・・

Haskell Platformが直接インストールできなイカらまずはGHC6.12.3をインナントカスルでゲソ。"Generic amd64 Linux." と書いてあるうちの上のを入れるでゲソ。下のは罠でゲソ。root権限が無いので海の家れもんの下にghcディレクトリを作って侵略の拠点とするでゲソ。

10IKA101@t2a006172:~> mkdir ghc
10IKA101@t2a006172:~> tar jxfv ghc-6.12.3-x86_64-unknown-linux-n.tar.bz2
・・・イカ略・・・
10IKA101@t2a006172:~> cd ghc-6.12.3/
10IKA101@t2a006172:~/ghc-6.12.3> ./configure --prefix=$HOME/ghc
・・・イカ略・・・
****************************************************
Configuration done, ready to 'make install'
(see README and INSTALL files for more info.)
****************************************************
10IKA101@t2a006172:~/ghc-6.12.3> make install
・・・イカ略・・・

続いてHaskell Platformをインストールするでゲソ。

10IKA101@t2a006172:~> cd haskell-platform-2010.2.0.0/
10IKA101@t2a006172:~/haskell-platform-2010.2.0.0> ./configure --prefix=$HOME/ghc
10IKA101@t2a006172:~/haskell-platform-2010.2.0.0> make
10IKA101@t2a006172:~/haskell-platform-2010.2.0.0> make install

これも三行でインストールできたでゲソ。良い時代になったじゃなイカ!あとはPATHを通せば出来上がりでゲソ。

export PATH=$HOME/ghc/bin:$PATH

cprbもParaisoもちゃんと動いたでゲソ。しかしこれではただのインストール日記でゲソね。もっと能あるイカらしい所をみせようじゃなイカ!

Chapter 3. Accelerating Haskell Array Codes on TSUBAME

わが眷属はすでにHPCの侵略を開始しているでゲソ。HaskellGPGPUを操ろうというプロジェクトは、Accelerate, Nikola, Obsidian, Paraisoなどいっぱいあるじゃなイカ!

Nikolaは有望なのでゲソが、公式サイトにThis code needs a lot of clean-up, ... Due to package dependencies, this release is compatible with GHC 6.10.4 but not GHC 6.12. と書いてあるでゲソ。これは明確な死亡フラグじゃなイカ?Obsidianは公開されているページを見付けられなかったでゲソ。Paraisoはプロトタイプが普通に動いたでゲソ!でもParaiso本体はまだ影も形も無いでゲソ・・・

というわけでaccelerateの方を試すでゲソ。実はTSUBAME内部からはインターネットへのアクセスが遮断されているのでcabalが動かないでゲソ。筆者は徹夜で(無駄な)苦労していたのでゲソが、読者は多分興味がなイカら省略させていただくでゲソ


・・・中略☆中略☆中略☆中略☆中略☆中略・・・

結局イカの点が有用だったでゲソ。

  • sshはできるがインターネットへのアクセスが制限されているマシンからインターネットにつなぐには、ローカルマシンからつなげるプロキシを、ssh -Rでフォワードしてやる
h102:~> ssh -R 8180:proxy.kuins.net:8080  -Y 10IKA101@login-t2gpu.g.gsic.titech.ac.jp
10IKA101@t2a006171:~> http_proxy='localhost:8180' wget www.google.com
10IKA101@t2a006171:~> http_proxy='localhost:8180' cabal update
  • cabalがdependencies conflictで失敗したり、cannot satisfy -packageになった時は、ghc-pkg unregister --user で怪しいパッケージを取り除いてやる。しかるのちcabal installし直せばまた入るから大丈夫。
  • accelerateはHackageの0.8.1.0よりdarcsレポジトリの0.8.2.0のほうが新しい http://code.haskell.org/accelerate/ 。サンプルが増えてたりするよ。
  • accelerateは --flags='cuda'を指定しないとCUDA対応版がインストールされない。accelerate.cabalがあるフォルダでイカの呪文をとなえるでゲソ。
10IKA101@t2a006175:~/accelerate-0.8.2.0> cabal install --flags='cuda'

普段バッドノウハウ駆動のおいらにはいい薬になったでゲソ・・・。


ともかく0.8.2.0を入れて、Accelerateの論文にもNikolaの論文にも載ってるblack-scholesサンプルをベンチマークしてるとこでゲソ。機会があればまた結果を報告させていただくでゲソ。これでTSUBAME 2.0も関数型イカ娘のお友達でゲソ!


                     ヘ(^o^)ヘ いいぜ
                      |∧  
                  /  /
               (^o^)/ 関数型言語じゃ
               /(  )   HPCは出来ねえってなら
      (^o^) 三   / / >
 \     (\\ 三
 (/o^)  < \ 三 
 ( /
 / く  まずはそのふざけた
       ゲソをぶち殺す
大☆勝☆利!

arXivをラジオニュース風に読み上げてくれるソフト

研究者は大変です。世界で戦っていくにはぜひとも毎日

  • 今日出た論文のチェック
  • 英語

この2つののトレーニングが欠かせません、これ結構時間くってしまいますよね・・・

そこで、この2つが同時に、しかもながら勉強でできてしまうという超画期的なライフハックツールを作ってみました!その名も
arXivRadio
です。これ実行するだけで、arXivから最新1日ぶんの論文リストを取ってきてニュース風に読み上げてくれます。朝ご飯たべながら、通勤中しながらなど、耳さえ空いていればいつでも使え、さらに英語のリスニングの勉強もできます!デフォルトでは宇宙物理学(astro-ph)を読みに行くようになってますが、スクリプトの中のNEWS_URLのとこを書き換えると、arXivのほかのページも読み上げてくれます。

いや、実は去年くらいからあってずっと公開しようと思ってたんですが、このたびgithubを覚えたのを機に、astro-ph以外にも対応させてみて、無事公開の運びとなりました。

色んな人に活用してほしいので、「動かん!!」を始め、なんでもコメント・ご意見をお待ちしております。




そう、これMacとLinuxで動くけどWindowsじゃ動かないんですよね・・・今調べたらMicrosoft Speech Platformなるものを使うのがいいのかな。だれか対応させてくれなイカ?

Haskell入門者*1のデバッグ3つ道具

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

要旨

絶対にバグが出ない言語などというものは
無く、理不尽な挙動を前にして
途方にくれる時もある。
それを乗り越える為には、確固
たる信念と洞察、そして
(1)deriving Show
(2)print
(3)Debug.Trace
が必要である。

Haskell

Haskellはやっぱり魅力的です。無限リストを使ったプログラム(速度はともかく)や、自分でも理解しないまま型を合わせただけのプログラムがなぜか無事に動いたりすると魔法すら感じられます。ソースの見た目も美しいです。シンプルに書けて副作用が無くて型推論が有るので、Haskellはバグの出にくい言語とされています。

それだけに、Haskellで出たバグは取りにくく、しかも苦労して突き止めた結果が言語設計の根幹に関わる深刻な問題とかならいいんですが僕の書くようなレベルでそういうことはそうそうなく、たいていは苦労に見合わないがっかりミスなわけで。

うっかり変数名が重複していて、なんでも無いはずの関数が停止しない無限再帰になってたり、なんでも無いはずのリストが無限リストになってたりするミス、ってのがけっこうありました。

デバッグ

こういうミスがあると、あるところでプログラムが無限(bottom)を評価しようとし、フリーズします。そうなったら僕はどうするかというと、まず(0)やっぱこれHaskellで書くのやめようかな、と思いますが、書きかかったものを捨てるのも勿体ないのでなんとかデバッグしないといけません。

まずめぼしいクラスを(1)deriving Showします。プログラムはたいていIOモナドなので、なんでも表示できる(2)print :: Show a => a -> IO ()を色んな所に追加して、プログラムの流れを追って行くとどこでフリーズしたのかがわかります。

えてしてフリーズした箇所は、無限(bottom)を評価した箇所であって、その無限を生成した箇所ではありません。爆弾(bottom)が仕込まれた時点を突き止めなければなりません。そのためには(3)Debug.Traceを使って、こんどはIOじゃない、普通の計算をやっている部分を遡っていきます。僕は以下のような小さなデバッグ用のライブラリを作っていつも使っています。

import qualified Debug.Trace as D

debugMode = True

trace::String -> a -> a
trace = if debugMode then D.trace else flip const

watch::Show a => a -> b -> b
watch a b = trace (show a) b

spy::Show a => a -> a
spy a = trace (show a) a

spyの型がid :: a -> aとほぼ同じなので、純粋なプログラムのどこにでも挟むことができます。spyを値が通過したとき、その値が表示されます。

まとめ

Haskellのバグは、えてしてバグを仕込んだ所と出現する所が離れている、凶悪なバグなので困ります。
ついでに、関数を含む型はderiving Showできないのも困ります。
しかしそいつは身勝手というもの。

spyを仕込んでいると、思いもかけない順番でトレースが出てきたり、出てくるはずのトレースが出て来なかったりして、デバッグ作業自身がサブ・パズルになってきます。そのパズルが解けるたびに遅延評価がすこしづつ分かった気になるので楽しいです。


今日のお話は、だいたいshelarcyさんの連載から学んだものです。ありがとうございました。