「ほっ」と。キャンペーン

<   2010年 10月 ( 26 )   > この月の画像一覧

再帰関数によるループを Ruby で

Haskell で再帰関数によループを記述しているうちに、Rubyでもできるのではないかと考えた。

$ irb
irb(main):001:0> def loop
irb(main):002:1> cs = gets
irb(main):003:1> puts cs
irb(main):004:1> loop
irb(main):005:1> end
=> nil
irb(main):006:0> loop
hello
hello
world
world

確かに動いた。ループを再帰関数で実現するのは、何もHaskell に特有のものではなく、ループの記述の方式の一つであることが分かる。手続き型言語ではあまり利用していなかった(スタックを消費するため)だけだったのだ。
[PR]
by tnomura9 | 2010-10-31 23:05 | Haskell | Comments(0)

IOモナドのループ処理

IOモナドのプログラミングは、基本的にIOモナドの関数を>>=演算子でつないでいくだけだということが分かった。

しかし、そうなると困ったことになる。ループ処理が記述できないのだ。関数を>>=演算子でつないでいく方法だと、処理は一方向にしか流れていかない。しかし、ループ処理の記述できないIO処理は役に立たない。いろいろとネット上をさまよったが、ループ処理を端的に解説してくれる記事が見つからなかった。

ただし、「Haskell ではループは全て再帰関数で記述する。」というコメントがあったので、無限再起する関数を書けばいいのではないかと思った。そこで、Hugsで次のように入力してみたが、show関数がないというエラーが出て動かなかった。

Main> loop where loop = do {getLine>>=putStrLn; loop}
ERROR - Cannot find "show" function for:
*** Expression : let {loop ($0 (0 do {...}) $0) $0 $0} in loop
*** Of type : IO a

いろいろ悩んだが、loop 関数もIOモナドの関数にするためには、IO型の戻り値を返さなければならないのだから、コードの最後に return () を入れて、IO () 型の戻り値を返すようにしてみたら、これがうまくいった。

Main> loop where loop = do {getLine>>=putStrLn; loop; return ()}
hello
hello
world
world
^C{Interrupted!}

これで無限ループができたので、回数限定のループを作ってみたが動いた。

Main> loop 2 where loop x = do {getLine>>=putStrLn; if x > 0 then loop (x-1) else return ()}
one
one
two
two
three
three

次に、入力が””ならループを脱出するようにしてみた。

Main> loop where loop = do {cs <- getLine; putStrLn cs; if cs == "" then return () else loop; return ()}
hello
hello
world
world

やはり、IOモナドのプログラミングには、手続き型の考え方を持ち込んではいけないようだ。ループ処理させることはできるが、それは、無限の再帰を行う関数を記述することでおこなう。>>=演算子を挟んだ処理は絶対に後戻りできないことを心に止めておかねばならない。

IOモナドのプログラミングは、一見手続き型言語風に見えても、あくまでも関数言語のプログラミングだ。手続き型言語の発想をそのままIOモナドのプログラミングに持ち込もうとしたために、IOモナドのプログラミングが必要以上に難しく写ってしまった。

Haskell を扱う上で大切なのは、全てを関数で考えるという発想の転換なのだ。
[PR]
by tnomura9 | 2010-10-31 08:44 | Haskell | Comments(0)

IOモナドの >> 演算子

IOモナドのプログラミングは基本的にIOモナドの関数を >>= 演算子でつないでいくことになる。したがって、前段の関数の出力(厳密にはIO a 型のパラメータ)の型と、後段の関数の引数の型が一致していなければならない。したがって、前段の戻値と後段の引数の型があわないとき、型エラーになってしまう。

例えば、次のようなプログラムはエラーになる。

Main> putStr "input name: " >>= getLine >>= putStrLn
ERROR - Type error in application
*** Expression : putStr "input name: " >>= getLine
*** Term : getLine
*** Type : IO String
*** Does not match : a -> b c

つぎのように、putStr は、IO () 型のデータを返すのに、getLine は引数を取らないからだ。

Main> :info putStr
putStr :: String -> IO ()

Main> :info getLine
getLine :: IO String

