3

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.

gregorias
  • 181
  • 9
  • You could try using [template haskell](https://hackage.haskell.org/package/template-haskell) - it seems to be [looked down upon](https://stackoverflow.com/q/10857030/17592995) by the community but I don't see a downside to using it for this kind of boilerplate generation. You should be able to reduce the declaration of `Activity` to something like: `mkActivity [''Withdrawal, ''Fee,...]`. [This](https://www.parsonsmatt.org/2015/11/15/template_haskell.html) is a good tutorial – Vikstapolis Feb 20 '22 at 10:01
  • Other than template haskell as mentioned above, `Typeable` is probably the closest you can get to what you desire (runtime type checking). Making unions *tagged* is intentional by the design of Haskell and is the correct decision IMO. But I do get your point. – Chase Feb 20 '22 at 10:09
  • 2
    Personally, I like [pattern synonyms](https://downloads.haskell.org/ghc/latest/docs/html/users_guide/exts/pattern_synonyms.html). One can write, e.g., `pattern K x y = K1 (K2 (K3 x y))` and then simply write `foo (K x y) = K (x+1) (y-1)` as if `K` were a constructor. You'll need to define your synonyms, so some boilerplate is still around (unless you automate it with Template Haskell). This won't reduce the boilerplate in defining the types, but that in using them later on. – chi Feb 20 '22 at 10:17
  • If you have two arguments, use a two-argument typeclass. `foo :: ClassBoth a b => a -> b -> c` – Daniel Wagner Feb 20 '22 at 13:09
  • So you basically want something like `Either`, but with support for an arbitrary number of choices instead of just two? – Joseph Sible-Reinstate Monica Feb 20 '22 at 19:18
  • @JosephSible-ReinstateMonica I'd say that's close to what I'm considering. – gregorias Feb 20 '22 at 19:50

2 Answers2

2

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.

danidiaz
  • 26,936
  • 4
  • 45
  • 95
  • Related: the "return a command" trick. https://www.haskellforall.com/2021/10/the-return-command-trick.html – danidiaz Feb 20 '22 at 13:20
0

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.

gregorias
  • 181
  • 9