Haskell-模式匹配数据类型

时间:2018-11-13 15:34:05

标签: haskell

我有这样的数据类型和功能:

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一样,因为它们只有一个变量。 MultAdd的一种情况,因为它们有两个变量。还有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)

3 个答案:

答案 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

其余的,有一种模式:

  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)

作为示例,请考虑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"