こういう時は、>>= 演算子の代わりに >> 演算子を使う。>> 演算子は前段の関数の戻値を無視して、後段に伝えない作用があるからだ。したがって、上のプログラムは putStr "input name: " >> getLine のように >>= 演算子ではなく、>> 演算子を使うとうまく動く。

Main> putStr "input name: " >> getLine >>= putStrLn
input name: Dolly
Dolly

do 記法の時は >>= と >> の選択は自動的に起きるようなので次のように書いてもエラーが出ない。そのため、一見手続き型のプログラムのようにみえるが、IOモナドが >>= 演算子を介してデータの受け渡しをやっているということが忘れられやすい。

main = do
  putStr "input name: "
  cs <- getLine
  putStrLn cs

また、余計な変数名を考えないでいい分、パイプライン型のプログラムのほうが楽な気がする。
[PR]
by tnomura9 | 2010-10-29 11:47 | Haskell | Comments(0)

IOモナドでパイプラインプログラム

IOモナドでパイプライン処理のプログラムを作ってみた。

パイプライン処理とはUnixのフィルタープログラムで使われている方法で、プログラムの出力を次のプログラムの入力につなげて処理をしていくやり方だ。IOモナドを使ったプログラムでも同様の方法を使うことができる。というより、do 記法の中の一見手続き型のプログラムも、実は、IOモナドの関数を>>=演算子によるパイプライン処理で繋いだものだ。

したがって、IOモナドでプログラミングするときパイプライン処理を意識してプログラムすれば楽なのだ。Hugsを使ってワンライナーでパイプライン処理のIOモナドプログラミングをしてみた。

まずはじめに、コンソールから文字列を入力してそれを出力するプログラムを書いてみた。

Hugs> getLine >>= putStrLn
hello
hello

パイプラインの中を流れるデータはプログラムのコードからは明示的には見えないが、getLine でコンソールから入力された文字列が >>= 演算子を介して putStrLn に渡され、それがモニタに表示されたと考えると分かりやすい。

つぎは、入力した文字列を加工してみる。

Hugs> getLine >>= hello >>= putStrLn where hello cs = return ("hello, "++cs)
world
hello, world

これは、getLine で入力した文字列に"hello, "を付け加えて putStrLn に渡している。上の hello という関数が自前のIOモナドで、where 以下に定義してある。例によって引数をひとつとりIO型のデータを return 関数で返している。

次のプログラムではコンソールから整数のリストを読み込み、その和を計算している。

Hugs> getLine >>= lread >>= return . sum >>= print where lread ls = return (read ls :: [Int])
[1,2,3,4,5]
15

getLine でリストの文字列を読み込み、lread 関数で [Int] 型のデータに変換し、sum 関数でその和を取り、print 関数で画面に出力している。return . sum のように、Haskellの世界の関数を return 関数を使ってIOモナドの世界に移していることを注目して欲しい。

つぎは、コンソールから文字列のリストを入力し、それをソートする。

Hugs> :l List
List> getLine >>= lread >>= return . sort >>= print where lread ls = return (read ls :: [String])
["hello","world","how","are","you"]
["are","hello","how","world","you"]

このように、IOモナドのKleisli圏の関数を >>= 演算子で合成していくことによってIOモナドのプログラミングを行っていくのを意識すると、IOモナドのプログラミングが楽しくなる。

Haskellは純粋関数型のプログラム言語で、IO関係の処理もやはり関数で記述される。手続き型のプログラムの発想を一旦捨てて、関数型の発想では同じようなことがどのようにできるのか考えると、IOモナドのプログラミングに対する不満も解消されるのではないだろうか。

要は慣れの問題で、いろいろなプログラムを関数型プログラムの発想から作っているうちに、IO関係の処理も自然に関数型のプログラムで記述できるようになるだろう。
[PR]
by tnomura9 | 2010-10-29 07:50 | Haskell | Comments(0)

モナドのKleisli圏

圏論からHaskellのIOモナドへの最短距離の近道を示してくれる文書を見つけた。

モナドへの近道・Haskell からの寄道』 中村翔吾著

がそれだ。数学的にきちんと説明してあるので、読んですぐ理解できるようなものではないが、何となくIOモナドの考え方の雰囲気のようなものは伝わった気がする。

