You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何避免含大量相似构造器的Haskell数据类型代码重复?

Reducing Redundancy in AST Traversal for 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 10:02:48