我有这样的数据类型和功能:
data Expr = Num Int | Add Expr Expr | Mult Expr Expr | Neg Expr | If Expr Expr Expr deriving (Show, Read)
prettyPrint :: Expr -> IO ()
prettyPrint expr = prettyPrint' expr 0
prettyPrint' :: Expr -> Int -> IO ()
prettyPrint' (Num x) i = putStrLn $ concat (replicate i " ") ++ "Num " ++ show x
prettyPrint' (Add x y) i = do
putStrLn $ concat (replicate i " ") ++ "Add"
prettyPrint' x (i+1)
prettyPrint' y (i+1)
prettyPrint' (Mult x y) i = do
putStrLn $ concat (replicate i " ") ++ "Mult"
prettyPrint' x (i+1)
prettyPrint' y (i+1)
prettyPrint' (Neg x) i = do
putStrLn $ concat (replicate i " ") ++ "Neg"
prettyPrint' x (i+1)
prettyPrint' (If x y z) i = do
putStrLn $ concat (replicate i " ") ++ "If"
prettyPrint' x (i+1)
prettyPrint' y (i+1)
prettyPrint' z (i+1)
在函数中,我正在使用模式匹配。问题在于它们是大量的代码重用。例如,Mult
和Add
的情况基本上是相同的代码。 Num
和Neg
也是如此。有没有一种方法可以根据表达式有多少个变量来编写此代码?像Num
和Neg
一样,因为它们只有一个变量。 Mult
和Add
的一种情况,因为它们有两个变量。还有If
的最后一种情况,因为该表达式具有三个变量。
注意:
我找到了这个答案,我认为这是比开始时更好的解决方案:
prettyPrint :: Expr -> IO ()
prettyPrint expr = putStrLn (prettyPrint' 1 expr)
prettyPrint' :: Int -> Expr -> String
prettyPrint' i (Num x) = "Num " ++ show x
prettyPrint' i expr =
let indent x = concat (replicate i " ") ++ x
(op, args) = case expr of
Add x y -> ("Add", [x,y])
Mult x y -> ("Mult", [x,y])
Neg x -> ("Neg", [x])
If x y z -> ("If", [x,y,z])
in intercalate "\n" (op : map (indent . prettyPrint' (i + 1)) args)
答案 0 :(得分:2)
首先,我将尽可能长时间地远离IO monad。让prettyPrint'
返回要打印的字符串。
prettyPrint :: Expr -> IO ()
prettyPrint = putStrLn . prettyPrint'
现在,prettyPrint'
的唯一工作是创建要打印的(可能是多行)字符串。对于数字,这很容易:只需使用show
实例。
prettyPrint' :: Expr -> String
prettyPrint' e@(Num _) = show e
-- or, ignoring the Show instance for Expr altogether
-- prettyPrint' (Num x) = "Num " ++ show x
其余的,有一种模式:
看起来像
prettyPrint' expr = let indent x = " " ++ x
(op, args) = case expr of
Add x y -> ("Add", [x,y])
Mult x y -> ("Mult", [x,y])
Neg x -> ("Neg", [x])
If x y z -> ("If", [x,y,z])
in intercalate "\n" (op : map (indent . prettyPrint') args)
作为示例,请考虑prettyPrint'
对表达式Add (Num 3) (Num 5)
的作用。首先,将op
设置为"Add"
,将args
设置为[Num 3, Num 5]
。接下来,它将indent . prettyPrint'
映射到参数列表,以获得[" Num 3", " Num 5"]
。将运算符放在列表的最前面会产生["Add", " Num 3", " Num 3"]
,然后将它们与intercalate
结合会产生"Add\n Num 3\n Num 5"
。
剩余的唯一样板位于case
表达式中。我认为可以消除这种情况,但这需要一定程度的我不熟悉的通用编程。我确信其他人可能会回答我以解决此问题。
答案 1 :(得分:1)
是的,只需创建一个函数即可打印Expr列表:
import Control.Monad (forM_)
printExprList::[Expr]->Int->String->IO ()
printExprList exprs i desc = do
putStrLn $ concat (replicate i " ") ++ desc
forM_ (zip exprs [i..]) $ \(e, j)-> prettyPrint' e (j+1)
然后调用它进行打印:
prettyPrint' :: Expr -> Int -> IO ()
prettyPrint' (Add x y) i = printExprList [x, y] i "Add"
prettyPrint' (Mult x y) i = printExprList [x, y] i "Mult"
prettyPrint' (Neg x) i = printExprList [x] i "Neg"
prettyPrint' (If x y z) i = printExprList [x, y, z] i "If"
prettyPrint' (Num x) i = putStrLn $ concat (replicate i " ")
++ "Num " ++ show x
答案 2 :(得分:1)
通常,在处理代码中的重复项时,请牢记rule of three。两次出现代码块不一定是问题。
也就是说,Haskell是一种(非常)强类型的语言,因此您通常无法像在Erlang或Clojure中那样对arity进行模式匹配。
如果您真的想抽象出递归数据结构的递归部分,则可以为其定义 catamorphism 。人们通常也将其称为 fold (折叠),因此我们保留一个更友好的名称:
data Expr =
Num Int | Add Expr Expr | Mult Expr Expr | Neg Expr | If Bool Expr Expr deriving (Show, Read)
foldExpr ::
(Int -> a) -> (a -> a -> a) -> (a -> a -> a) -> (a -> a) -> (Bool -> a -> a -> a) -> Expr -> a
foldExpr num _ _ _ _ (Num x) = num x
foldExpr num add mul neg iff (Add x y) =
add (foldExpr num add mul neg iff x) (foldExpr num add mul neg iff y)
foldExpr num add mul neg iff (Mult x y) =
mul (foldExpr num add mul neg iff x) (foldExpr num add mul neg iff y)
foldExpr num add mul neg iff (Neg x) = neg (foldExpr num add mul neg iff x)
foldExpr num add mul neg iff (If b x y) =
iff b (foldExpr num add mul neg iff x) (foldExpr num add mul neg iff y)
这是一个完全通用的函数,使您可以将任何Expr
的值转换为a
类型的任何值,而不必担心每次都要重新实现递归。您只需要提供处理每种情况的功能即可。
例如,您可以轻松编写评估器:
evaluate :: Expr -> Int
evaluate = foldExpr id (+) (*) negate (\p x y -> if p then x else y)
(顺便说一句,顺便说一句,我更改了If
的定义,因为我看不到OP定义的工作原理。)
您也可以编写将Expr
值转换为字符串的函数,尽管该函数只是草图;它需要缩进或括号逻辑才能正常工作:
prettyPrint :: Expr -> String
prettyPrint =
foldExpr
show -- Num
(\x y -> x ++ "+" ++ y) -- Add
(\x y -> x ++ "*" ++ y) -- Mult
(\x -> "(-" ++ x ++ ")") -- Neg
(\p x y -> "if " ++ show p ++ " then " ++ x ++ " else " ++ y) -- If
您可以在GHCi中试用:
*Q53284410> evaluate (Num 42)
42
*Q53284410> evaluate (Add (Num 40) (Num 2))
42
*Q53284410> evaluate (Add (Mult (Num 4) (Num 10)) (Num 2))
42
*Q53284410> prettyPrint $ Num 42
"42"
*Q53284410> prettyPrint $ Mult (Num 6) (Num 7)
"6*7"
*Q53284410> prettyPrint $ Add (Mult (Num 2) (Num 3)) (Num 7)
"2*3+7"