Home > Net >  How can I concisely represent a heterogenous sum type in Haskell?
How can I concisely represent a heterogenous sum type in Haskell?

Time:02-21

I'm writing a program that transcodes financial statements into a ledger. In that program I have types representing different activities:

data Withdrawal = Withdrawal { wTarget :: !Text, wAmount :: !Cash, wBalance :: !Cash }
data Fee = { fFee :: !Cash, fBalance :: !Cash }
-- many more

I use those types, because I have functions that are transaction-type specific.

I also wanted to write an activity parser that translates CSV records into those types, so I created an Activity sum type:

data Activity =
  ActivityFee Fee
  | ActivityWithdrawal Withdrawal
  | -- ...

parseActivity :: CsvRecord -> Activity

That Activity is quite boilerplate'y. Having to have a new Activity* constructor for a new activity type is slightly cumbersome.

Is there a more idiomatic or better design pattern for this problem? Was it C , std::variant would be convenient, because adding a new activity type wouldn't entail adding a new boilerplate constructor.

I've considered type-classes, but the problem with them is that they are not closed and I can't pattern match to create a function like applyActivity :: Activity -> Wallet -> Wallet. I see that I could make applyActivity into a function of an Activity class, but then problem is that this solution is only straightforward if only one argument is using this pattern. If we had two arguments like foo :: (ClassOne a, ClassTwo b) => a -> b -> c, then it's not clear to which class foo should belong.

CodePudding user response:

One option is not bothering to define the sum type, and instead make parseActivity return the Wallet -> Wallet operation that characterizes activities, wrapped in some Parser type with an Alternative instance.

parseActivity :: CsvRecord -> Parser (Wallet -> Wallet)

You would still need to define a big Parser value using a bunch of <|> that composed the Parsers for each possible activity.

Additional operations other than Wallet -> Wallet could be supported by making the parser return a record of functions:

data ActivityOps = ActivityOps { 
        applyActivity :: Wallet -> Wallet,
        debugActivity :: String
    }

This is still not as versatile as the sum type, because it constrains beforehand the operations that we might do with the activity. To support a new operation, we would need to change the Parser ActivityOps value. With the sum type, we would simply define a new function.

A variant of this solution would be to define a typeclass like

class ActivityOps a where
    applyActivity :: a -> Wallet -> Wallet
    debugActivity :: a -> String
 

And make the Parser return some kind of existential like:

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GADTSyntax #-}
data Activity where
    MakeActivity :: ActivityOps a => a -> Activity

This is sometimes frowned upon, but it would have the benefit of being able to easily invoke ActivityOps methods on activities of known type.

CodePudding user response:

Extensible sums are a possible alternative. In that case one would write

type Activity = Sum '[Fee, Withdrawal]

and use match (\fee -> ...) (\withdrawal -> ...) as a substitute for pattern matching.

  • Related