Haskell可倉数のパラメヌタヌを持぀関数を実装する1぀の方法に぀いお







-そしお、あなたはタヌトル「ラむク」を芋たしたか

「いいえ」ずアリスは蚀った。 「それが誰なのかさえ知りたせん。」

「たあ」ず女王は蚀った。 -これが「カメのスヌプのように」䜜られおいるものです。



 ルむス・キャロル 
                            「䞍思議の囜のアリス」 


「あなたのスピヌチから刀断するず、ファンゎヌンをよく知っおいたすか」 アラゎルンは返事を求めた。

-䜕がありたす -老人に答えた。 -これには100人の呜だけでは十分ではありたせん。 しかし、時々ここに行きたす。



 ゞョンR. R.トヌルキン、 
                           「ロヌドオブザリング」-Haskellに぀いおの私の知識に぀いお 




ホミネスダムドセント、ディスント。 他の人に説明-あなたは理解したす。

 ラテン語のこずわざ 




Haskellの関数は本質的に1぀のパラメヌタヌの関数であるこずは誰もが知っおいたす。 いく぀かのパラメヌタの「そのたたの」関数は、単に最初の匕数を受け入れ、元の関数の2番目の匕数を取り、関数を返すなどの別の関数を返したす。 非関数型の倀を既に返しおいる最終関数 currying ぞ。



この状況で議論できるパラメヌタヌの可倉数はどのように思えたすか ただし、 printfの゜ヌスを確認したり、wiki.haskellを読んだりするず、 FPがこの問題に察するやや「難解な」解決策ではなく、かなり矎しい解決策ぞの鍵を提䟛しおいるこずが明らかになりたす。



この出版物では、簡単な䟋を䜿甚しおこのようなメカニズムを実装する方法の1぀を怜蚎し、 テンプレヌトHaskellに基づく䞀般化された゜リュヌションを提案しお、リスト型の最埌のパラメヌタヌを持぀通垞の関数のファミリヌを「あたかも倉数番号パラメヌタヌを持぀」関数に倉換したす以䞋、テキストは単に「可倉数パラメヌタヌ付き」です。



非垞に簡単な䟋から始めお、゜リュヌションの本質を簡単に説明したす。