大げさな話になるが、この世界は何でできているかというと、いろいろな物とそれらのあいだの関係で成り立っていると言ってもいい。すなわち、世界のモデルの雛形として、集合Xと集合YとX->Yの関数 f(x) の集まりである関数の集合 Hom(X,Y) を考えることができるということだ。

たとえば、集合 X={1, 2} と集合 Y={a, b} からなる世界があり、X->Yの関数を集めた集合、Hom(X,Y) ={f, g} があったとする。すると、X, Y, Hom(X,Y) の三つの組みでこの世界は成り立っていると言えるだろう。これを圏 C と呼ぶことにする。ところが、また、別のところに、集合V={3, 4}と集合W={v, w}とそれらの関数Hom(V,W)={j, k}からなる圏 D という世界があったとする。

当然興味が湧くのは、そのような圏 C と圏 D の構造が同じものなのか、違うものなのかということだ。これをどうやって調べるのかというと、圏Cの要素から圏Dの要素への写像Fを作ってみればいいのだ。Cの世界の集合XはF(X)によって、集合Vに写像する。また集合YはF(Y)によって、集合Wに、Hom(X,Y) はF(Hom(X,Y))によって、Hom(V,W)に写像してやる。このような写像Fが存在すれば、圏Cと圏Dの間には構造的な類似があることがわかる。このFのことを圏論では関手と呼ぶ。

ところで、Hom(X,Y)の要素、f g にはその合成 f * g を定義できることを前回のエントリーで示した。また id * f = f * id = f となるような単位元の関数 id も存在することを示した。実は関手 F はF(idx) = idy, F(g*f) = F(g) * F(f) のように、単位元と関数の積の関係が保存されている必要がある。IOモナドで、return 関数が単位元で、bind 演算子が積にあたる。

要するに、関数の単位元や合成といった性質を保存しながら、圏Cの世界を圏Dの世界へ映しだすのが関手Fの機能だ。

上の例では関手Fは圏Cを圏D上に写像していたが、逆方向の圏Dから圏Cへの関手Gも考えることができる。この際に関手Fと関手Gの合成写像GFは圏Cから圏Cへの写像となる。したがって圏Cの対象cとGFの像GFcとの間の関数ηを考えることができる。同様に圏GFの像と圏Gの対象gとの間の関数εを考えることができるが、これによって、

F = εFη = F や G = ηGε = G

のようにGやFの前後に適用することによって関手の恒等関数としてはたらくことのできる自然変換、ηとεを取ることができる。この、関手F,関手G、η、εの4つを組みにして、随伴<F, G, η, ε>という。

これまでは、圏Cと圏Dの間の関手を考えてきたが、圏Cから圏Cへの関手Tも考えることができる。この場合は圏Cの自分自身の姿を、自分自身の中に映し出すことになる。合わせ鏡のようなものだ。このような関手Tでは随伴は<T, η, μ>となり、これをモナドと呼ぶ。関数ηは単位元、関数μは乗法である。

モナドの関手や単位元や乗法のほんとうの意味はわからなかったが、モナドという名前が指しているのが、圏でも、圏の関数でもなく、関手と単位元と乗法の三組をさすものだということがわかったのは収穫だった。

このモナドが決まると、このモナドから派生するKleisli圏というのが決まる。このKleisli圏の特徴が次のようなものらしい。

• XT の対象はX の対象である,
• 射の集合XT (xT , yT ) はX(x, Ty),
• XT における対象xT の恒等射は,ηx : x → Tx,
• fT ∈ XT (xT , yT ), gT ∈ XT (yT , zT ) の合成はμz ◦ TgT ◦ fT : x → Ty → T2y → Tz.

これをよく見ると、IOモナドに出てくる馴染みのある表現が見られる。例えば、Kleisli圏の射は引数が x で戻値が Ty つまり IO a だ。また、恒等射 nx は return :: x -> IO x だし、合成の型は a -> IO b -> b -> IO c というふうに写像されていく。IOモナドの単位元(return) とbind演算子(>>=) はKleisli圏の性質そのものだったのだ。

上の定義でIOモナドの関手Tと単位元とbind演算子は既定のものだから、プログラマが操作するのは、個々の射(関数)だけだ。そうして、個々の射はX(x, Ty)の要素なので、「引数一つ、戻値IO」という規定を満たしてさえいればいい。

