2013年9月28日土曜日

モナドの実際の使い方

私の感覚としては、モナドはかなりわかりづらい。モナドの定義とか計算法則はわりとわかる。以下サイトがおすすめ。

Haskell の State モナド (1) - 状態を模倣する | すぐに忘れる脳みそのためのメモ:
http://jutememo.blogspot.jp/2009/10/haskell-state-1.html

しかし実際にプログラムを書いてみると、使い方がわからない。特に IO モナドがわからない。実際にプログラムを書いて困った点、それらをどのように解決しているか? ということをまとめる。
今から振り返ると Real World Haskell の内容ちゃんと理解して読んでたら問題にすらならなかったんですけどね。

1. 副作用の分離?

モナドは副作用のある計算を分離するなど言われる。確かに多くのモナドでは純粋な関数からモナドを使用し、run することにより値を取り出すことができ、状態によって結果が変わるかもしれないコードを分離することができる。素晴しい。
ただし、最もよく使われるであろう IO モナドは値を取り出してモナドから純粋な関数に戻ることができない (unsafePerformIO は考えないことにする)。つまり、「IO モナドの関数は IO モナドの関数からしか使用できない」。
例えば以下のようなコード。
-- Test1.hs
import Data.Char

main = do
    let s = func0
    func1 s

func0 = "s"

func1 s = func11 $ s ++ s
func11 = func12 . map toUpper
func12 = putStrLn
func1 > func11 > func12 と続く連鎖のなかで、IO を使用している関数は func12 のみだが、その間で純粋な関数しか使用しない func1, func11 も連鎖して IO モナドの関数になってしまう。
これは非常に扱いづらいように思える。どんなプログラムでも、関数の連鎖の一番先に IO 使用する場合、そこに至る全ての関数は IO モナドにしなくてはいけない。これは 「IO モナドが伝染する」 と言われたりするよう。
上の例だと単純に書き方が下手なだけだけど、例えばネットワークプログラミングをするとしばしば利用するであろう inet_ntoa なんかは副作用なんてあるわけなさそうだが、C の関数を利用するからという理由で IO になっている。実際のプログラムを書くにあたってこの問題は避けて通れないと思う。

2. グローバル変数?

これも副作用を排すという Haskell の基本ルールだが、グローバル変数が使用できない。ただ、これも実際にプログラムを書くといくらでも使いたくなる。こういった性質の変数がないと、「状態」 を使用するプログラムが極端に書きづらくなる。
以下、ユーザから入力を受け取り、その状態に基づき出力結果を変えるプログラムのサンプル。
-- Test2.hs
import Data.Char

main = do
    s <- func0
    func1 s
    func2 s

func0 = do
    putStrLn "Enter some string:"
    getLine

func1 s = do
    putStrLn "Repeat plese:"
    s' <- getLine
    putStrLn $ if s == s' then "Correct"
                          else "Wrong"

func2 s = do
    putStrLn "In upper cases please:"
    s' <- getLine
    putStrLn $ if (map toUpper s) == s' then "Correct"
                                        else "Wrong"
当然だけれど最初に入力した文字列をその後の処理を行う関数に渡す必要がある。実際のプログラムではこの関数がさらに別の関数を呼び…と続き、引数の引き回し地獄となること必須。こんなんだったらグローバル変数使わせてくれ、と思ってしまう。

3. モナドの実際の使い方

では、これらの問題をどうやって解決するかということで、実際のプログラムを見てみる。私の Haskell プログラムのリファレンスはだいたい xmonad です。今回のコードは xmonad-0.11 より。
-- Xmonad/Main.hsc
xmonad :: (LayoutClass l Window, Read (l Window)) => XConfig l -> IO ()
xmonad initxmc = do
    -- snip --
    allocaXEvent $ \e ->
        runX cf st $ do
            -- snip --

            -- main loop, for all you HOF/recursion fans out there.
            forever $ prehandle =<< io (nextEvent dpy e >> getEvent e)

    -- snip --
