Haskell - 数据类型的模式匹配

5

我有一个数据类型和以下的函数:

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) 

在我使用模式匹配的函数中,问题在于代码的重复使用。例如,MultAdd的情况基本上是相同的代码。对于NumNeg也是如此。是否有一种方法可以根据表达式包含的变量数量来编写代码?例如,对于只有一个变量的NumNeg,可以写一个case。对于有两个变量的MultAdd,可以写一个case。对于有三个变量的If,可以写一个最后的case。
注意:
我找到了这个答案,我认为它比我开始的解决方案更好:
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)
3个回答

2

首先,尽可能避免使用IO单子。让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

对于其余部分,有一个模式:

  1. 确定构造函数
  2. 确定它的参数
  3. 使用换行符连接构造函数名称和其漂亮打印的参数。每个参数相对于其运算符缩进一级; 递归将处理多个级别的缩进。

看起来会像这样:

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)

例如,考虑使用表达式Add (Num 3) (Num 5)来说明prettyPrint'的作用。首先,它将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
一般来说,当解决代码重复时,牢记三次原则是很有帮助的。两个代码块的出现并不一定是一个问题。
话虽如此,Haskell是一种(非常)强类型语言,因此通常无法像Erlang或Clojure那样按参数模式匹配。
如果您真的想抽象递归数据结构的递归部分,可以为其定义范畴论。人们通常也将其称为折叠,因此让我们保留这个稍微友好的名称:
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的定义,因为我看不出原来的定义会怎样工作。)
你也可以编写一个函数将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"

0

是的,只需创建一个打印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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接