{-# LANGUAGE FlexibleInstances, RankNTypes #-} -- (0) class VarArgs a where prc :: String -> a -- (1) instance VarArgs String where prc = id -- (2) instance (Show a, VarArgs r) => VarArgs (a -> r) where -- (3) prc acc = \x -> prc $ acc ++ " " ++ show x magic = prc [] useMagic :: (forall a. VarArgs a => a) -> IO () -- (4) useMagic f = do putStrLn $ f 1 putStrLn $ f 1 "qwe" putStrLn $ f 1 "qwe" [1, 2, 3] main :: IO () main = do putStrLn $ magic 1 2 "qwe" [1,2,3] 123.456 useMagic magic -- (5)
      
      





ここで䜕が起こっおいたすかたた、任意の数のパラメヌタヌだけでなく、異なるタむプのパラメヌタヌもマゞック関数に枡すこずができたすか



したがっお、1では、文字列から特定の型の倀を䜜成する方法を単に知っおいる単䞀のprcメ゜ッドでVarArgsクラスを宣蚀したす。 さらに、2では、String型[Char]ずも呌ばれるのこのクラスのむンスタンスを実装したす。 FlexibleInstances0拡匵を䜿甚する必芁があるこずに泚意しおください。そうしないず、そのようなむンスタンスは「違法」になりたす。



代替゜リュヌションがありたすが、TypeFamilies拡匵機胜たたはGADTも䜿甚したす。
 {-# LANGUAGE TypeFamilies #-} instance a ~ Char => VarArgs [a] where prc = id
      
      







VarArgs Stringむンスタンスは、実際には可倉数のパラメヌタヌを持぀関数の「本䜓」です。 この䟋では、単に蓄積されたパラメヌタヌを返したす。 ここで最も興味深い郚分に移りたしょう。3では、関数型 a- > rのVarArgsのむンスタンスを宣蚀したすが、匕数型aが文字列にマップでき 、結果型rがVarArgsクラスに再び属するこずを芁求したす。



これは、「犬が駆け抜ける」堎所です。特に関数も蚱可する戻り型を持぀関数型でクラスをむンスタンス化するこずにより、呌び出しのコンテキストに応じお、コンテキストが文字列を必芁ずする堎合、 文字列型の最終倀ず関数の䞡方を返すprcメ゜ッドを蚱可したすprc呌び出しの結果の型は、機胜的ずしおコンテキストから掚枬されたす。



次に、 VarArgsa-> rのむンスタンスのprc定矩を怜蚎したす。 型prcを掚定するず、次のようになりたす



 prc :: (Show a, VarArgs r) => String -> (a -> r)
      
      





぀たり 文字列ずしお衚される倀で䜕かをする関数を返さなければなりたせん。 acc匕数は、パラメヌタヌの順次凊理の結果の「ドラむブ」の本質です。 この堎合、パラメヌタの文字列衚珟をスペヌスで远加したす。



重芁な点は、増分された「アキュムレヌタ」を返すだけでなく、結果の関数の「本䜓」で prcを再垰的に呌び出しお、目的の結果型rを取埗するこずです。 どのprc実装が呌び出されるか぀たり、どのタむプが掚論されるかはコンテキストに䟝存したすHaskell関数は方皋匏であり、蚈算プロセスはパラメヌタヌを曎新する匏の順次眮換であるこずを忘れないでください。



最も興味深いのは、「準合法」ステヌタスにもかかわらず、別の関数の匕数ずしお可倉数のパラメヌタヌを持぀関数を枡す4ず䜿甚する5こずです。 確かに、このためには、呌び出し関数の定矩4で別の拡匵機胜RankNTypes 0ずforall量指定子を䜿甚する必芁がありたした。



少しわかりにくいので、4の$の右偎の匏がどのように蚈算されるかを芋おみたしょう。



  1. magicaka prc [] はパラメヌタヌ1で呌び出されたす。 機胜的なコンテキストで䜿甚されるため、むンスタンスは機胜したす

    VarArgsa-> r 、最終的に返される...
  2. ...再び機胜する ここでも匕数2がありたす。 機胜コンテキスト
  3. 「qwe」ず[1,2,3]は同様に凊理されたす
  4. 最埌に、以前のパラメヌタヌず珟圚のパラメヌタヌ123.456の蓄積された文字列衚珟を持぀prc関数の最埌の呌び出しの結果は、 putStrLn関数のパラメヌタヌのような文字列コンテキストを既に必芁ずしたす-prcはVarArgs Stringむンスタンスから起動されたす


次に、もう少しやや耇雑な䟋を考えおみたしょう。 逆ポヌランド蚘法の匏蚈算機です。 次のようなもの



 > calcRPN 5 8 (*) 2 (+) -- 5*8 + 2 42
      
      





最も原始的な実装は次のようになりたす。



 {-# LANGUAGE ExtendedDefaultRules, FlexibleInstances, GADTs #-} data Expr = Num Double | Op (Double -> Double -> Double) calcRPN' :: [Expr] -> Double calcRPN' = head . foldr rpnStep [] . reverse rpnStep :: Expr -> [Double] -> [Double] rpnStep (Num n) stack = n : stack rpnStep (Op f) (x:y:stack) = (fxy) : stack class ArgPrc a where prc :: [Expr] -> a class ArgSrc a where toArg :: a -> Expr instance ArgPrc Double where prc = calcRPN' . reverse instance (ArgSrc a, ArgPrc r) => ArgPrc (a -> r) where prc acc = prc . (: acc) . toArg -- (2) instance ArgSrc Expr where toArg = id instance a ~ Double => ArgSrc (a -> a -> a) where toArg = Op instance ArgSrc Integer where toArg = Num . fromIntegral instance ArgSrc String where toArg = Num . fst . head . (reads :: ReadS Double) instance ArgSrc [Double] where toArg = Num . sum calcRPN :: ArgPrc a => a calcRPN = prc [] main = do print $ calcRPN' [Num 5, Num 5, Op (*)] print $ calcRPN [1::Double,2,3] "5" (*) 7 (*)
      
      





可倉数のパラメヌタヌを実装するためのスキヌムは前の䟋ず同じですが、ここでのみ





最埌に、printf関数の回路図実装を芋おみたしょう。



 {-# LANGUAGE GADTs, FlexibleInstances, ExtendedDefaultRules #-} type FmtRes = (String, String) class PfVal a where doFmt :: (String, String) -> a -> FmtRes instance PfVal Integer where doFmt (fmt, res) x = let (b, s) = span (/= '%') fmt in (res ++ (tail . tail $ s), b ++ show x) instance PfVal String where doFmt (fmt, res) x = let (b, s) = span (/= '%') fmt in (res ++ (tail . tail $ s), b ++ x) class ArgProc a where prc :: FmtRes -> a instance ArgProc String where prc = uncurry (++) instance ArgProc (IO ()) where prc = putStrLn . uncurry (++) instance (PfVal a, ArgProc r) => ArgProc (a -> r) where prc st = prc . doFmt st printf fmt = prc (fmt, "") main :: IO() main = do putStrLn $ printf "%d %s" 1 "qwe" printf "%s %d" "This is" 123
      
      





コヌドに特別なコメントは必芁ないず思いたす。パラメヌタヌを蓄積する代わりに、結果を「オンザフラむ」で再び生成し、 ArgProcクラスの2぀のタヌミナルむンスタンスを実装するこずに泚意しおください  String型ずIO型 。



図のスキヌムを䞀般化するず、次の芁玠を区別できたす。



  1. いく぀かのタむプはバッテリヌです Aを呌び出したしょう。タむプaのパラメヌタヌに基づいた予備蚈算結果です。 「予備」の皋床は、リストタむプのコンテナ内のパラメヌタの単玔な环積 逆ポヌランド衚蚘の䟋のようにから、珟圚のパラメヌタセットのほが準備完了の結果 printfの䟋のようにたで倉化したす。 このタむプから必芁なのは、タむプのオペレヌションの存圚だけです



      A -> a -> A
          
          





  2. メむンクラス ArgProcを呌び出したしょう。むンスタンスを通じお、可倉数のパラメヌタヌのすべおの「メカニクス」が実装されたす。 このクラスには、バッテリヌAで䜕かを行う1぀のメ゜ッド prcを呌び出したしょうがありたす。



     class ArgProc a where prc :: A -> a
          
          





  3. 倀をパラメヌタヌ型に倉換する機胜をサポヌトするパラメヌタヌずしお機胜できるタむプのクラス ArgSrcを呌び出したしょう䞀郚のタむプaは操䜜:: A-> a- > Aを蚱可したす



  4. パラメヌタヌの凊理ず予備結果の蓄積を行うメむンクラスのむンスタンス



     instance (ArgSrc a, ArgProc r) ArgProc (a -> r) where prc :: A -> (a -> r)
          
          





    printfを䜿甚した䟋では、結果はすぐに环積されペアの2番目の芁玠、状態は途䞭で凊理されたすフォヌマット文字列。 逆ポヌランド蚘法を䜿甚した䟋では、パラメヌタはリストに远加され、さらに凊理されたす。



  5. 予備結果の最終凊理を担圓するメむンクラスのタヌミナルむンスタンス



     instance ArgProc R1 where prc :: A -> R1 instance ArgProc R2 where prc :: A -> R2 ...
          
          





    逆ポヌランド衚蚘法の䟋では、結果のDouble型に察しおそのようなむンスタンスが1぀だけありたす。以前に蓄積されたパラメヌタヌのリストの蚈算を開始するだけです。 printfの䟋では、Stringのむンスタンスは、フォヌマットされた文字列ず残りのフォヌマットを単玔に連結したすそこにはフラットテキストが残っおいるず想定されたす。 IOのむンスタンスはさらに結果を衚瀺したす。



  6. 予備蚈算結果Aの初期状態の初期化子は、䞀般的な堎合、固定パラメヌタヌのセットの関数であり、䟋では、定数倀[]および関数です。



      \x -> (x, "") :: String -> (String -> String)
          
          





このようなスキヌムは、「ブラックマゞック」 テンプレヌトHaskellの手段を䜿甚しお実装できるこずがわかりたす。 これは、玠材を統合するのに適した挔習であり、 テンプレヌトHaskellを詊しおタンバリンで螊るのに適したプラットフォヌムです。



珟圚の実装では、私は自分自身を䞀般的なスキヌムのサブセットに限定したした。ドラむブはあるタむプの倀のリストに過ぎず、初期化子はそれに応じお単玔です[] 。 このような制限には確かに䞍利な点がありたすが、アむデアは、同じタむプのパラメヌタヌを持぀最埌のリストずさたざたな戻り倀タむプを持぀通垞の関数のファミリヌを、可倉数のパラメヌタヌリストに移動する固定パラメヌタヌに加えおを取る関数に倉換するこずです。



途䞭で、指定された型からパラメヌタヌリストの芁玠の型ぞの「暗黙的なキャスト」他のPLの甚語ではのプロセスを自動化したす。 もう1぀の制限は、「ドナヌ」関数には「単玔な」型量指定子ず制限のない非倚型型が必芁であるこずです。



繰り返しになりたすが、すぐに䜿甚䟋から始めたすので、アむデアは明確になりたす。それから初めお実装を簡単に説明したす。 それでは、簡単なものから始めたしょう。



 {-# LANGUAGE TemplateHaskell, FlexibleInstances, ExtendedDefaultRules #-} import Data.Function.Vargs -- (1) -- (2) tester' q (x : y : z) = putStrLn $ "Fixed parameter " ++ q ++ ", next x = " ++ x ++ " and y = " ++ y ++ " and rest = " ++ show z $( return [] ) -- (3) -- (4) defVargsFun "tester" ['tester'] (''Integer, [| ("Int " ++) . show |]) -- (5) (''(), [| const "NULL" |]) -- (6) ([t| [Int] |], [| ("[Int] " ++) . show |]) -- (7) ([t| [Double] |], [| ("[Double] " ++) . show |]) -- (8) main :: IO () main = do tester "<const>" "qwe" 100500 () [1::Int,2,3] [4::Double,5,6] -- (9)
      
      





この䟋では、次の型のテスタヌ関数のテスタヌラッパヌ関数を䜜成したす。



 tester' :: String -> [String] -> IO ()
      
      





テキストを芋おいきたしょう。





さらに進むか、逆ポヌランド蚘法を䜿甚しお䟋に戻りたす。



 {-# LANGUAGE TemplateHaskell, FlexibleInstances, ExtendedDefaultRules, GADTs #-} -- import Data.Function.Vargs data Expr = Num Double | Op (Double -> Double -> Double) calcRPN' :: [Expr] -> Double calcRPN' = head . foldr rpnStep [] . reverse rpnStep :: Expr -> [Double] -> [Double] rpnStep (Num n) stack = n : stack rpnStep (Op f) (x:y:stack) = (fxy) : stack $( return [] ) defVargsFun "calcRPN" ['calcRPN'] (''Integer, [| Num . fromIntegral |]) (''String, [| Num . fst . head . (reads :: ReadS Double) |]) (Genz [t| Double -> Double -> Double |], [| Op |]) -- (1) ([t| [Double] |], [| Num . sum |]) main = do print $ calcRPN' [Num 5, Num 5, Op (*)] print $ calcRPN [1::Double,2,3] "5" (+) 7 (*)
      
      





この䟋では、1぀興味深い点1を陀いお、すべおが前の䟋ず䌌おいたす。ここでは、TypeQだけでなく、GenQラッパヌを䜿甚しおいたす。 このラッパヌは、ゞェネレヌタヌに次の圢匏のむンスタンスを䜜成させたす。



 instance a ~ Double => ArgSrc (a -> a -> a) where
      
      





暙準の代わりに



 instance ArgSrc (Double -> Double -> Double) where
      
      





この堎合、型チェックに合栌したせん。



さお、最埌の䟋は、同じprintf関数、たたはそれず類䌌した回路図です



 {-# LANGUAGE TemplateHaskell, FlexibleInstances, ExtendedDefaultRules, ExistentialQuantification #-} import Data.Function.Vargs type FmtRes = (String, String) class PfVal a where doFmt :: FmtRes -> a -> FmtRes instance PfVal Integer where doFmt (fmt, res) x = let (b, s) = span (/= '%') fmt in (res ++ (tail . tail $ s), b ++ show x) instance PfVal Double where doFmt (fmt, res) x = let (b, s) = span (/= '%') fmt in (res ++ (tail . tail $ s), b ++ show x) instance PfVal String where doFmt (fmt, res) x = let (b, s) = span (/= '%') fmt in (res ++ (tail . tail $ s), b ++ x) data PfValWrap = forall a. PfVal a => Val a -- (1) printf_String :: String -> [PfValWrap] -> String -- (2) printf_String fmt vs = uncurry (flip (++)) $ foldl step (fmt, "") vs where step fmt (Val f) = doFmt fmt f printf_IO :: String -> [PfValWrap] -> IO () -- (3) printf_IO fmt = putStrLn . printf_String fmt $( return [] ) defVargsFun "printf" ['printf_String, 'printf_IO] -- (4) [''Integer, ''Double, ''String] main :: IO () main = do let fmt = "Number one is %d, number two is %f and string is \"%s\"" printf_IO fmt [Val 100, Val 123.456, Val "ok"] putStrLn $ printf fmt 100 123.456 "ok" printf fmt 100 123.456 "ok"
      
      





泚目に倀する3぀の新しいポむントがありたす。





ここで、それがすべおどのように機胜するかに぀いおのいく぀かの蚀葉 実際、 defVargsFunが行うこずは、 reifyから受け取った情報ず、可倉数のパラメヌタヌを持぀関数自䜓の宣蚀ず定矩に基づいお、いく぀かのクラスずむンスタンスを䜜成するこずだけです。 この「キッチン」はすべお、以前に䟋で怜蚎された䞀般的なスキヌムに察応しおいたす。 繰り返したすが、正確に生成されたものを䟋で瀺すこずは、より明確で簡単です。 呌び出しによっお生成されたコヌドを怜蚎しおください。



 defVargsFun "printf" ['printf_String, 'printf_IO] [''Integer, ''Double, ''String]
      
      





-ddump-splicesスむッチを䜿甚しおghcを実行するず、このコヌドが衚瀺されたす。 明確にするために、フォヌマットを修正し、䜙分な括匧を削陀したした。



 class ArgPrc_printf_aa3M a where -- (1) prc_printf_aa3O :: String -> [PfValWrap] -> a -- (2) class ArgSrc_printf_aa3N a where -- (3) toArg_printf_aa3Q :: a -> PfValWrap -- (4) instance ArgPrc_printf_aa3M String where -- (5) prc_printf_aa3O a1 = printf_String a1 . reverse instance ArgPrc_printf_aa3M (IO ()) where -- (6) prc_printf_aa3O a1 = printf_IO a1 . reverse -- (7) instance (ArgSrc_printf_aa3N a, ArgPrc_printf_aa3M r) => ArgPrc_printf_aa3M (a -> r) where prc_printf_aa3O a1 acc_printf_aa3P = prc_printf_aa3O a1 . (: acc_printf_aa3P) . toArg_printf_aa3Q -- (8) instance ArgSrc_printf_aa3N PfValWrap where toArg_printf_aa3Q = id instance ArgSrc_printf_aa3N Integer where toArg_printf_aa3Q = Val instance ArgSrc_printf_aa3N Double where toArg_printf_aa3Q = Val instance ArgSrc_printf_aa3N String where toArg_printf_aa3Q = Val -- (9) printf :: forall a. ArgPrc_printf_aa3M a => String -> a printf a1 = prc_printf_aa3O a1 []
      
      





Monad Qは、ナニヌクな名前の生成を提䟛したす-そのため、名前の「キャッチヌな」゚ンディングです。 テキストを芋おいきたしょう。





コメント付きのData.Function.Vargsモゞュヌルの゜ヌスコヌドずその䜿甚䟋はここにありたす; haddoc圢匏のドキュメントはここずパッケヌゞの䞀郚ずしお入手できたす。 珟時点では、パッケヌゞは「完党」ずいう蚀葉から実隓段階にありたす;



おそらく、時間の経過ずずもにそれを思い起こさせたす-少なくずも、゚ラヌ状態無効たたは互換性のないタむプを最倧で分析および凊理する必芁がありたす。



それから、 ハッカヌに投皿するのは恥ずかしいこずではないず思いたす 同様のトピックに関するHListのような適切なパッケヌゞは既にありたすが。



トピックに関する圹立぀リンク



  1. 可倉匕数
  2. 倚倉量関数
  3. テンプレヌトhakell
  4. 存圚タむプ
  5. Hlist



All Articles