Haskell での可変長引数
『簡約! λカ娘(4)』読んだ!おもしろかった!で、その中に「Haskell でも printf じゃないか!?」っていう @nushio さんの記事で、可変長引数関数(可変個引数関数)の話があって自分なりに悩んで納得したので忘れないようにメモしておこうと思う。
作りたいもの
作りたい関数は次のようなものとします。型はイメージ。
printf :: (Show arg1, Show arg2, Show arg3, ...) => arg1 -> arg2 ->arg 3 -> ... -> String printf "Hello, world!" :: String -- => "\"Hello world\"" printf 'I' "am" 23 :: String -- => "\'I\' \"am\" 23"
引数の個数?
正確を期すならそもそも Haskell で可変長引数なんておかしいですよね。だって全ての関数は引数を1つしか取らないのだもん。ということは、これから作る printf
関数はこんな感じの型になっているはずです。
printf :: (Show arg1, Show arg2, Show arg3, ...) => arg1 -> (arg2 -> (arg3 -> (... -> String))...)
型クラス
printf 'I' "am" 23
を例にもう少し詳しく見てみましょう。
printf :: Char -> (String -> (Int -> String)) printf 'I' :: String -> (Int -> String) printf 'I' "am" :: Int -> String printf 'I' "am" 23 :: String
この4つの式の内、上3つは1引数関数、最後の1つは文字列となりますが、最後の式だって後ろに引数を加えられると、1引数関数として振る舞うはずです。つまり、次のようにもなるということです。
printf 'I' "am" 23 ":p" :: String printf 'I' "am" 23 :: String -> String
1つの式がいくつもの型になる?どういうことでしょう?しかし、maxBound
という関数を知っている人は多いんじゃないですか?
ghci> maxBound :: Char '\1114111' ghci> maxBound :: Int 2147483647
それぞれ Char
、Int
として最大の値が返ってきます。これを実現しているのが型クラスです。イメージとしてですが次のようなコードになっています。
class Bounded a where maxBound :: a ... instance Bounded Char where maxBound = '\1114111' ... instance Bounded Int where maxBound = 2147483647 ...
ということは、printf 'I' "am" 23
の型 String
と String -> String
はなんらかの同じ型クラスのインスタンスであるということが分かります。同じように、printf
、printf 'I'
、printf 'I' "am"
もいくつかの型を持ちそれらの型はさきほど述べた型クラスのインスタンスです。その型クラスの名前を PType
としましょう。(PType
はλカ娘4からの引用で何の略称かは分からない。(追記) PrintfType
っぽい。)
次にどういった型をインスタンスにするか考えましょう。まず、String
がインスタンスになることは明らかです。String
以外にインスタンスにすべき型はいっぱいありそうです。しかし何か共通的なものがないですか?
-- printf 'I' "am" 23 :: String に、おいて printf :: Char -> (String -> (Int -> String)) printf 'I' :: String -> (Int -> String) printf 'I' "am" :: Int -> String -- printf 'I' "am" 23 ":p" :: String に、おいて printf 'I' "am" 23 :: String -> String
関数だ、ってことですね。(Show b) => b -> r
とかどうでしょう。うーん。b
は引数として与えられた値の型として r
の型って何でしょう。上のコードを見てみると、上2つの式は関数、下2つは String
です。あれ?どこかで聞いたような。今考えていた PType
型クラスのインスタンスそのものじゃないですか!ということは、関数の場合は再帰的な型になっていたんですね。ここまでをまとめると次のようになります。
class PType a where ... instance PType String where ... instance (Show b, PType r) => PType (b -> r) where ...
ふむ。
方法?メソッド!
型クラスはまだできていません。... の部分を記述しなくてはなりません。さて、どういうメソッドを作りましょう。とりあえず printf をメソッドにしてみましょう。
class PType a where printf :: a instance PType String where -- printf :: String ... instance (Show b, PType r) => PType (b -> r) where -- printf :: (Show b, PType r) => b -> r ...
型はこんな感じになりますね。問題はこれでいいのかどうかです。String
に関する方を見てみると、ううん?printf :: String
だって?これじゃ定数しか返せないじゃないですか。とうわけでボツです。
じゃあ、どうするのか。ちょっと printf 'I' "am" 23 :: String
の例に立ち返ってみましょう。左の引数 'I'
から順に受け取って、最終的にはそれら引数を用いて作った String
を返します。ということはどこかに引数を溜め込んでおく必要があります。もしくは返す String
を順々に作っていくか。引数を String
に変換してから溜め込む方向で考えていくことにします。
単純にリストを使って引数を溜め込むようにしてみましょう。溜め込み場所であるリストに次のように溜め込まれていきます。お尻に付け加えていくのはコストが高いですから。
[] → ["\'I\'"] → ["\"am\"", "\'I\'"] → ["23", "\"am\"", "\'I\'"]
イメージとしては畳み込みでのアキュムレーターと同じですね。畳み込み関数と同じように、第1引数にこのリストを渡すようにしましょう。
class PType a where spr :: [String] -> a instance PType String where -- spr :: [String] -> String ... instance (Show b, PType r) => PType (b -> r) where -- spr :: (Show b, PType r) => [String] -> b -> r ... printf :: (PType a) => a printf = spr []
printf
メソッドを PType
型クラスに持たせる代わりに spr
メソッドにしました。(spr
も何の略称なのか。)printf
関数は (PType a) => a
型なので spr
に溜め込み用リストを渡せばちょうど型が合って得られますね。ここで spr
に渡すリストは初期値ですので []
です。
ここまでくれば後1歩です。
型はできた、値を詰めよう。
String
の spr
から考えてみましょう。["23", "\"am\"", "\'I\'"]
が与えられたとすると "\'I\' \"am\" 23"
を返すようにすればいいんでしたね。
instance PType String where -- spr :: [String] -> String spr acc = unwords $ reverse acc
ポイントフリー化すると次のように。まぁ、お好きな方で。
instance PType String where -- spr :: [String] -> String spr = unwords . reverse
次は関数の方の定義です。関数を返すわけですから次のような感じになるはずですよね。
instance (Show b, PType r) => PType (b -> r) where -- spr :: (Show b, PType r) => [String] -> b -> r spr acc = (\b -> ...)
... の部分を考えます。毎度の printf 'I' "am" 23 :: String
を例に考えていきましょう。こうなってほしいっていう簡約過程を次に示します。
printf 'I' "am" 23 :: String -- (1) = spr [] 'I' "am" 23 :: String -- (2) = spr ["\'I\'"] "am" 23 :: String -- (3) = spr ["\"am\"", "\'I\'"] 23 :: String -- (4) = spr ["23", "\"am\"", "\'I\'"] :: String -- (5) = "\'I\' \"am\" 23" -- (6)
式(1)から式(2)は定義のまんまですよね。式(2)から式(3)を考えます。spr []
は、関数を返しているはずですよね。どういう関数かというと、'I'
を引数に取って spr [\'I\'"]
という関数を返す関数ですね。この情報から定義が書けそうです。「引数を String
化して溜め込み用のリストの先頭に追加して、そしてそれに spr
を適用する」という関数を返せばいいわけです。
instance (Show b, PType r) => PType (b -> r) where -- spr :: (Show b, PType r) => [String] -> b -> r spr acc = (\b -> spr ((show b):acc))
おお。いいんじゃないでしょうか。ラムダ式を使わずに次のように書いてもいいです。お好みで。
instance (Show b, PType r) => PType (b -> r) where -- spr :: (Show b, PType r) => [String] -> b -> r spr acc b = spr ((show b):acc))
式(3)から式(5)まではこの定義でいけます。式(5)から式(6)は先に定義した String
の方の関数ですよね。
これで動くところまでいけたんじゃないでしょうか!
-- Main.hs class PType a where spr :: [String] -> a instance PType String where spr acc = unwords $ reverse acc instance (Show b, PType r) => PType (b -> r) where spr acc = (\b -> spr ((show b):acc)) printf :: (PType a) => a printf = spr [] main :: IO () main = do putStrLn $ printf "Hello, world!" putStrLn $ printf 'I' "am" 23
よし。カタカタ タッーンッ!
$ runghc Main.hs src/Main.hs:6:10: Illegal instance declaration for `PType String' (All instance types must be of the form (T t1 ... tn) where T is not a synonym. Use -XTypeSynonymInstances if you want to disable this.) In the instance declaration for `PType String'
ぬ。このエラー、『Real World Haskell』 で解説されてた気がする。とりあえず次の1行をコードの前に書き加えておいてください。記憶曖昧だけど、抽象型に型引数を与えて得た具体型をインスタンスにすることはできないとかなんとか。で、それをなだめるのが次のコンパイラーオプション。
{-# LANGUAGE FlexibleInstances #-}
気をとりなおして、カタカタ タッーンッ!
$ runghc Main.hs "Hello, world!" 'I' "am" 23
うおー!
【番外編】IO ()
も返しちゃう
実際の printf
は String
だけじゃなく IO ()
も返すみたいです。俺々 printf
も IO ()
を返すようにしたい!
IO ()
を PType
のインスタンスにしてみるとどうすべきか見えてくるんじゃないでしょうか。
-- Main.hs ... instance PType (IO ()) where -- spr :: [String] -> IO () spr acc = putStrLn $ spr acc ... main :: IO () main = do printf "pi is" pi
カタカタ タッーンッ!来い!
$ runghc Main.hs "pi is" 3.141592653589793
わーい。
λカ娘
サークル「参照透明が海を守る会」の「λカ娘」シリーズ(1〜4巻)おもしろいのでぜひ!
(追記)もうちょっと詳しそうなお話
shelrcy さんのは僕の知識ではなんのこっちゃという感じですが。
- Haskellでprintf書くのってどうやるの? - Yet Another Ranha
- http://page.freett.com/shelarcy/log/2008/diary_04.html#continuation_fest_2008
引数の数や書式が実際に渡された値と一致しない場合、実行時エラーとなるのが問題だよね、といった話。1つめのページのリンクで知りました。