4

I'm defining an AST for expression, and it has three type arguments, like following:

{-# language DeriveFunctor, DeriveFoldable, DeriveTraversable #-}

-- | a general represetation of an expression
-- , with ref info contained in r and two attributes contained in a1, a2
data Expr r a1 a2
    = Ref r a1 a2
    | App FunName [Expr r a1 a2] a1 a2
    | Lit Value a1 a2
    deriving (Functor, Foldable, Traversable)

Now using DeriveFunctor can only help me to define instance Functor (Expr r a1), so I can fmap over a2, but if I want to fmap over a1 or r, I find that it is impossible to use DeriveFunctor with a newtype , since the following code doesn't work:

newtype ExprR a1 a2 r = MkExprR { getExpr :: Expr r a1 a2 }

deriving instance Functor (ExprR a1 a2)

If I need only two type arguments, then Bifunctor may be a good idea, and there indeed is some package which provide DeriveBifunctor, but what if we need three? do we need DeriveTrifunctor or DeriveQuadfunctor etc. ?

And, what if we need more than Functor? considering Foldable, Traversable etc.

Is there any solution about this problem? how does people solve this problem in haskell practice?

luochen1990
  • 3,124
  • 1
  • 16
  • 30
  • 2
    FYI: in reasonably recent versions of GHC, `DeriveTraversable` implies both `DeriveFunctor` and `DeriveFoldable`. – dfeuer Jan 14 '19 at 05:18
  • Csongor Kiss has done some very clever work on doing the sort of thing you want with generics. I believe others have done related things as well. None of those fit into the current GHC generics system quite right; I imagine that system will be replaced within the next few years. You can probably find other solutions using Template Haskell. – dfeuer Jan 14 '19 at 05:30

1 Answers1

1

Not a direct answer to your question, but an observation. If this is the real type you are working with, it could use some factorization. a1 and a2 are used identically, and they are used once per node. You can factor this.

data ExprNode r a = ExprNode (ExprF r a) a
    -- it's a bifunctor

data ExprF r a
    = Ref r
    | App FunName [ExprNode r a]
    | Lit Value
    -- it's a bifunctor

type Expr r a1 a2 = ExprNode r (a1, a2)

Now all you need is bifunctor. Whether you want to do this depends on what your intended meanings of a1 and a2 are, but I suspect you do want to do this. Factoring out uniformity in your data types can clean up the rest of your code significantly: it can tease out hidden opportunities for generalization, reveal standard structures you are relying on, and reduce the amount of boilerplate functions by leaning more on the standard library.

luqui
  • 57,324
  • 7
  • 134
  • 191
  • To factor out the redundancy of the same data accompanying each case. But now that you mention it, I guess `App FunName [(ExprF r, a)]` or equivalent would suffice. Interesting point about performance, I usually don't worry about non-asymptotic performance things. – luqui Jan 14 '19 at 22:10
  • Yes, I saw the redundancy you were trying to get at, eventually. Another option: instead of `(a1, a2)`, use `(,,) a1 a2`, taking a functor as an argument. Not so good for deriving though. – dfeuer Jan 14 '19 at 22:12
  • 1
    This is just a sample case used to explain my question, the real situation is more complicated than this and can not be simplified via using tuple, thanks for your suggestion but it isn't my point. – luochen1990 Jan 15 '19 at 01:56