詳しい数学的な意味はわからなかったが、結局モナドとは、Haskellのいろいろな型のデータとそれらに関連する関数からなる圏Cの、それ自身に対する関手(圏から圏への写像)Tと単位元、合成の三組を指すことがわかった。また、その三組によってKleisli圏が定められる。Kleisli圏の射(関数)は「引数ひとつ、戻値IO型」の関数で、これらを合成することによって様々なKlieisli圏の関数を作ることができるが、そうして出来上がった関数がHaskellのIOモナド関連のプログラムだ。

結局、IOモナドとはHaskellの世界をIOモナドのKleisli圏という世界にIOモナドの関手によって映写して、副作用による影響をIOモナドの世界に閉じ込めてしまう方法だったのだ。おまけにその方法はあっけないほど簡単だ。一変数のHaskell世界の関数を return 関数でIOモナドのKleisli圏の関数に変えてやるだけだ。

IOモナドを関数型に埋め込まれた手続き型のプログラムととらえると、意味の分からないエラーがでて困惑するが。IOモナドのプログラムは結局IOモナドのKleisli圏の射(関数)を>>=演算子で組み合わせたものだと考えると、プログラムの発想がずいぶん楽になるのではないだろうか。それも、特殊な発想ではなく、Unixのパイプライン処理で経験済みの方法だ。前段の関数のデータを後段のフィルターにつないでやるというあの感覚だ。

「引数ひとつ、戻値IO型」というルールはどうも理論的にも根本的なようだ。理論がどうあれ、この「引数ひとつ、戻値IO型」のルールでIOモナドの自前の関数をつくれば、IOモナドを自在に活用できるようになるだろう。
[PR]
by tnomura9 | 2010-10-26 08:41 | Haskell | Comments(0)

圏論とIOモナド

オブジェクトとして集合X={1}、射としてX->Xの関数f(x)=xひとつだけを持つ圏は、圏や関手のイメージを掴むのに便利だった。しかし、もう少し複雑な圏にも挑戦しよう。

オブジェクトとして、集合X={1,2}と集合Y={a,b}という二つの集合をとり、射として X->Yの関数 id, f の二つの関数を持つ圏 C を考えてみよう。この場合 Hom(X,Y)={id, f} である。このとき、id, f をそれぞれ次のように定義する。

id(1)=a, id(2)=b
f(1)=b, f(2)=a

これが圏になるためには、id と f の合成 * が定義されている必要があるが、id も f も共に X->Y 型の関数であり、このままでは合成写像を作ることができない。そこで、技巧的だが関数と関数の合成をする演算子 * をつぎのように定めることにする。

f * g は、gの値が a の時は、f の引数として1を渡し、gの値が b の時は、f の引数として2を渡す。

このように定義すると、Hom(X,Y)の関数の合成ができて、たとえば、

(f*f)(1)=f(*f(1))=f(*b)=f(2)=a

のように合成関数を作ることができる。また、このように演算子 * を定めると、次の結合法則が成り立つ。

(h * g) * f = h * (g * f)

また、

(id * f) (1) = id * (f(1)) = id *(b) = id (2) = b = f(1) から id * f = f が、

f * id (1) = f * a = f(1) から f * id = f がわかる。

これらのことから C が圏であることが分かる。

ところで、圏 Cの事情はIOモナドに通じるものがある。いま、集合XをHaskellの通常の型のデータ全てからなる集合、集合YをIO型のデータからなる集合と考えると、X->Y の関数の全てからなる集合について、演算子 >>= によって結合法則、

(h x >>= g ) >>= f = h x >>= (g >>= f)

が成立し、return 関数についてみると、

return x >>= f = f x, f x >>= return = f x

となって、return 関数がIOモナドの単位元になっていることが分かる。したがって、IOモナドは圏の条件を満たしている事が分かる。「引数ひとつをとり、IO型を返す」という単純なルールは、その関数がIOモナドの関数であるという条件を満たしている事を理論的にも保証しているのかもしれない。

