今回説明するテーマは「多相性」です。英語ではpolymorphism(ポリモーフィズム)になります。多態性や多様性などと呼ぶこともあります。オブジェクト指向をご存じの方ならおなじみの言葉ですね。
多相性は「ある関数や型を,複数の型に対して使用できる」という性質を示す言葉です。こうした性質を持つ関数であれば,数値計算や文字列の連結,I/O(入出力)処理など一見全く違うように見えるような処理を,あたかも同じもののように扱うことができます。
なお,この連載ではその回の理解に必要な知識は解説していきますが,すべての機能を網羅的に説明していくことはしません。Haskellについて体系的に学習したければ,入門書や「Haskell 98 言語とライブラリ 改訂レポート」(原文はLanguage and library specification)を見てください。
前回の補足 この連載では,前回の記事に関して状況が変化したり誤りが判明した場合,補足説明や訂正,追加情報などを加えていきたいと思っています。 まず,記事の紹介です。2006年8月24日に発売されたWEB+DB PRESS vol.34という雑誌に,著名なPerlハッカーの小飼弾さんが書いた「関数型言語Haskell入門」というHaskellの入門記事が掲載されました。日本のHaskellハッカーの草分けである山下伸夫さんが監修しています。9ページと短い記事なので,Haskellがはじめてという方にとってはよいきっかけになるでしょう。 あと1点,補足があります。前回のコラムでWindowsではHGLがきちんと動かないことに言及しました。こうした状況を考慮して,Windows版のGHCからはHGLが取り除かれることになりました。2006年9月1日にRelease Candidate版が公開され,近々リリースが予定されているGHC6.6からは,WindowsでHGLを利用することは完全にできなくなります。 |
適切な型を推論してくれる「型推論」
では,多相性について説明していきます。まずは処理系の対話環境を使って,前回定義したrepeatedの型を見てみることにしましょう。repeatedは「指定した回数分,ある関数を適用する関数」でしたね。
今回は処理系を使って動作を逐一説明していきます。ぜひ動かして試しながら読んでみてください。一読するだけではすぐにはわからないところも,試しながら読めば,案外すんなり理解できると思います。
Haskellの主な処理系は二つありますが,説明の都合上,今後は基本的にGHCを使用することにします。GHCの対話環境は,ghciコマンドを実行するか,ghcコマンドに--interactiveオプションをつけて実行することで起動できます。Windowsでは「スタート」→「プログラム」メニューの「GHC」→「6.4.x(バージョン番号)」→「GHCi」を選んでも起動できます。Mac版をインストーラを使ってインストールした場合は,Finderから「アプリケーション」内にある「Glasgow Haskell Compiler 6.4.1(バージョン番号を含んだGHCの名前)」ディレクトリ配下のGHC Interactive System (Haskell 98).appまたはGHC Interactive System (with Extentions).appをダブルクリックして起動します。両者の違いは,標準Haskell(Haskell 98 Revised)の範囲外にあるGHCの拡張機能を有効にするかどうかという点です。今回は拡張機能を使用しないので,どちらから起動してもかまいません。
$ ghci ___ ___ _ / _ \ /\ /\/ __(_) / /_\// /_/ / / | | GHC Interactive, version 6.4.2, for Haskell 98. / /_\\/ __ / /___| | http://www.haskell.org/ghc/ \____/\/ /_/\____/|_| Type :? for help. Loading package base-1.0 ... linking ... done. Prelude>
対話環境を終了させるには,:quitコマンドを使います。GHCiやHugsには,コマンドの最初の頭文字を使った省略形も用意されています。例えば:quitと:qは同じ動作になります。なお,GHCiとHugsの基本的な使用法の多くは共通です。なので,特に「GHCiの」とか「Hugsでは」といった断り書きがなければ,同じように使用できると思っていただいてかまいません。
Prelude> :quit Leaving GHCi.
それではrepeatedの型を確かめてみましょう。GHCiの対話環境内で関数を定義する場合には,以下のようにletの後にスペースを空けて入力する必要があります。
Prelude> let repeated f n = \x -> (iterate f x)!!n Prelude>
なお,もう一度letを付けて同じ名前の関数を定義することで,前の関数定義を現在の環境のスコープから隠して新しい定義に置き換えることができます。
letを付けずに定義しようとしたり,letとの間にスペースを入れ忘れると,エラー・メッセージが表示されます。
Prelude> repeated f n = \x -> (iterate f x)!!n <interactive>:1:13: parse error on input `=' Prelude>
今回は,repeated関数の中で前回説明してなかった部分について説明しようと思います。\x -> (iterate f x)!!nの部分は「ラムダ抽象(lambda abstraction)」と呼ばれるものです。例えば\x -> f xは,値xを取ってf xを評価した結果を返す無名関数(anonymous function,匿名関数)を作成しています。Lispをご存じの方なら,(lambda (x) (f x))だと思えばいいでしょう。ラムダ抽象は名前を付けるまでもないちょっとした関数を定義するのに便利です。今回の記事でもところどころで使用しています。
定義したrepeatedの型を確かめるには,:typeコマンドを使います。:tという省略形も使えます。
Prelude> :type repeated repeated :: (a -> a) -> Int -> a -> a Prelude>
::の右側に表示されているのが,repeatedの型です。この型は,(a -> a)という型を持つ関数と,Int型の値,aという型の値を取り,aという型の値を返すという関数の定義を表現しています。回数を表す変数nの型がIntになっているのは,nに適用される!!演算子の型が[a] -> Int -> aであるためです。このようにHaskellでは,型を明示的に書かなくても適切な型(できるだけ広く適用できる型)を自動的に付けてくれます。この機能を「型推論(type inference)」といいます。
でも一つ疑問が残りますね。整数を表すIntはともかく,aという小文字のアルファベットで表されている型は何でしょうか。これは実は「型変数(type variable)」と呼ばれるものです。任意の型に置き換えることができます。また,関数の定義に使う変数と同じく,同じ名前の型変数はすべて同じ型に置き換えられます。
型変数の働きについて見ていきましょう。repeatedの3番目の変数(仮引数がxの変数)に適当な数字である12を与えてみます。これにより,型aが数字の型に確定します。実行してみましょう。
Prelude> :t \f n -> repeated f n 12 \f n -> repeated f n 12 :: (Num a) => (a -> a) -> Int -> a Prelude>
どうやらaの型は一意に決定されてはいないようです。代わりに=>を使って「文脈(context)」が付いているのがわかります。これは「aが,型クラス(type class)であるNumのインスタンス(instance)でなければならない」という制約を示すものです。:infoコマンドを使って,Numクラスについて見てみましょう。
Prelude> :info Num class (Eq a, Show a) => Num a where (+) :: a -> a -> a (*) :: a -> a -> a (-) :: a -> a -> a negate :: a -> a abs :: a -> a signum :: a -> a fromInteger :: Integer -> a -- Imported from GHC.Num instance Num Float -- Imported from GHC.Float instance Num Double -- Imported from GHC.Float instance Num Integer -- Imported from GHC.Num instance Num Int -- Imported from GHC.Num Prelude>
型クラスNumのインスタンスとしてFloatやDouble,Integer,Intといった型が用意されているのがわかります。ほかに,NumクラスがEqクラスやShowクラスを継承(inheritance)したサブクラス(subclass)になっていることや,定義がGHC.Numモジュール(module)やGHC.Floatモジュールからインポート(import)されているといった情報も表示されています。ただ,今はまだ特に気にしなくてもかまいません。
さて,12にNumクラスのインスタンスであるDoubleという型を明示的に与えてみましょう。
Prelude> :t \f n -> repeated f n (12::Double) \f n -> repeated f n (12::Double) :: (Double -> Double) -> Int -> Double Prelude>
このように型変数aはDoubleに置き換わりました。Doubleは倍精度の不動小数点数を表す型なので,12の代わりに12.0や1.2e1を使ってもかまいません。ただし,浮動小数点数には,ほかにも単精度のFloat型があるため,型を明示的に与えない場合の結果はDoubleにはなりません。
Prelude> :t \f n -> repeated f n 12.0 \f n -> repeated f n 12.0 :: (Fractional a) => (a -> a) -> Int -> a Prelude>
次に,repeatedに関数を与えることで,(a->a)の型を与えてみましょう。ここでは,文字列の末尾に" ring-ring"を追加する関数にしてみました。関数が適用されるたびに,文字列の末尾に「ring-ring」という電話のベルが鳴る音が加わっていきます。
Prelude> :t repeated (++ " ring-ring") repeated (++ " ring-ring") :: Int -> [Char] -> [Char] Prelude>
(++ " ring-ring")というのは,++演算子を右の" ring-ring"に適用するという意味を持ったセクション(section)です。
さて,(++ " ring-ring")の型は何でしょうか?
Prelude> :t (++ " ring-ring") (++ " ring-ring") :: [Char] -> [Char] Prelude>
つまり,関数(a->a)の型変数aが[Char]に決定したことにより,外側にある他の型変数aも[Char]に決まっているのです。[Char]というのはChar型の要素を持つリストのことです。現在の標準Haskellには,実は文字列という型は存在しません。このため,文字列の代わりに文字のリストを使用します。文字列を表すString型を見てみましょう。
Prelude> :i String type String = [Char] -- Imported from GHC.Base Prelude>
[Char]の別名(型の同義名(type synonym,型シノニム))であることがわかります。また,++演算子は文字列だけを対象としたものではなく,同じ型の要素を持つリスト同士であれば何でも連結できる,汎用的なものとして定義されています。
Prelude> :t (++) (++) :: [a] -> [a] -> [a] Prelude> :i ++ ++ :: [a] -> [a] -> [a] -- Imported from GHC.Base infixr 5 ++ Prelude> [3,14] ++ [1592,65,3] [3,14,1592,65,3] Prelude>
:typeコマンドを使う場合には,++ではなく(++)というセクションの形にしなければならないことに注意してください。:typeコマンドの代わりに:infoコマンドで型を見ることもできます。:infoコマンドの場合は,セクションではなく演算子を指定します。infixrは関数の結合順序を示すものですが,今は説明を省略します。
ついでにリストの定義も見てみましょう。
Prelude> :i [] data [] a = [] | a : [a] -- <wired into compiler> instance Eq a => Eq [a] -- Imported from GHC.Base instance Monad [] -- Imported from GHC.Base instance Functor [] -- Imported from GHC.Base instance Ord a => Ord [a] -- Imported from GHC.Base instance Read a => Read [a] -- Imported from GHC.Read instance Show a => Show [a] -- Imported from GHC.Show Prelude>
リストという「代数的データ型(algebraic data type)」を作っている「型構成子(type constructor,型構築子)」(2行目左側の[])や「データ構成子(data constructor,データ構築子)」(2行目右側の[]や:)は,特別扱いになっているようです(関連リンク)。例えば,リストの表記として,[] aの省略記法として[a]を使ったり,:のような中置のデータ構成子を使うことができます。リスト型のデータを,[3,1,4]のようなリテラルを使わずにデータ構成子を使って直接書き下した例を以下に示します。
Prelude> :t 3:1:4:[] 3:1:4:[] :: (Num a) => [a] Prelude>
こうした特別扱いが許されていなければ,リストのデータ型の定義はdata [] a = [] | : a ([] a)であり,上のコードは: 3 (: 1 (: 4 (: [])))と書かなければなりません。このようなリストに対する特別扱いは,他の型と比べてみるとわかります。まずMaybeという型を見てみましょう。
Prelude> :i Maybe data Maybe a = Nothing | Just a -- Imported from Data.Maybe instance Eq a => Eq (Maybe a) -- Imported from Data.Maybe instance Monad Maybe -- Imported from Data.Maybe instance Functor Maybe -- Imported from Data.Maybe instance Ord a => Ord (Maybe a) -- Imported from Data.Maybe instance Read a => Read (Maybe a) -- Imported from GHC.Read instance Show a => Show (Maybe a) -- Imported from GHC.Show Prelude> :t Just 3 Just 3 :: (Num a) => Maybe a Prelude>
Maybeは,答があるかどうかわからないものを表現するのに便利な型です。例えば,Graphics.SOEのmaybeGetWindowEvent関数では,Window上でキーボードのKeyやマウスのButtonなどからイベントが発行されたかどうかを表現するためにMaybeを使っています。問い合わせに対し,何かイベントが発行されていればそのイベントを型構成子Justに包み込んで返し,イベントが何も発行されていなければNothingを返します。
次にTreeという型について見てみましょう。その前に,説明しておかなければならないことがあります。GHCiのプロンプトに表示されているPreludeは,標準ライブラリのうち使用頻度の比較的高い関数やデータ型,クラスを集めたPreludeモジュール(標準プレリュード(Standard Prelude))のことです(関連リンク)。Preludeという表示は,Preludeモジュールの関数などを使ってプロンプトへの入力を評価している,ということを示します。Hugsではプロンプトにデフォルトで別のモジュール名が表示されますが,Preludeを使っていると考えて差し支えありません(Hugsのプロンプトに表示されるモジュールの中身は,モジュールの宣言のみの空っぽのものです)。
もうおわかりだと思いますが,TreeはPreludeモジュールには含まれていません。Treeについて見るには,:moduleコマンドを使って,評価に使用するモジュールを,Treeを定義しているData.Treeに切り替える必要があります。
Prelude> :i Tree Top level: Not in scope: type constructor or class `Tree' Prelude> :module Data.Tree Prelude Data.Tree> :i Tree data Tree a = Node {rootLabel :: a, subForest :: (Forest a)} -- Imported from Data.Tree instance Eq a => Eq (Tree a) -- Imported from Data.Tree instance Functor Tree -- Imported from Data.Tree instance Read a => Read (Tree a) -- Imported from Data.Tree instance Show a => Show (Tree a) -- Imported from Data.Tree Prelude Data.Tree> :t Node 3 [Node 1 [Node 4 []], Node 1 [], Node 5 []] Node 3 [Node 1 [Node 4 []], Node 1 [], Node 5 []] :: (Num a) => Tree a Prelude Data.Tree>
なお,Hugsはインタプリタのみの処理系であるという制約上,:moduleコマンドでは評価に使用するモジュールをData.Treeに変えることはできません(Hugsの対話環境でモジュールを使う方法については,あとで説明します)。
ここまで紹介してきたような,型変数を持つ型を「多相型(polymorphic type,多様型)」といいます。多相型の概念を理解すれば,多相性にはいくつかの種類があることがわかります。まず,repeated関数や++演算子のように,関数の型情報の一部をパラメータ化した中立的な定義をすることで,関数そのものの振る舞いを変えることなく得られる多相性です。これをパラメータ多相(parametric polymorphism,パラメトリック多相)と呼びます。一方で,Numクラスで定義されている+演算子などのように,型によって変わる振る舞いを型クラスなどを通して関数に付け足すことで発生する多相性があります。これを「アドホック多相(ad-hoc polymorphism)」と呼びます。
文字列を効率的に扱える「Data.ByteString」
文字列の話題が出てきたので,文字列を効率的に扱える「Data.ByteString」というライブラリの話をしておきましょう。GHCやHugsでは,Cleanのように,リストではなく「非ボックス化配列(unboxed array)」を使って文字列を構成するData.PackedStringというライブラリを利用できます。しかし,このライブラリは,より効率的にバイト列を読み書きできるData.ByteStringに置き換えられることがすでに決定しています。GHC6.6からはData.PackedStringは非推奨になるので,特別な理由がない限り使わないようにしましょう。
それでは実際にData.ByteStringを使ってみましょう。最新版のHugsやGHC6.6(あるいは開発版のGHC6.5)にはData.ByteStringが入っていますが,それ以前の処理系にはData.ByteStringは入っていません。使いたければ自分でインストールする必要があります。Data.ByteStringのインストール方法については最後のコラムで説明します。
処理系にData.ByteStringが入っていれば,Hugsではファイルを読み込む:loadコマンド,GHCiでは前述の:moduleコマンドを使うことで,評価に使用するモジュールをData.ByteStringに切り替えられます。ただ,実際にはData.ByteStringではなくData.ByteString.Char8を使います。Data.ByteStringでは" ring-ring"のような"で囲んだ文字列リテラルを使用できず,文字列を直接与える場合には,明示的にバイト列に変換する必要があるからです。Data.ByteString.Char8のpack関数を使えば,制限された範囲ではあるものの,文字列をバイト列に変換できます。
まず,Hugsを使った例から見てみます。
$ hugs __ __ __ __ ____ ___ _________________________________________ || || || || || || ||__ Hugs 98: Based on the Haskell 98 standard ||___|| ||__|| ||__|| __|| Copyright (c) 1994-2005 ||---|| ___|| World Wide Web: http://haskell.org/hugs || || Report bugs to: hugs-bugs@haskell.org || || Version: 20051031 _________________________________________ Haskell 98 mode: Restart with command line option -98 to enable extensions Type :? for help Hugs> :load Data.ByteString.Char8 Data.ByteString.Char8> :m Data.List Data.List> :m Data.Tree ERROR - Cannot find module "Data.Tree" Data.List> :m Data.ByteString.Char8 Data.ByteString.Char8> :t (\f n x -> (iterate f x)!!n) (flip append (pack " ring-ring")) (\f n x -> iterate f x !! n) (flip append (pack " ring-ring")) :: Int -> ByteStr ing -> ByteString Data.ByteString.Char8>
このように,Hugsで:moduleコマンドによって使用できるのは,明示的に読み込んだファイル,あるいは読み込んだファイルが暗黙的に読み込んだファイルのモジュールのみです。また,Hugsでは対話環境内で関数を定義できないので,repeatedの定義を(\f n x -> (iterate f x)!!n)のように書き下して使っています。このようにHugsには少し使いづらい点があるので,説明では主にGHCiを使っています。
では,GHCiを使った説明に戻ります。
Prelude> :m Data.ByteString.Char8 Prelude Data.ByteString.Char8> :t repeated (flip append (pack " ring-ring")) repeated (flip append (pack " ring-ring")) :: Int -> ByteString -> ByteString Prelude Data.ByteString.Char8>
まず,pack関数で文字列をバイト列に変換しています。文字列の追加には,++演算子の代わりにappend関数を使っています。++演算子はリストに対してしか定義されておらず,また,Data.ByteStringではPreludeものと衝突する演算子を定義していないからです。ただし,append関数は中置演算子(infix operators)ではないためセクションにはならず,第1引数の(pack " ring-ring")に適用されることになります。このため,append関数をそのまま++演算子の代わりに使ってしまうと,異なる結果になってしまいます。これを防ぐには,上のように関数の第1引数と第2引数を入れ替える関数flipを使うか,`append`のように`で囲んで中置演算子にする必要があります。
ちょっと難しかったかもしれません。例を挙げて詳しく見ていきましょう。最初の"Ring:"という文字列に続けて," ring-ring"を5回繰り返した文字列を返すことにします。Stringと++を使った場合には,以下のように正しく表示されます。
Prelude Data.ByteString.Char8> repeated (++ " ring-ring") 5 "Ring:" "Ring: ring-ring ring-ring ring-ring ring-ring ring-ring" Prelude Data.ByteString.Char8>
一方,flipを使わずにappendを用いた場合には,以下のように表示されてしまいます。
Prelude Data.ByteString.Char8> repeated (append (pack " ring-ring")) 5 (pack "Ring:") " ring-ring ring-ring ring-ring ring-ring ring-ringRing:" Prelude Data.ByteString.Char8>
flip appendもしくは`append`を使った場合には,最初のStringと++を用いたのと同じ結果が表示されます。
Prelude Data.ByteString.Char8> repeated (flip append (pack " ring-ring")) 5 (pack "Ring:") "Ring: ring-ring ring-ring ring-ring ring-ring ring-ring" Prelude Data.ByteString.Char8> repeated (`append` (pack " ring-ring")) 5 (pack " Ring:") "Ring: ring-ring ring-ring ring-ring ring-ring ring-ring" Prelude Data.ByteString.Char8>
ついでにpackを適用し忘れたらどうなるか試してみましょう。
Prelude Data.ByteString.Char8> repeated (append (pack " ring-ring")) 5 "Ring:" <interactive>:1:40: Couldn't match `ByteString' against `[Char]' Expected type: ByteString Inferred type: [Char] In the third argument of `repeated', namely `"Ring:"' In the definition of `it': it = repeated (append (pack " ring-ring")) 5 "Rin g:" Prelude Data.ByteString.Char8> repeated (append " ring-ring") 5 "Ring:" <interactive>:1:17: Couldn't match `ByteString' against `[Char]' Expected type: ByteString Inferred type: [Char] In the first argument of `append', namely `" ring-ring"' In the first argument of `repeated', namely `(append " ring-ring")' Prelude Data.ByteString.Char8>
ByteStringと型があわないことを示すエラー・メッセージが表示されました。
このように,Data.ByteStringは速い反面,文字列リテラルがないことやリストとして扱えないことに起因する使いづらさがあります。このあたりはトレードオフです。また今のところ扱えるのは,バイト表現そのもの,または同じく1バイト=1文字と仮定するのに都合が良いように0から255までの範囲に制限したUnicode文字に限定されています。このままでは日本語の処理には使えません。この問題を解決すべく,Data.ByteStringにUnicode文字をすべて扱えるレイヤーを用意するというプロジェクトが今年のGoogleのSummer of Codeで進められていました。今後も開発が継続され,その成果が取り入れられる日が来ることに期待したいですね。
アクションを利用して表示を行う
引き続き,repeatedを使った例を見ていきます。
HaskellはIOを取り扱うために「モナド(monad)」と呼ばれる特別な仕組みを使用することで有名です。ただ,(IO a -> IO a)という型の関数さえあれば,モナドを明示的に使用しなくても「動作(action,アクション)」を作ることができます。まずはControl.Exceptionのfinallyを使って,5という数字を5回表示させてみます。
Prelude Data.ByteString.Char8> :m Control.Exception Prelude Control.Exception> :t finally finally :: IO a -> IO b -> IO a Prelude Control.Exception> (\x -> repeated (\y -> finally y x) 5 (return ())) (print 5) 5 5 5 5 5 Prelude Control.Exception>
Control.Exceptionというモジュール名とfinallyという関数名からある程度は見当が付くかもしれません。finallyは実行するアクションに後処理を加える関数です。上に示したような,単に5回表示するだけの定義であれば,finally x yのxとyの順番はどちらでもかまいません。return ()は,この場合はIO()を返す関数になります。printは,Showクラスのインスタンスになっている型をプロンプト上に表示する関数です。
Prelude Control.Exception> :i print print :: Show a => a -> IO () -- Imported from System.IO Prelude Control.Exception>
以下に示すように,多くの型はShowクラスのインスタンスになっているので,printデバッグに用いるのに便利です(ただし型からわかるように,IOモナドの内部で使う必要があります)。
Prelude Control.Exception> :i Show class Show a where showsPrec :: Int -> a -> ShowS show :: a -> String showList :: [a] -> ShowS -- Imported from GHC.Show instance Show Bool -- Imported from GHC.Show instance Show Char -- Imported from GHC.Show instance (Show a, Show b) => Show (Either a b) -- Imported from GHC.Show instance Show Int -- Imported from GHC.Show instance Show a => Show (Maybe a) -- Imported from GHC.Show instance Show Ordering -- Imported from GHC.Show instance Show () -- Imported from GHC.Show instance (Show a, Show b) => Show (a, b) -- Imported from GHC.Show instance (Show a, Show b, Show c) => Show (a, b, c) -- Imported from GHC.Show instance (Show a, Show b, Show c, Show d) => Show (a, b, c, d) -- Imported from GHC.Show instance (Show a, Show b, Show c, Show d, Show e) => Show (a, b, c, d, e) -- Imported from GHC.Show instance Show Integer -- Imported from GHC.Num instance Show Double -- Imported from GHC.Float instance Show Float -- Imported from GHC.Float instance Show ArithException -- Imported from GHC.IOBase instance Show ArrayException -- Imported from GHC.IOBase instance Show AsyncException -- Imported from GHC.IOBase instance Show Exception -- Imported from GHC.IOBase instance Show IOException -- Imported from GHC.IOBase instance Show a => Show [a] -- Imported from GHC.Show Prelude Control.Exception>
GHCiではプロンプトでの実行結果の表示にprintを使っているため,評価の結果,Showクラスのインスタンスがなければ以下のようなエラーが表示されます。
Prelude Control.Exception> (1 +) Top level: No instance for (Show (a -> a)) arising from use of `print' at Top level Probable fix: add an instance declaration for (Show (a -> a)) In a 'do' expression: print it Prelude Control.Exception>
次に「IO aを取って,aを表示し,aに+1した結果を返す」関数printThenAddを用意し,これを使って0から順に+1した数を表示させてみることにします。
Prelude Control.Exception> let printThenAdd v = do {x <- v; print x; return (x+1)} Prelude Control.Exception> :t printThenAdd printThenAdd :: (Num a) => IO a -> IO a Prelude Control.Exception> repeated printThenAdd 5 (return 0) 0 1 2 3 4 Prelude Control.Exception>
このように,外部で(IO a -> IO a)という関数を用意しておけば,repeatedではモナドのことを別に気にしなくても繰り返しのアクションを行わせることができます。ここでのreturn 0は,aをIO aに変えて,(IO a -> IOa)で扱うことのできる初期値にする役割を果たしています。
さて,repeated printThenAdd 5 (return 0)という一連の式を評価した,最後の結果はどこに行ったのでしょうか? GHCiではこの結果を,最後に評価した結果を返すit,またはprintThenAddの内部で使用している<-を使って取り出すことができます。
Prelude Control.Exception> it 5 Prelude Control.Exception> var <- repeated printThenAdd 5 (return 0) 0 1 2 3 4 Prelude Control.Exception> var 5 Prelude Control.Exception>
<-を使って明示的に「束縛した(bind)」場合には,itは結果に束縛されないので注意してください。これらの値もletを使って定義した関数同様,新しい定義に置き換えることができます。
GHCiのプロンプトで使える<-が,printThenAddの{}の中で使われているものと似ていることに気づいた方がいるかもしれませんね。その通りです。今回は詳細には深く立ち入りませんが,こうした機能はdo式(do expression,あるいは,do記法(do-notation))の内部であるかのようにGHCiのプロンプトを扱うことによって実現されています(関連リンク)。そして,プロンプトで式を評価する際にletや<-を使った変数の束縛がなければ,暗黙的にitをその対象とするのです(関連リンク)。
ここまでは(a->a)という型を持つ関数を使ってきました。さて,(a->b)という型を持つ関数を使うとどうなるでしょうか? 結果は以下のいずれかになります。
- 関数の型が(a->a)または(b->b)の範囲に制限され,repeatedからは(a->a)の関数として扱われる
- (a->b)なので型が合わないので,エラーになる
- データ構成子あるいはそれに準ずる役割を持つ(a -> cons a)という型の関数を与えた場合,iterate関数によって積み重なっていくデータ構成子が無限の型を生じさせてしまうためエラーになる
GHCiでは,それぞれ以下のように表示されます。
Prelude Control.Exception> :t repeated fromIntegral repeated fromIntegral :: (Integral b) => Int -> b -> b Prelude Control.Exception> :t repeated readFile <interactive>:1:9: Couldn't match `FilePath' against `IO String' Expected type: FilePath -> FilePath Inferred type: FilePath -> IO String In the first argument of `repeated', namely `readFile' Prelude Control.Exception> :t repeated Just <interactive>:1:9: Occurs check: cannot construct the infinite type: a = Maybe a Expected type: a -> a Inferred type: a -> Maybe a In the first argument of `repeated', namely `Just' Prelude Control.Exception>
ソースコードに関数を定義する
最後に,関数の定義をソースファイルにしましょう。Data.ByteString.Char8などのimport宣言やrepeatedやprintThenAddの型の宣言も行っておきます。ソースコードにすることで,Hugsでも関数を問題なく評価できるようになります。
import Data.ByteString.Char8 import Control.Exception repeated :: (a -> a) -> Int -> a -> a repeated f n = \x -> (iterate f x)!!n printThenAdd :: (Num a) => IO a -> IO a printThenAdd v = do {x <- v; print x; return (x+1)}
以上の定義を拡張子hsのファイルに書いて,:loadコマンドで読み込めばOKです。
Prelude Control.Exception> :l c:\home\Repeated Compiling Main ( c:\home\Repeated.hs, interpreted ) Ok, modules loaded: Main. *Main>
import宣言を追加したのは,:loadコマンドが,評価に使用するモジュールを読み込んだファイルに変えてしまうためです。この時ファイルにimport宣言があれば,そのモジュールで定義されている関数やデータ型,クラスを使うことができます。ちなみに,プロンプトからPreludeという文字が消えていますが,Preludeは暗黙的にインポートされるので問題ありません。
ファイルの定義を変更した場合には,:reloadコマンドを使って再読み込みできます。
*Main> :reload Compiling Main ( c:\home\repeated.hs, interpreted ) Ok, modules loaded: Main. *Main> :t repeated repeated :: (Integral a1) => (a -> a) -> a1 -> a -> a *Main>
上の例では,repeated関数の定義を変更しているのがわかりますね。
今回のまとめ
いかがでしたか? 多相性というものの持つ可能性が理解できたのではないでしょうか?
今回説明した内容だけにとどまらず,「多相性を使ってどんなことができるのか」と考えを広げてみると,より理解を深められると思います。例えば,++演算子や文字列リテラルが単にリストやStringに対して用意されているのではなく,Stringableのような型クラスに対して定義されていれば,アドホック多相とパラメータ多相の組み合わせによってより汎用的に使えるのではないか? repeatedはこんな処理に対しても使えるだろうか? あるいはこんな関数があったとして,repeatedと同じように使えるだろうか? そんなことを考えてみるとよいと思います。
ほかにも標準Haskellを超えた範囲には「部分型多相(subtype polymorphism)」や「ランクN多相性(rank-N polymorphism)」といったものがあります。余力があり,かつ興味のある方は,それらについて調べてみるとよいかもしれません。いずれこの連載でもそれらについて触れる日が来ればと思います。
Data.ByteStringがない場合には Data.ByteStringが入っていない処理系を使っている人向けに,Data.ByteStringのインストール方法を説明しておきます。まず,Data.ByteStringのサイトに行き,Data.ByteStringのソースを入手しましょう。fps(fpsはData.ByteStringの旧名の略称)と書かれたところからファイルをダウンロードしてください。Haskellではスタンダードなバージョン管理システムであるdarcsを利用することもできます。 インストールにはCabalというHaskell用のパッケージ・ツールを使います。ただ,実際にはCabelの動作を意識する必要はありません。GHCのrunhaskell(あるいはrunghc)というコマンドを使って以下のようにSetup.hsを実行します。これによりData.ByteStringをインストールできます。 $ runhaskell Setup.hs configure $ runhaskell Setup.hs build $ runhaskell Setup.hs install 一方,HugsでCabalを使用する場合には,いろいろと問題があります。基本的には,素直に最新版のHugsを試すことをお勧めします。 前回,紹介した「The Haskell School of Expression: Learning Functional Programming through Multimedia」(SOE)という本のサポート・サイトで提供されているバイナリのWindows版では,Cabalはうまく動きません。GHCのrunhaskell(あるいはrunghc)を使う必要があります。GHCが入っていれば,以下のコマンドでData.ByteStringをインストールできます。 $ runhaskell Setup.hs configure --hugs --libdir $prefix/Hugs98_SOE --libsubdir packages/$pkg (↑実際には1行) $ runhaskell Setup.hs build $ runhaskell Setup.hs install --hugsというオプションは,インストール先がHugsであることを示します。--libdirや--libsubdirはそれぞれインストール先のディレクトリを指定するオプションです。詳しくはCabalのマニュアルのオプションに関する説明を見てください。 |
著者紹介 shelarcy 2006年8月26日に開催されたLightweight Language Ring(通称: LL Ring)というイベントのデモセッション―「LL Gong」で,「HaskellでGUI」という発表をしました。当日の発表に使用したソースコードや,当日の発表とほぼ同じ内容のスライドが公開されています(発表資料)。 「IOモナドを使い,あたかも手続き型言語(命令型言語(imperative language))を使っているかのようにプログラミングする」。それこそがHaskellを現実の世界に持ってくる唯一の方法だと世間的に認識されつつあります。でも,それでは関数型の意味があまりありません。適切な仕組みを用意すれば,もっとHaskellらしい書き方で現実の物事を解決できることを示そうとしたのですが,いかがだったでしょうか? 詳細に関する部分をかなり削ったのですが,それでも5分で語ろうとするには少々重いテーマだったかもしれません。 repeatedを使って文字列を表示させるという今回の処理も,実はこうした例の一つです。このあたりのテーマは私が取り組んでいることの一つです。いずれ皆さんに十分な前提知識がついたあとに,改めてじっくりと説明したいと思っています。 |