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.