I'm trying to use DataKinds
along with type-level literals to make a type safe currency conversion library. So far I have defined these data types:
data Currency (s :: Symbol) = Currency Double
deriving Show
type USD = Currency "usd"
type GBP = Currency "gbp"
usd :: Double -> USD
usd = Currency
gbp :: Double -> GBP
gbp = Currency
data SProxy (s :: Symbol) = SProxy
Along with a function that allows me to convert between them:
convert :: forall a b. (KnownSymbol a, KnownSymbol b) => Currency a -> Currency b
convert (Currency a) = case (symbolVal (SProxy :: SProxy a),
symbolVal (SProxy :: SProxy b)) of
("usd", "gbp") -> Currency (a * 0.75)
("gbp", "usd") -> Currency (a * 1.33)
Here, I have used ScopedTypeVariables
to supply the constraint KnownSymbol a
to symbolVal SProxy
. This works fine, however, I would like to be able to make the conversion rates update from an external source, perhaps a text file or an API such as fixer.
Obviously, I could just wrap the return type in IO
, forming
convert :: forall a b. (KnownSymbol a, KnownSymbol b) => Currency a -> IO (Currency b)
but I would like to be able to keep a pure API. My first thought was to have the conversion rate map be obtained using unsafePerformIO
, but this is unsafe so I instead thought I could use another function getConvert
with a type to the effect of
getConvert :: IO (forall a b. (KnownSymbol a, KnownSymbol b) => Currency a -> Currency b)
(i.e an IO action returning a convert
type function) so that it could be used like this:
do
convert <- getConvert
print $ convert (gbp 10) :: USD
However, I have been unable to get this to type check - GHC complains that it:
Couldn't match expected type ‘forall (a :: Symbol) (b :: Symbol).
(KnownSymbol a, KnownSymbol b) =>
Currency a -> Currency b’
with actual type ‘Currency a0 -> Currency b0’
When I had GHC infer the type of return convert
, it did not infer the type I wanted but instead moved the forall a b
to the prenex position, which type checks, until I try to use convert' <- getConvert
, at which point it fails saying that there is No instance for (KnownSymbol n0)
My question is why does this not type check, and what would be the correct type for the function getConvert
?
First I thought that it may be the fact both ScopedTypeVariables
and RankNTypes
use the forall
quantifier in different ways, but toggling RankNTypes
has had no effect. I also tried moving the quantifier to the front as GHC suggested but this does not give me the rank-2 type that I need.
ImpredicativeTypes
don't can't really work right; avoid them. Instead of using IO (forall a. b. ...)
you can wrap the exchange rate conversion table in a data
type that preserves its polymorphic type.
data ExchangeRates = ExchangeRates {
getER :: forall a b. (KnownSymbol a, KnownSymbol b) => Currency a -> Currency b
}
and return an IO ExchangeRates
instead
-- getConvert :: IO (forall a b. (KnownSymbol a, KnownSymbol b) => Currency a -> Currency b)
getConvert :: IO ExchangeRates
getConvert = return (ExchangeRates convert)
And use it almost as you expected. Notice the parenthesis to group the :: USD
type signature with the converted value.
main = do
convert <- getER <$> getConvert
print $ (convert (gbp 10) :: USD)