简体   繁体   English

Haskell - 与数据类型匹配的模式

[英]Haskell - Pattern matching with data types

I have a data type and function like this: 我有一个这样的数据类型和函数:

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) 

In the function I am using pattern matching. 在我正在使用模式匹配的函数中。 The problem is that their is a lot of reuse of code. 问题是他们的代码重用很多。 For example, the case for Mult and Add is basically the same code. 例如, MultAdd情况基本上是相同的代码。 Same goes for Num and Neg . NumNeg Is there a way to write this based on how many variables the expression have? 有没有办法根据表达式有多少变量来编写? Like one for Num and Neg , since they have only one variable. 就像NumNeg ,因为它们只有一个变量。 One case for Mult and Add , since they have two variables. MultAdd一个案例,因为它们有两个变量。 And a last case for If , since that expression have three variables. If的最后一个例子,因为该表达式有三个变量。

NOTE: 注意:

I landed on this answer, I think it's a better solution than I started with: 我找到了这个答案,我认为这是一个比我开始时更好的解决方案:

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)

First, I would stay out of the IO monad for as long as possible. 首先,我会尽可能长时间地离开IO monad。 Have prettyPrint' return a string to be printed. prettyPrint'返回一个要打印的字符串。

prettyPrint :: Expr -> IO ()
prettyPrint = putStrLn . prettyPrint'

Now, the only job of prettyPrint' is to create a (possibly multiline) string to be printed. 现在, prettyPrint'的唯一工作就是创建一个(可能是多行)字符串来打印。 For numbers, that's easy: just use the show instance. 对于数字,这很简单:只需使用show实例。

prettyPrint' :: Expr -> String
prettyPrint' e@(Num _) = show e
-- or, ignoring the Show instance for Expr altogether
-- prettyPrint' (Num x) = "Num " ++ show x

For the rest, there is a pattern: 其余的,有一个模式:

  1. Identify the constructor 确定构造函数
  2. Identify its arguments 确定其论点
  3. Join the constructor name and its pretty-printed arguments with newlines. 使用换行符加入构造函数名称及其漂亮打印的参数。 Each argument will be indented one level relative to its operator; 每个参数将相对于其运算符缩进一个级别; the recursion will take care of multiple levels of indentation. 递归将处理多个级别的缩进。

That will look like 这看起来像

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)

As an example, consider what prettyPrint' will do with the expression Add (Num 3) (Num 5) . 作为一个例子,考虑一下prettyPrint'将使用表达式Add (Num 3) (Num 5) First, it sets op to "Add" and args to [Num 3, Num 5] . 首先,它将op设置为"Add"并将args[Num 3, Num 5] Next, it maps indent . prettyPrint' 接下来,它映射indent . prettyPrint' indent . prettyPrint' over the argument list, to get [" Num 3", " Num 5"] . indent . prettyPrint'在参数列表上,得到[" Num 3", " Num 5"] Putting the operator on the front of the list yields ["Add", " Num 3", " Num 3"] , then joining them with intercalate produces "Add\\n Num 3\\n Num 5" . 将操作符放在列表的前面会产生["Add", " Num 3", " Num 3"] ,然后使用intercalate连接它们会产生"Add\\n Num 3\\n Num 5"


The only remaining boilerplate is in the case expression. 唯一剩下的样板是在case表达式中。 I think it's possible to eliminate that, but it requires a level of generic programming I'm not familiar with. 我认为可以消除它,但它需要一定程度的我不熟悉的通用编程。 I'm sure someone else could probably run with my answer to fix that. 我相信其他人可能会以我的答案来解决这个问题。

Yes, just create a function to print list of Expr: 是的,只需创建一个打印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)

and then call it to print: 然后调用它打印:

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

In general, when addressing duplication in code, it pays to keep the rule of three in mind. 一般来说,在解决代码中的重复问题时,要记住三个规则是值得的 Two occurrences of a block of code isn't necessarily a problem. 两次出现的代码块不一定是个问题。

That said, Haskell is a (very) strongly-typed language, so you generally can't pattern-match on arity like you can in, say, Erlang or Clojure. 也就是说,Haskell是一种(非常)强类型的语言,所以你通常不能像在Erlang或Clojure中那样在arity上进行模式匹配。

If you really want to abstract away the recursion part of a recursive data structure, you can define the catamorphism for it. 如果你真的想抽象出递归数据结构的递归部分,你可以为它定义catamorphism People often also call this a fold , so let's keep that slightly more friendly name: 人们通常也称之为折叠 ,所以让我们保持这个稍微友好的名字:

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)

This is an entirely generic function that enables you turn turn any Expr value into any value of the type a , without worrying about reimplementing recursion every time. 这是一个完全通用的函数,可以将任何Expr值转换为类型a任何值,而无需每次都重新实现递归。 You just have to supply functions that deal with each of the cases. 您只需提供处理每种情况的函数。

You can, for example, easily write an evaluator: 例如,您可以轻松编写评估程序:

evaluate :: Expr -> Int
evaluate = foldExpr id (+) (*) negate (\p x y -> if p then x else y)

(Notice, BTW, that I changed the definition of If , because I couldn't see how the OP definition would work.) (注意,顺便说一句,我改变了If的定义,因为我看不出OP定义是如何工作的。)

You can also write a function to turn an Expr value into a string, although this one is just a sketch ; 您还可以编写一个函数将Expr值转换为字符串,尽管这只是一个草图 ; it needs indentation or bracket logic to work correctly: 它需要缩进或括号逻辑才能正常工作:

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

You can try it out in GHCi: 你可以在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"

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM