Hatena::Grouphaskell

[ pred x | x <- "Ibtlfmm!ojllj" ] RSSフィード

2008-03-13

[]Text.Printf 02:22

Haskellでは引数が可変長の関数を定義できない。できないのだが、Text.Printf.printfの動作を見ると可変長にしか見えない。

>:m Text.Printf
>printf "foo\n"
foo
>printf "foo %s %s %d\n" "bar" "baz" 42
foo bar baz 42

引数の数が合わなかったりすると実行時エラーが出る。

> printf "foo: %s %s %d\n" "bar" "baz"
foo: bar baz *** Exception: Printf.printf: argument list ended prematurely

printfの型はこうだ。

printf :: PrintfType r => String -> r
class PrintfType t

PrintfTypeの中身は公開されていない。インスタンスはこの三つ。

instance (PrintfArg a, PrintfType r) => PrintfType (a -> r)
instance PrintfType (IO a)
instance PrintfType String

ここまでで読み取れること。

  • printfはStringを受け取ってからPrintfTypeを返す
  • PrintfTypeというのは以下のどれか
    • IO a
    • String
    • PrintfArg -> PrintfTypeという型の関数

PrintfArgのインスタンスはこれら。

instance PrintfArg Char
instance PrintfArg Double
instance PrintfArg Float
instance PrintfArg Int
instance PrintfArg Integer
instance PrintfArg String

型から推測される内部の動作はこうなる。

  • printfが書式を受け取って、PrintfArgを待ち受ける関数を返す
  • IO aとして扱われたら、現状の引数と書式を合わせて出力するか例外を出す
  • Stringとして扱われたら、現状の引数と書式を合わせて文字列にするか例外を出す
  • 関数として扱われたら、引数を文字列化して埋め込む
>:m Text.Printf
>printf "foo\n" :: IO ()
foo
>printf "foo\n" :: String
"foo\n"
>printf "foo: %d\n" :: Int -> String

<interactive>:1:0:
    No instance for (Show (Int -> String))
    ...

引数をひとつひとつfoldlしているような感じだろうか。途中で中断される可能性があるが。

型チェックが弱くなるので、実際に使う時は書式だけ与えたものに型をつけておくほうが無難だと思う。

foo :: Int -> String
foo = printf "foo: %d\n"

ここまで読んでから内部のコードを見てみた。推測と全然違った。

class PrintfType t where
    spr :: String -> [UPrintf] -> t

instance PrintfType String where
    spr fmt args = uprintf fmt (reverse args)

instance PrintfType (IO a) where
    spr fmts args = do
	putStr (uprintf fmts (reverse args))
	return undefined

instance (PrintfArg a, PrintfType r) => PrintfType (a -> r) where
    spr fmts args = \ a -> spr fmts (toUPrintf a : args)

class PrintfArg a where
    toUPrintf :: a -> UPrintf

data UPrintf = UChar Char | UString String | UInteger Integer Integer | UFloat Float | UDouble Double
  • PrintfTypeというのは書式文字列とUPrintfから作れる値のこと
  • PrintArgというのはUPrintfに変換できる値のこと
  • 関数として扱われたら引数のUPrintfのリストに追加して再帰
  • 文字列として扱われたらuprintfに書式とUPrintfのリストを渡して文字列化
  • IO値として扱われたら同様に文字列にしてから出力

関数として扱われた時に再帰することで値を保存しているということのようだ。foldlには変わりないが、一度consして集めている。

トラックバック - http://haskell.g.hatena.ne.jp/illillli/20080313

2008-02-16

[]Data.Generics:タプルをmapする

Data.Genericsを使うとタプル*1の値を一度に変換できる。gmapTが基本で、everywhereは再帰的にmapしてくれる。

mkT(Transformの略)は、通常の関数の制約を外してジェネリックにしてくれるユーティリティのようだ。

Prelude Data.Generics> :t gmapT
gmapT :: (Data a) => (forall b. (Data b) => b -> b) -> a -> a
Prelude Data.Generics> :i GenericT
type GenericT = (Data a) => a -> a
        -- Defined in Data.Generics.Aliases
Prelude Data.Generics> :t mkT
mkT :: (Typeable b, Typeable a) => (b -> b) -> a -> a
Prelude Data.Generics> gmapT (+1) (1,2,3)

<interactive>:1:8:
    Could not deduce (Num b) from the context (Data b)
    ...
Prelude Data.Generics> gmapT (mkT (+1)) (1,2,3)
(2,3,4)
Prelude Data.Generics> gmapT (mkT (+1)) (1,(2,3),4)
(2,(2,3),5)
Prelude Data.Generics> everywhere (mkT (+1)) (1,2,3)
(2,3,4)
Prelude Data.Generics> everywhere (mkT (+1)) (1,(2,3),4)
(2,(3,4),5)

どちらも同じ型の範囲でしか変換できないのが問題*2適用できない場合はそのまま値を残すという動作らしい。

Prelude Data.Generics> everywhere (mkT show) (1,2,3)
(1,2,3)
Prelude Data.Generics> gmapT (mkT show) ("1","2","3")
("\"1\"","\"2\"","\"3\"")
Prelude Data.Generics> everywhere (mkT (fromIntegral :: Int -> Double)) (1,2,3)

<interactive>:1:17:
    Couldn't match expected type `Int' against inferred type `Double'
      Expected type: Int -> Int
      Inferred type: Int -> Double
    ...

代わりになるgmapQという関数で、mkQ(Queryの略)と組み合わせて使う。

Prelude Data.Generics> :t gmapQ
gmapQ :: (Data a) => (forall a1. (Data a1) => a1 -> u) -> a -> [u]
Prelude Data.Generics> :i GenericQ
type GenericQ r = (Data a) => a -> r
        -- Defined in Data.Generics.Aliases
Prelude Data.Generics> :t mkQ
mkQ :: (Typeable b, Typeable a) => r -> (b -> r) -> a -> r
Prelude Data.Generics> gmapQ (mkQ Nothing (\x -> Just . (/ 2) . fromIntegral)) (1,2,3)
[Just 0.5,Just 1.0,Just 1.5]
Prelude Data.Generics> gmapQ (mkQ Nothing (Just.(/2).fromIntegral)) (1,"hoge",3)
[Just 0.5,Nothing,Just 1.5]

mkQの引数の意味は下のようになっている。

  • 適用できない場合何を返すか
  • できる場合どう変換するか

サンプルコードを見ると、著者は中置記法で書くのが好みらしい。

main = print $ ( gmapQ   ([] `mkQ` leaf) term
               , gmapQ'  ([] `mkQ` leaf) term
               )

*1:タプルにかぎらず、deriving Data とすればユーザのデータ型もOK

*2:タプル以外の普通のデータ型に使うことを考えると、要素の型ごと置き換えるのは不可能

トラックバック - http://haskell.g.hatena.ne.jp/illillli/20080216