省略した部分は基本的にメインの処理に至るまでの前準備で、ポイントはメインのループ処理。ここで、ユーザからの入力やアプリケーションの状態変化を受け取り、その状態に応じて処理を行う。この関数は "X" モナドになっており、それが runX によって走らされている。X モナドの定義は以下。
-- XMonad/Core.hs
-- | The X monad, 'ReaderT' and 'StateT' transformers over 'IO'
-- encapsulating the window manager configuration and state,
-- respectively.
--
-- Dynamic components may be retrieved with 'get', static components
-- with 'ask'. With newtype deriving we get readers and state monads
-- instantiated on 'XConf' and 'XState' automatically.
--
newtype X a = X (ReaderT XConf (StateT XState IO) a)
    deriving (Functor, Monad, MonadIO, MonadState XState, MonadReader XConf, Typeable)

-- | Run the 'X' monad, given a chunk of 'X' monad code, and an initial state
-- Return the result, and final state
runX :: XConf -> XState -> X a -> IO (a, XState)
runX c st (X a) = runStateT (runReaderT a c) st
コメントのとおりだが、IO, State, Reader モナドをトランスフォーマーによって組み合わせたものが X モナドである。X モナドの関数の中で get / set (State) すれば XState を操作、ask (Reader) すれば XConf を参照できる。また、X モナドは MonadIO のインスタンスになっている。このことにより、X モナドの関数中では liftIO を使用することにより IO の関数を呼び出すことができ、見かけ上 IO モナドの伝染を防ぐことができる (なお、xmonad では io = liftIO としている)。

上述の問題で悩んでいるとき、State モナドの存在は知っていたけど、メインの処理を全てモナドで包んでやるという発想が出てこなかった。これだと、グローバル変数使うのと何が違うのか? という疑問も出るが、少なくともその変数の所在が明らかになるという点でメリットはあるのではないかと思う。

参考

Real World Haskell Chapter 18. Monad transformers: http://book.realworldhaskell.org/read/monad-transformers.html
本物のプログラマはHaskellを使う - 第29回 グローバル変数の代わりに使えるReaderモナドとWriterモナド:ITpro: http://itpro.nikkeibp.co.jp/article/COLUMN/20090303/325807/

2013年4月24日水曜日

Haskell に興味がある人向け xmonad 設定ガイド



2016/03/12 追記: あらためて読み返してみると,Haskell に関連する記述はだいたい間違っていることがわかりました.xmonad の設定をするうえではあまり困らないことだとは思いますが,その旨ご了承ください.

2013年4月14日日曜日

Linux で USB DAC を使う

いいヘッドフォンを買ったので、USB DAC にもチャレンジしてみた。
購入前はどの DAC が Linux で使えるかよくわからなかったので、調べたことをメモメモ。
環境は以下のとおり。
  • Linux 3.7.10-gentoo
  • ALSA
  • PulseAudio とか JACK は使ってない、よくわからない
まずどの DAC が使用可能かは、Windows や Mac の OS 標準ドライバで動くと謳っているものは (だいたい?) Linux でも使用可能なよう。
USB オーディオの接続方式として、大きく分けると以下の 3 つがある。
  • USB Audio Class 1
  • USB Audio Class 2
  • その他独自方式
USB Audio Class 1, 2 が USB.org で策定されており Linux もこれらに対応しているため、普通に ALSA のドライバで使える。
独自方式にものについては Windows や Mac にも OS 標準ドライバとして搭載できないため別途ドライバをインストールする必要があり、そうすると Linux 向けのドライバがあるものは少ないでしょう。

