'How can I concisely represent a heterogenous sum type in Haskell?
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.
Solution 1:[1]
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.
Solution 2:[2]
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.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | gregorias |