圏論を理解することがHaskellを使うための必須条件なら、この便利なプログラム言語を使うことのできる人は非常に限られてくるだろう。しかし、「引数一つをとり、IO型を返す」という単純なルールにしたがってコーディングするだけでIOモナドが使いこなせるのなら、これを使わないという手はない。
[PR]
by tnomura9 | 2010-10-24 23:18 | Haskell | Comments(0)

圏論

圏論が気になって仕方がない。Haskellをやり始めなかったら、言葉も知らなかったと思うのだが、IOモナドをきちんと理解しようと思うとやはり圏論の知識が必要なのではないかと思った。

ネットで検索した圏の定義は次のようなものだ。

「対象」とよばれるモノ( = 数学的な対象 )のクラスと対象 X から Yへの
「射(morphism)」とよばれる要素からなる集合 Hom(X, Y) が指定されていて、f ∈ Hom(X, Y)
と g ∈ Hom(Y, Z) に対してはその「合成」とよばれる射 gf が定められていて、以下の3条件を
満たしているときこれらの総合概念 C を 圏(category)とよぶ。

 (Cat 1) 射の合成は結合法則 h(gf) = (hg)f をみたす.
 (Cat 2) 任意の対象 X に対して ∃ε∈Hom(X, X) ∀f∈Hom(X, Y)∀g∈ Hom(Z, X), fε=f, εg=g
 (Cat 3) 対象のペア (X, Y), (X', Y') が異なる ⇒ Hom(X, Y) ∩ Hom(X', Y') = φ

とりつくしまもない。

抽象的なものを抽象的なまま理解するのは特殊な能力が必要だろう。一般人は、何か具体例がないと理解出来ない。そこで、要素1だけを含む集合XにX->Xの写像f(x)=xを考えてみた。明らかにf(1)=1で、要素は1だけしかないので、f(x)の値も1しかとらない。

この集合X={1}、とf(x) = x の組み合わせはどうも圏になるようだ。上の定義に当てはめると、対象はXで、対象Xから対象Xへの写像 f(x)=x は射であるということになる。X->Xへの射を全て集めたものが、Hom(X,Y)だが、X->Xの写像はf しかないから、Hom(X,X)={f}だ。また、関数fについてはその合成関数ffを、ff(x) = f(f(x)) = f(x) = x のように定義できるから合成ffが存在することがわかる。

したがって、f(ff) = (ff)f は、f(ff)(x)= f(ff(x))=f(f(f(x)))=f(f(x))=f(x)=x のように展開することで簡単に結合法則を満たすことを証明できる。

また、e=f と読み替えると、ef(x) = e(x) = x = f(x) = f(e(x)) = fe(x) だから、ef = f = fe となる。また、全てのHom(X,X) は f ひとつしかないので全ての f ∈Hom(X,X)について、e が単位元であることがわかる。

したがって、集合{X}とf:X->X の組は圏であることがわかる。

関手については次のように定義されている。

圏 C の対象 X に圏 D の対象 F(X), 圏 C の射 f:X → Y に圏 D の射 F(f):F(X) → F(Y)
を対応させる(クラス間の)写像 F が

 (F1) F(gf) = F(g)F(f)
 (F2) F(1X) = 1F(X)

を満たしているとき F を C から D への 共変関手(covariant functor)
もしくは単に 関手 とよび F:C → D と書く。
C から D への関手の全体を Hom(C, D) で表す。
共変関手の双対として 反変関手(contravariant functor)が定義される。

そこで圏Cに{1} とf(x)=x からなる圏をとり、圏Dに {2} とg(x) = x からなる圏をとる。するとF(1) = 2、F(f) = g となる関手Fを考えることができる。対象の関数Fと射の関数Fは同じ名前だが違う関数だ。すると、ff(x)=f(f(x))=f(x) から、ff=f なので、F(ff)= F(f) である。また、gg(x)=g(g(x))=g(x) なので、gg=g つまり、F(f)F(f) = F(f)となるから、F(ff) = F(f)F(f) となり、関手によって射の合成が保存されるのがわかる。F2の単位元の性質も関手によって変更されないことがわかる。

従って、圏Cは、関手Fによって、その性質を変えないで圏Dへ写像されることがわかる。

これでなんとなく圏や関手の意味がわかったような気がする(かもしれない)。
[PR]
by tnomura9 | 2010-10-24 19:06 | 考えるということ | Comments(0)