カーネルコンフィグで有効にするものは以下。
CONFIG_SND_USB_AUDIO=m
これでデバイスを接続するとあっさりと認識された。
$ dmesg
--- snip ---
[   53.016830] usb 3-3.7: New USB device found, idVendor=0909, idProduct=0015
[   53.016833] usb 3-3.7: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[   53.016837] usb 3-3.7: Product: USB Headphone Amp.
[   53.016840] usb 3-3.7: Manufacturer: audio-technica
[   53.017051] usb 3-3.7: usb_probe_device
[   53.017067] usb 3-3.7: configuration #1 chosen from 1 choice
[   53.017213] usb 3-3.7: Successful Endpoint Configure command
[   53.017402] usb 3-3.7: adding 3-3.7:1.0 (config #1, interface 0)
[   53.017526] usbhid 3-3.7:1.0: usb_probe_interface
[   53.017540] usbhid 3-3.7:1.0: usb_probe_interface - got id
[   53.027020] input: audio-technica USB Headphone Amp. as /devices/pci0000:00/0000:00:14.0/usb3/3-3/3-3.7/3-3.7:1.0/input/input11
[   53.027346] hid-generic 0003:0909:0015.0005: input,hidraw4: USB HID v1.00 Device [audio-technica USB Headphone Amp.] on usb-0000:00:14.0-3.7/input0
[   53.027421] usb 3-3.7: adding 3-3.7:1.1 (config #1, interface 1)
[   53.030843] usb 3-3.7: adding 3-3.7:1.3 (config #1, interface 3)
[   53.039448] snd-usb-audio 3-3.7:1.1: usb_probe_interface
[   53.039463] snd-usb-audio 3-3.7:1.1: usb_probe_interface - got id
[   53.049793] usb 3-3.7: Successful Endpoint Configure command
[   53.050427] usb 3-3.7: Successful Endpoint Configure command
[   53.050878] usb 3-3.7: Successful Endpoint Configure command
[   53.051410] usb 3-3.7: Successful Endpoint Configure command
[   53.052305] usbcore: registered new interface driver snd-usb-audio
--- snip ---
$ cat /proc/asound/cards 
 0 [PCH            ]: HDA-Intel - HDA Intel PCH
                      HDA Intel PCH at 0xa0610000 irq 47
 1 [Amp            ]: USB-Audio - USB Headphone Amp.
                      audio-technica USB Headphone Amp. at usb-0000:00:14.0-3.7, full speed
$ aplay -L
--- snip ---
sysdefault:CARD=Amp
    USB Headphone Amp., USB Audio
    Default Audio Device
front:CARD=Amp,DEV=0
    USB Headphone Amp., USB Audio
    Front speakers
surround40:CARD=Amp,DEV=0
    USB Headphone Amp., USB Audio
    4.0 Surround output to Front and Rear speakers
--- snip ---

実際に音を出すには、ALSA のデフォルトの出力先か、アプリケーション単位で出力先デバイスを設定する必要がある。前者は ~/.asoundrc に設定すれば良い。
# ~/.asoundrc
pcm.!default sysdefault:Amp
この設定はアプリケーション起動時のものが反映されるようなので、ノート PC を使用している時など接続変更が発生する時でも、接続後に設定、アプリーケション起動としてやれば良い。

USB DAC をつないで音が良くなったか? というのは別問題。耳が追いついていない。

参考

第1回 USB-DACといっても必ずしも同じではない | Gaudio+PCオーディオfan: http://www.pc-audio-fan.com/special/usb-dac-research/20101115_1438/
USB.org - Approved Device Class Document Download: http://www.usb.org/developers/devclass_docs
What's Best Forum - USB audio: http://www.whatsbestforum.com/showthread.php?2932-USB-audio
.asoundrc - ALSA wiki: http://alsa.opensrc.org/.asoundrc

2013年4月12日金曜日

QuickCheck を使う

Real World Haskell の Chapter 11 に QuickCheck の使い方が載っていて、これは素晴らしいと思ったのだけれど、一部記載が古いバージョン (QuickCheck 1) に関するものであり現行バージョン (QuickCheck 2) でそのまま使えない箇所があったため、補完的な意味で書いてみる。
動作環境は以下のとおり。

  • ghc-7.4.2
  • QuickCheck-2.4.2

1. 概要

QuickCheck はユニットテストを行うための仕組みで、テストにランダムな値を生成して与えることができる。具体的な値を決めてテストを実施するわけではないので、テスト通過可否はその性質によって判断する。
以下はクイックソートの例。クイックソート本体 (qsort) の他に、qsort の動作を確認するための関数 prop_sorted を記述している。これは qsort 後のリストが昇順にソートされているかを確認する。
-- file: QuickSort.hs
import Data.List
import Test.QuickCheck

qsort :: Ord a => [a] -> [a]
qsort []     = []
qsort (x:xs) = qsort lhs ++ [x] ++ qsort rhs
    where lhs = filter  (< x) xs
          rhs = filter (>= x) xs

prop_sorted :: Ord a => [a] -> Bool
prop_sorted xs = sorted $ qsort xs
    where sorted (x:xs@(x':_)) = x <= x' && sorted xs
          sorted _             = True
GHCi 上でテスト実施。prop_sorted に 100 通りの値を与えて qsort の動作を確認している。
ghci> :l QuickSort.hs
[1 of 1] Compiling Main             ( qsort.hs, interpreted )
Ok, modules loaded: Main.
ghci> quickCheck (prop_sorted :: [Integer] -> Bool)
+++ OK, passed 100 tests.
具体的には、こんな値を与えている (verboseCheck で確認可能)。
ghci> verboseCheck (prop_sorted :: [Integer] -> Bool)
Passed:  
[]
Passed: 
[]
Passed:  
[0,-2]
Passed:  
[0,3,0]
Passed:  
[-3,-2,-3,4]
--- snip ---
Passed:   
[-29,68,-24,-85,-7,-76,27,34,65,-31,-62,-27,65,-53,76,-82,24,97,-43,-70,-67,58,
56,-95,-48,18,26,-91,97,-56,33,10,-57,29,-61,-60,-23,-87,77,-9,-30,21,-89,-32,
-42,-73,21,60,65,-9,-76]
Passed:   
[97,-9,-27,42,-30,49,-97,46,-48,11,-87,-47,-36,45,-55,-25,2,-58,-43,82,3,81,97,
-28,-84,75,-13,42,-50,0,7,54,46,-13,4,73,-35,-32,-77,-37,-42,81,-3]
Passed:   
[-78,85,3,18,-67,94,99,-17,-66,94,-1,-83,-96,-68,-31,72,-79,81,47,24,-78,20,-87,
-56,38,65,-69,40,-54,-53,42,96,15,62,55,-10,-89,-83,67,36,-85,-97,71,-28,-3,77,
84,-7,57,-55,11,84,-85,-89]
+++ OK, passed 100 tests.
ランダムテストの良いところは、人間が思いもよらない値をテストに与えることができること、具体的な値を定義する手間が省けること。そもそもコードとテストを書く人間が同じ場合、テストパターンとして思い浮かぶ時点で、コードにもその考慮がされていることが多いんじゃないでしょうか。

2. 値の生成

値の生成は System.Random で乱数を発生させて、それに基づき Arbitrary クラスで定義された arbitrary 関数で当該型の値を生成するという流れになる。なお、Gen モジュールでランダムな値を生成するための便利な関数が用意されているため、arbitrary 関数で直接 System.Random の乱数を扱う必要は多分ほとんどない。以下がキーとなる (と RWH に書かれていた) 関数。
-- module: Test.QuickCheck.Gen
elements :: [a] -> Gen a
choose :: Random a => (a, a) -> Gen a
oneof :: [Gen a] -> Gen a
なお、Gen は state-passing monad ということだがモナドはまだよく理解できていない。とにかく、実際に GHCi 上で値を取り出して挙動を確認するためには sample 関数を使用する。
-- module: Test.QuickCheck.Gen
sample :: Show a => Gen a -> IO ()
GHCi 上でテスト。
ghci> :m +Test.QuickCheck
ghci> sample $ elements [0,1,2,3,5,8]
8
3
1
8
0
5
5
2
3
2
8
ghci> sample $ choose ('a','z')
'h'
'f'
'g'
'q'
'c'
'i'
'a'
'a'
'a'
'r'
'g'
ghci> sample $ oneof [choose (0,10), choose (100,110), choose (200,210)]
207
0
9
207
105
8
106
202
8
202
110
これらを使用して、必要な型の乱数値を生成する。例えば、あらかじめ実装されている Char は以下のようになっている。
-- module: Test.QuickCheck.Arbitrary
instance Arbitrary Char where
  arbitrary = chr `fmap` oneof [choose (0,127), choose (0,255)]
これを使用して独自の型の乱数値を生成する例。良い例が思い浮かばなかったので Real World Haskell の例そのまま。
-- Doc.hs
import Test.QuickCheck  
import Control.Monad (liftM, liftM2)  
  
data Doc = Empty  
         | Char Char  
         | Text String  
         | Line  
         | Concat Doc Doc  
         | Union Doc Doc  
         deriving (Show)  
  
instance Arbitrary Doc where  
    arbitrary =  
        oneof [ return Empty  
              , liftM Char arbitrary  
              , liftM Text arbitrary  
              , return Line  
              , liftM2 Concat arbitrary arbitrary  
              , liftM2 Union arbitrary arbitrary ]  
arbitrary は型推論され、関数に必要な型の乱数値を与えている。Text には String を、Concat には再帰して Doc の値を与える。
ghci> :t arbitrary
arbitrary :: (Arbitrary a) => Gen a
ghci> :m +Control.Monad
ghci> :t liftM Text
liftM Text :: (Monad m) => m String -> m Doc
ghci> :t liftM2 Concat
liftM2 Concat :: (Monad m) => m Doc -> m Doc -> m Doc
また、arbitrary 関数中の oneof .. の部分は以下のように書くこともできるが、oneof ... のほうがすっきりしてわかりやすい。
instance Arbitrary Doc where
    arbitrary = do
        n <- choose (1, 6) :: Gen Int
        case n of
             1 -> return Empty

             2 -> do x <- arbitrary
                     return (Char x)
--- snip ---
sample 関数で生成を確認する。
ghci> :l Doc.hs
[1 of 1] Compiling Main             ( Doc.hs, interpreted )
Ok, modules loaded: Main.
ghci> sample (arbitrary :: Gen Doc)  
Char 'r'  
Line  
Union (Text "Ce") (Char '\194')  
Text "\179lj(\169K"  
Text "\233y\189d"  
Union (Union (Char 'K') (Concat (Concat Empty (Char 'c')) Empty)) (Char 'A')  
Text "\213V\236s\ESC\167\NUL\224\ENQ4u"  
Union (Union Empty (Char '\219')) (Char '\138')  
Line  
Union Line (Concat Empty (Concat (Char 'w') Line))  
Char '}'  

3. テストをまとめて実行する

テストの作り方は上に示したが、テストを複数作ってそれをひとつずつ GHCi で実行していくわけにはもちろんいかないので、まとめる。関数の中にひとつずつ quickCheck prop_hoge と記述していっても良いが、全部のテストをまとめて行うための仕組みも用意されている。{-# LANGUAGE TemplateHaskell #-} したうえで $quickCheckAll を実行するだけ。
-- TestSuite.hs
{-# LANGUAGE TemplateHaskell #-}

import Test.QuickCheck
import Test.QuickCheck.All

prop_plus1 :: Integer -> Bool
prop_plus1 x = x + 1 > x

prop_abs :: Integer -> Bool
prop_abs x = abs x >= 0

prop_square :: Integer -> Bool
prop_square x = x ^ 2 > x

main = $quickCheckAll
これで全てのテストが実行される。prop_square は失敗するテストの例。
$ runghc TestSuite.hs 
=== prop_plus1 on TestSuite.hs:6 ===
+++ OK, passed 100 tests.
=== prop_abs on TestSuite.hs:9 ===
+++ OK, passed 100 tests.
=== prop_square on TestSuite.hs:12 ===
*** Failed! Falsifiable (after 1 test):  
0
False
一番大事なことは、テストを書くことをめんどくさがらないことですかね。

参考

Real World Haskell Chapter 11. Testing and quality assurance: http://book.realworldhaskell.org/read/testing-and-quality-assurance.html
Introduction to QuickCheck2: http://www.haskell.org/haskellwiki/Introduction_to_QuickCheck2