如何避免含大量相似构造器的Haskell数据类型代码重复?
Great question! This is such a common frustration when dealing with algebraic data types (ADTs) that have a bunch of structurally identical constructors. Let’s walk through a few solid approaches to clean up that repetitive refVars code:
1. Refactor Your AST to Abstract Binary Operations
The cleanest long-term fix is to restructure your AST to group all binary operations under a single constructor. This eliminates repeated pattern matches entirely:
-- First define a separate type for binary operation kinds data BinOp = AddOp | SubOp | MulOp | DivOp deriving (Show, Eq) -- Update your AST to use a single BinOp constructor data AST = Imm Integer | ArgName String | Arg Integer | BinOp BinOp AST AST deriving (Show, Eq)
Now your refVars function only needs one case for all binary operations:
refVars :: M.Map String Integer -> AST -> Maybe AST refVars d (ArgName s) = Arg <$> d M.!? s refVars _ (Imm n) = Just $ Imm n refVars _ (Arg n) = Just $ Arg n refVars d (BinOp op a1 a2) = BinOp op <$> refVars d a1 <*> refVars d a2
Pros: No redundancy, makes adding new binary operations trivial later on.
Cons: Requires modifying your existing AST definition, which might ripple through other code if your project is already large.
2. Use View Patterns to Group Similar Constructors
If you can’t modify your AST (e.g., it’s used extensively elsewhere), view patterns let you create a "virtual" pattern that matches all binary operation constructors:
First define a helper function that converts binary ops into a common structure:
import GHC.ViewPatterns -- Extracts the operation constructor and its arguments asBinOp :: AST -> Maybe (AST -> AST -> AST, AST, AST) asBinOp (Add a1 a2) = Just (Add, a1, a2) asBinOp (Sub a1 a2) = Just (Sub, a1, a2) asBinOp (Mul a1 a2) = Just (Mul, a1, a2) asBinOp (Div a1 a2) = Just (Div, a1, a2) asBinOp _ = Nothing
Then use this view pattern in refVars:
refVars :: M.Map String Integer -> AST -> Maybe AST refVars d (ArgName s) = Arg <$> d M.!? s refVars _ (Imm n) = Just $ Imm n refVars _ (Arg n) = Just $ Arg n refVars d (asBinOp -> Just (op, a1, a2)) = op <$> refVars d a1 <*> refVars d a2
Pros: No changes to your original AST, keeps refVars concise and readable.
Cons: You still have to write the asBinOp helper once, but it’s a one-time cost that keeps your traversal logic clean.
3. Leverage Generic Programming
For more complex ADTs, you can use Haskell’s Generic typeclass to automatically derive traversal logic, avoiding manual pattern matches entirely. You’ll need a library like generic-lens to simplify this:
First, add Generic to your AST derivation:
{-# LANGUAGE DeriveGeneric #-} import GHC.Generics import Data.Generics.Lens (traverseOf, traverse) -- From generic-lens data AST = Imm Integer | ArgName String | Arg Integer | Add AST AST | Sub AST AST | Mul AST AST | Div AST AST deriving (Show, Eq, Generic)
Then rewrite refVars using generic traversal. We handle the ArgName case explicitly, and let the generic traversal handle recursing into all other sub-ASTs:
refVars :: M.Map String Integer -> AST -> Maybe AST refVars d ast = traverseOf traverse go ast where go :: AST -> Maybe AST go (ArgName s) = Arg <$> d M.!? s go other = Just other
Pros: Automatically handles new constructors added to the AST later, no manual pattern matching for recursive cases.
Cons: Adds a dependency on a generic library, with a small learning curve if you’re new to generic programming.
内容的提问来源于stack exchange,提问作者Reuben Dubester