Windows に Haskell Platform をインストールした。

Real World Haskell を読み始めたが、GHCを使う想定になっていたので、The Haskell Platform からダウンロードした。今のところWindows7上でも問題なく動いている。

インストールすると、スタートメニューに GHCi のアイコンが登録される。アイコンをクリックすると、GHCi が起動するが、そのままでは、:edit で編集したプログラムを :load でロードできない。そこで、:cd コマンドで、パーミッションのあるディレクトリにカレントディレクトリを変更する必要がある。

Prelude> :cd C:\Users\user_name\Documents\Haskell

これで、Real World Haskell のサンプルを :edit コマンドで作成して、:load コマンドで GHCi に読み込むことができるようになる。ただし、Hugsとは操作が少し違うので注意が必要だ。たとえば、Hugsではコマンドラインで where を使うことができるが、GHCi ではそれができない。逆に、Hugsでは関数の定義はすべてファイルに作成しなくてはいけなかったが、GHCi では、ワンライナーなら let 命令を使って、コマンドラインから定義することができる。

Prelude> let fact n = if n == 0 then 1 else n * fact (n-1)
Prelude> fact 5
120

また、Windows 用のGHCiでは、Linux のターミナルと同じようにTabキーでキーワードの補完ができるし、矢印キーやCTRL+Pで以前に入力した内容を呼び出すことができる。

これだけを知っていれば、後はネット上の入門記事や、チュートリアルを読みながら実行させることができるはずだ。

Haskellはいくつかの注意点に気をつけていれば、わりに抵抗なく入っていける。その注意点とは次のようなものだ。

  1. GHCをインストールする。GHCiのカレントディレクトリを :cd で上記のように変更する。
  2. GHCi のプロンプト上でまず電卓として使ってみる。100 * 1.02 ** 10 のような計算でも電卓ではやる気がしないだろう。
  3. 簡単な計算式の関数を定義してみる。bmi w h = w / h / h は体重w(kg)と身長h(m)からBMIを計算する式だ。BMI > 25 以上は肥満症だ。これも電卓でやるより簡単だ。
  4. 上のような式を、:edit コマンドと :load コマンドを使ってファイルに記述して動かせるようにする。(これまでの4つのポイントをクリアすれば、GHCiは十分実用的に使える。)
  5. リスト関係の関数の働きを覚える。
  6. 階乗の関数を定義できる。パターンを使ったリスト操作の関数を定義できる。
  7. 関数の動作を知るのに、式の置き換え(展開)を手動でやってみて、関数の動作が全てこのような形式的な置き換えの操作で説明できることを知る。
  8. 関数の定義の際にきちんと、型の定義をすることができる。
  9. getLine、putStr などの簡単なIOモナドを使ってみる。do 記法だけではなく、>>= 演算子でIOモナドを結合させてみる。
  10. IOモナドが「引数一個で、戻り値IO型」と割り切って、自前のIOモナドを作り、標準のIOモナドと組み合わせてみる。

Haskellは関数の発想に馴染んでくれば、記述の簡潔さから手放せなくなる。また、Haskellには変数への代入がないので、変数名を考える手間が少なくなるのもお気楽プログラミングの理由のひとつだろう。今のところ、日本語の教科書が少なかったり、日本語の文字列が扱えないなどの問題はあるが、それも何年もしないうちに解決されるだろう。IOモナドのせいであまり評判がよいとはいえないが、「一引数、IO戻り」とIOモナドを割り切って使ってみれば、そんなに高尚な知識は要らないし、感覚的に使えるようになるのではないだろうか。

要するに、一般の関数はIO型の入力は受け付けないので、IOモナドの出力が一般の関数に渡されることはないし、一般の関数の戻値はIO型ではないので、IOモナドの関数の入力とすることができない。したがって、副作用を生じるようなIOモナドの関数の入出力は、IOモナド間でしか伝わらない。これが、IO関係の副作用が、純粋関数と隔離されるという意味だ。

また、IOモナドの連結は >>= 演算子で行うが、>>= 演算子は、必ず左辺が実行されないと、右辺へ値が渡されることはない。したがって、Haskellの遅延評価の機能をそのままにして、実行順序の保存が保証されると考えればよい。do 記法の中の一見手続き型的なプログラムは、実質は >>= 演算子でつながれたIOモナド関数の並びである。したがって、無理に手続き的な発想を持ち込むよりは、IOモナドの連結をイメージしてプログラムを作ったほうが分かりやすい。

以上のように、IOモナドは理論上の分かりにくさとは異なって、ひとつの引数をとり、IO型の値を返す関数だと考えておけば、IO処理を含んだプログラムを記述するのはそれほど難しくはない。

Haskell は使って見れば、きっと面白くなるはずだ。
[PR]
by tnomura9 | 2010-10-21 03:11 | Haskell | Comments(0)

普通の関数をIOモナドに変える

IOモナドが嫌われる理由の一つは、IOモナドの外で定義した普通の関数がIOモナドの中では使えないことだろう。これも、IOモナド型関数とは「引数を一つだけとり、IO型の戻り値を返す。」という観点から考えると、簡単に解決することができる。つまり、引数が一つだけの関数があれば、その関数を return 関数と合成してやればそれはそのまま、IOモナド型関数になってしまうということだ。

例えば、("hello, " ++) という関数は、String 型の変数をひとつとり、文字列 "hello, " と連結した文字列を返す関数だが、

Hugs> ("hello, " ++) "Dolly"
"hello, Dolly"

これを、return 関数と合成した return . ("hello, " ++) はIOモナドになってしまう。したがって、標準のIOモナド型関数と >>= 演算子で連結することができる。

Hugs> getLine >>= return . ("hello, " ++) >>= putStrLn
Dolly
hello, Dolly
普通の関数をIOモナド型関数にしてしまえば、どんなものも >>= 演算子でつなげてしまうことができるので、プログラム作成がちょうど Unix のパイプライン処理のようにモジュールをつなげる操作になってしまう。

Hugs> :l List
List> getLine >>= return . (read :: String -> [Int]) >>= return . sort >>= print
[2,3,5,1,6]
[1,2,3,5,6]

上のプログラムは、次のように手続き型のプログラムに似せて作ることができるが、簡単な例なら do 記法よりは >>= 演算子でつないでやった方が楽だ。 do 記法の時は、return 関数との合成は $ 演算子で行う。(上の例では関数の後ろに引数がないから . で合成できる。)

import List

main = do
   cs <- getLine
   lst <- return $ (read :: String -> [Int]) cs
   print (sort lst)

このように、IOモナドを「一つの引数をとり、IO型の戻り値を返す関数」ととらえただけで、簡単に既存の関数もIOモナド型関数化して利用できるようになる。
[PR]
by tnomura9 | 2010-10-19 08:25 | Haskell | Comments(0)

メタ思考

公理から定理を演繹して導きだすのが論理学だ。さらに、その論理学がどのようにして論理的体系を形成できるのか、論理学的体系に矛盾がないことをどのようにして知るのかなど論理学について調べるのがメタ論理学だ。

思考の場合も、思考によって問題解決をするのが直接的な思考だが、どうしてそのように思考したら成功したのかあるいは失敗したのかを考えるメタ思考的な態度が、思考力を鍛えるのに大切だ。

例えば数学の問題を間違えたとする。解答をみて正解を知ってそれを記憶すれば同じような問題は正解を出せるようになるかもしれない。しかし、その時に、どうしてその問題を間違ってしまったのか、あるいは解法の鍵となる公式をなぜ思い出せなかったのか、その問題のパターンを見つける方法は他になかったのかなどと、自分の思考過程について色々と考えてみると、問題やそれを解くときの思考法のコツのようなものに気づくことがある。

また、教科書を読んでいる時も、一通り読んだ後、この知識の本質は何だろうか、関連の知識とどのようなつながりがあるのだろうか、著者はこの内容を伝えるために文章の構成などにどのような工夫をしているのだろうかなどとあれこれ考えるのも、一種のメタ思考になる。

学習や思考によって得られた知識について、さらに、メタ思考の立場からあれこれ検討してみる習慣をつけると、表面的な知識にならずに知識の深みを増すことができる。
[PR]
by tnomura9 | 2010-10-18 22:43 | 考えるということ | Comments(0)