How to reduce code duplication when dealing with recursive sum types

前端 未结 2 969
我在风中等你
我在风中等你 2021-01-31 14:04

I am currently working on a simple interpreter for a programming language and I have a data type like this:

data Expr
  = Variable String
  | Number Int
  | Add          


        
2条回答
  •  旧巷少年郎
    2021-01-31 14:47

    As an alternative approach, this is also a typical use case for the uniplate package. It can use Data.Data generics rather than Template Haskell to generate the boilerplate, so if you derive Data instances for your Expr:

    import Data.Data
    
    data Expr
      = Variable String
      | Number Int
      | Add [Expr]
      | Sub Expr Expr
      deriving (Show, Data)
    

    then the transform function from Data.Generics.Uniplate.Data applies a function recursively to each nested Expr:

    import Data.Generics.Uniplate.Data
    
    substituteName :: String -> Int -> Expr -> Expr
    substituteName name newValue = transform f
      where f (Variable x) | x == name = Number newValue
            f other = other
    
    replaceSubWithAdd :: Expr -> Expr
    replaceSubWithAdd = transform f
      where f (Sub x (Number y)) = Add [x, Number (-y)]
            f other = other
    

    Note that in replaceSubWithAdd in particular, the function f is written to perform a non-recursive substitution; transform makes it recursive in x :: Expr, so it's doing the same magic to the helper function as ana does in @chi's answer:

    > substituteName "x" 42 (Add [Add [Variable "x"], Number 0])
    Add [Add [Number 42],Number 0]
    > replaceSubWithAdd (Add [Sub (Add [Variable "x", 
                         Sub (Variable "y") (Number 34)]) (Number 10), Number 4])
    Add [Add [Add [Variable "x",Add [Variable "y",Number (-34)]],Number (-10)],Number 4]
    > 
    

    This is no shorter than @chi's Template Haskell solution. One potential advantage is that uniplate provides some additional functions that may be helpful. For example, if you use descend in place of transform, it transforms only the immediate children which can give you control over where the recursion happens, or you can use rewrite to re-transform the result of transformations until you reach a fixed point. One potential disadvantage is that "anamorphism" sounds way cooler than "uniplate".

    Full program:

    {-# LANGUAGE DeriveDataTypeable #-}
    
    import Data.Data                     -- in base
    import Data.Generics.Uniplate.Data   -- package uniplate
    
    data Expr
      = Variable String
      | Number Int
      | Add [Expr]
      | Sub Expr Expr
      deriving (Show, Data)
    
    substituteName :: String -> Int -> Expr -> Expr
    substituteName name newValue = transform f
      where f (Variable x) | x == name = Number newValue
            f other = other
    
    replaceSubWithAdd :: Expr -> Expr
    replaceSubWithAdd = transform f
      where f (Sub x (Number y)) = Add [x, Number (-y)]
            f other = other
    
    replaceSubWithAdd1 :: Expr -> Expr
    replaceSubWithAdd1 = descend f
      where f (Sub x (Number y)) = Add [x, Number (-y)]
            f other = other
    
    main = do
      print $ substituteName "x" 42 (Add [Add [Variable "x"], Number 0])
      print $ replaceSubWithAdd e
      print $ replaceSubWithAdd1 e
      where e = Add [Sub (Add [Variable "x", Sub (Variable "y") (Number 34)])
                         (Number 10), Number 4]
    

提交回复
热议问题