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 Parser
s 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.