I wrote a toy library that uses dependent types to represent money with its currency in the type signature:
data Currency = CHF | EUR | PLN | USD
deriving stock (Bounded, Enum, Eq, Read, Show)
data CurrencyWitness (c :: Currency) where
CHFWitness :: CurrencyWitness CHF
EURWitness :: CurrencyWitness EUR
PLNWitness :: CurrencyWitness PLN
USDWitness :: CurrencyWitness USD
deriving stock instance Eq (CurrencyWitness c)
deriving stock instance Show (CurrencyWitness c)
data Money (currency :: Currency) representation = Money
{ moneyCurrency :: !(CurrencyWitness currency)
, moneyAmount :: !representation
}
deriving stock (Eq, Show)
add :: (Num r) => Money c r -> Money c r -> Money c r
add (Money witness1 amount1) (Money _ amount2) =
Money witness1 (amount1 + amount2)
data SomeMoney r where
SomeMoney :: Money c r -> SomeMoney r
How can I now write addSomeMoney :: (Num r) => SomeMoney r -> SomeMoney r -> Maybe (SomeMoney r)
that safely adds money only if they are of the same currency? You can imagine that this might be needed in a situation where users provide money arguments, so we can't check their currency at compile time.
My best approach is:
addSomeMoney (SomeMoney m@(Money c _)) (SomeMoney m'@(Money c' _)) = case (c, c') of
(CHFWitness, CHFWitness) -> Just . SomeMoney $ add m m'
(PLNWitness, PLNWitness) -> Just . SomeMoney $ add m m'
-- ...
(_, _) -> Nothing
This approach has the drawback that it is cumbersome to write, repetitive, and becomes faulty when I add a new currency as the compiler won't warn me that that case is not handled.
addSomeMoney (SomeMoney m@(Money c _)) (SomeMoney m'@(Money c _)) = _
doesn't work. GHC complains about duplicate c
: Conlicting definitions of 'c'
.
A completely "type safe" approach is possible, but we have to write some boilerplate.
We start from the OP's code, mostly verbatim. We only add a few extensions, enable warnings (which should more properly be done elsewhere, but let's keep this simply) and
import Data.Typeable
.(I haven't checked if some extensions can be removed)
Now the new part.
We first "prove" that any currency type
c
satisfies theTypeable
constraint. This is trivial to do, but requires some boilerplate.Importantly, adding new currencies will trigger a non-exhaustive patter matching warning, so it is reasonably easy to keep it in sync.
Then, the currency-adding function. We invoke
withTypeableCurrency
to put twoTypeable
constraints in scope. Then we leverageeqT
to check if the two currency types are the same. In the affirmative case, we do not get a boringTrue
boolean, but a convenientRefl
which allows the compiler to identify the types. The rest is then easy.The advantage of this approach is that the boilerplate is confined within
withTypeableCurrency
, and does not have to be repeated in each function dealing withSomeMoney
likeaddSomeMoney
,subtractSomeMoney
, etc.Update: some of the boilerplate can be removed exploiting the
singletons
package and some Template Haskell.The key function is even simpler, exploiting
%~
.