-- * Haskell98! -- * The final view to the typed sprintf AND sscanf module PrintScanF where import Prelude hiding ((^)) -- This code defines a simple domain-specific language of string -- patterns and demonstrates two interpreters of the language: -- for building strings (sprintf) and parsing strings (sscanf). -- This code thus solves the problem of typed printf/scanf sharing the -- same format string, posed by Chung-chieh Shan. -- This code presents scanf/printf interpreters in the final style; -- it is thus the dual of the code in PrintScan.hs -- September 2008. -- See http://okmij.org/ftp/typed-formatting/ -- The typed sprintf problem has been investigated extensively: the first -- solution was shown by Danvy; more solutions were proposed by Hinze and -- Asai. The typed scanf problem received significantly less attention, -- if any. It seems that the implementation of the typed sprintf and -- sscanf sharing the same formatting specification has not been known. -- We demonstrate that lambda-abstractions at the type level are -- expressible already in the Hindley-Milner type system. -- * Specification: -- * sprintf spec arg1 ... argn ==> String -- * sscanf input spec (\arg1 ... argn -> res) ==> Maybe res -- The typed sprintf takes the formatting specification and several -- arguments and returns the formatted string. The types and the number -- of arguments depend on the formatting specification. Conversely, the -- typed sscanf parses a string according to the formatting -- specification, passing parsed data to a consumer function. Again, the -- number and the types of the arguments to the consumer depend on the -- formatting specification. -- * Simple examples tp1 = sprintf $ lit "Hello world" -- "Hello world" ts1 = sscanf "Hello world" (lit "Hello world") () -- Just () tp2 = sprintf (lit "Hello " ^ lit "world" ^ char) '!' -- "Hello world!" ts2 = sscanf "Hello world!" (lit "Hello " ^ lit "world" ^ char) id -- Just '!' -- A formatting specification is built by connecting primitive -- specifications (such as lit "string", int, char) with (^). We observe -- that whereas -- sprintf $ lit "Hello world" -- has the type String, -- sprintf $ lit "The value of " ^ char ^ lit " is " ^ int -- has the type Char -> Int -> String. Likewise, the type of the consumer -- of the values parsed by sscanf varies with the formatting specification. -- The example of tp3 and ts3 demonstrates that sprintf and sscanf can -- indeed use exactly the same formatting specification, which -- is a first-class value. -- * // -- * Main idea -- The formatting specification is a `type transformer', a functor. -- * Ralf Hinze: Functional Pearl: Formatting: a class act. -- * JFP, 13(5):935-944, September 2003 -- int ===> Int-> -- Examples: -- * sprintf (lit "xxx") :: String -- * sprintf int :: Int -> String -- * sprintf (char ^ int) :: Char -> Int -> String -- * -- * sscanf inp (lit "xxx") :: x - > Maybe x -- * sscanf inp int :: (Int -> x) -> Maybe x -- * sscanf inp (char ^ int) :: (Char -> Int -> x) -> Maybe x -- * // -- Ralf Hinze representation of functors: type constructors -- * Identity data Id -- * (x->) data From x -- * Composition data C f x -- Need interpretation -- * // -- * data F a b -- * Identity data Id F a a -- * (x->) data From x F a (x->a) -- * Composition data C f x exercise -- comp:: F a b -> F b c -> F a c -- Emulating lambda in Prolog -- Currying: (X,(Y,E)) <==> ((X,Y),E) -- * // -- * DSL for string patterns class FormattingSpec repr where lit :: String -> repr a a int :: repr a (Int -> a) char :: repr a (Char -> a) fpp :: PrinterParser b -> repr a (b -> a) (^) :: repr b c -> repr a b -> repr a c infixl 5 ^ -- Unit is to keep the type of fmt3 polymorphic and away from the -- monomorphism restriction fmt3 () = lit "The value of " ^ char ^ lit " is " ^ int -- Printer/parsers (injection/projection pairs) data PrinterParser a = PrinterParser (a -> String) (String -> Maybe (a,String)) fmt :: (FormattingSpec repr, Show b, Read b) => b -> repr a (b -> a) fmt x = fpp showread -- * // -- * The interpreter for printf -- * The type of sprintf is exactly as we wanted! -- From our examples sprintf :: FPr String b -> b sprintf (FPr fmt) = fmt id -- It implements Asai's accumulator-less alternative to -- Danvy's functional unparsing newtype FPr a b = FPr ((String -> a) -> b) -- Reminds you of something? -- Right Kan extension! instance FormattingSpec FPr where lit str = FPr $ \k -> k str int = FPr $ \k -> \x -> k (show x) char = FPr $ \k -> \x -> k [x] fpp (PrinterParser pr _) = FPr $ \k -> \x -> k (pr x) (FPr a) ^ (FPr b) = FPr $ \k -> a (\sa -> b (\sb -> k (sa ++ sb))) tp3 = sprintf (fmt3 ()) 'x' 3 -- "The value of x is 3" -- * // -- * The interpreter for scanf -- * The type is again what we expected sscanf :: String -> FSc a b -> b -> Maybe a sscanf inp (FSc fmt) f = fmap fst $ fmt inp f newtype FSc a b = FSc (String -> b -> Maybe (a,String)) instance FormattingSpec FSc where lit str = FSc $ \inp x -> maybe Nothing (\inp' -> Just (x,inp')) $ prefix str inp char = FSc $ \inp f -> case inp of (c:inp) -> Just (f c,inp) "" -> Nothing fpp (PrinterParser _ pa) = FSc $ \inp f -> maybe Nothing (\(v,s) -> Just (f v,s)) $ pa inp int = fpp showread (FSc a) ^ (FSc b) = FSc $ \inp f -> maybe Nothing (\(vb,inp') -> b inp' vb) $ a inp f ts3 = sscanf "The value of x is 3" (fmt3 ()) (\c i -> (c,i)) -- Just ('x',3) -- * Other interpreters are possible (to C-style strings) -- One can easily build another interpreter, to convert a formatting -- specification to a C-style formatting string -- Tests tp4 = sprintf (lit "abc" ^ int ^ lit "cde") 5 -- "abc5cde" ts4 = sscanf "abc5cde" (lit "abc" ^ int ^ lit "cde") id -- Just 5 -- The format specification is first-class. One can build format specification -- incrementally -- This is not the case with OCaml's printf/scanf (where the -- format specification has a weird typing and is not first class). -- Unit is to keep the type of fmt3 polymorphic and away from the -- monomorphism restriction fmt50 () = lit "abc" ^ int ^ lit "cde" fmt5 () = fmt50 () ^ fmt (undefined::Float) ^ char tp5 = sprintf (fmt5 ()) 5 15 'c' -- "abc5cde15.0c" ts5 = sscanf "abc5cde15.0c" (fmt5 ()) (\i f c -> (i,f,c)) -- Just (5,15.0,'c') -- Utility functions -- Primitive Printer/parsers showread :: (Show a, Read a) => PrinterParser a showread = PrinterParser show parse where parse s = case reads s of [(v,s')] -> Just (v,s') _ -> Nothing -- A better prefixOf function -- prefix patt str --> Just str' -- if the String patt is the prefix of String str. The result str' -- is str with patt removed -- Otherwise, the result is Nothing prefix :: String -> String -> Maybe String prefix "" str = Just str prefix (pc:pr) (sc:sr) | pc == sc = prefix pr sr prefix _ _ = Nothing main = do print tp1 print ts1 print tp2 print ts2 print tp3 print ts3 print tp4 print ts4 print tp5 print ts5