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

それぞれ CharInt として最大の値が返ってきます。これを実現しているのが型クラスです。イメージとしてですが次のようなコードになっています。

class Bounded a where
    maxBound :: a
    ...

instance Bounded Char where
    maxBound = '\1114111'
    ...

instance Bounded Int where
    maxBound = 2147483647
    ...

ということは、printf 'I' "am" 23 の型 StringString -> String はなんらかの同じ型クラスのインスタンスであるということが分かります。同じように、printfprintf '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歩です。

型はできた、値を詰めよう。

Stringspr から考えてみましょう。["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 () も返しちゃう

実際の printfString だけじゃなく IO () も返すみたいです。俺々 printfIO () を返すようにしたい!
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 さんのは僕の知識ではなんのこっちゃという感じですが。