Hey so I was reading this post on type-driven development in Java. I had trouble grokking the Java types, so I tried writing it in Haskell. However, I have two problems:
data Dollar = Dollar Double
, where a value like Dollar 4.0
is money, and Dollar
the type is the currency. And I think Dollar :: Double -> Dollar
would be something not exported.exchange :: (Money a, Money b) =>[ExchangeRate] -> a -> b
. Then a bank is just an object that contains a collection of ExchangeRates, but I don't know what type ExchangeRate is.The code I have so far is:
class Money m where
money :: (Money m) => Double -> m
amount :: (Money m) => m -> Double
add :: (Money m) => m -> m -> m
add a b = money $ amount a + amount b
class (Money a, Money b) => ExchangeablePair a b where
newtype Dollar = Dollar Double
deriving (Show, Eq)
instance Money Dollar where
money = Dollar
amount (Dollar a) = a
newtype Franc = Franc Double
deriving (Show, Eq)
instance Money Franc where
money = Franc
amount (Franc a) = a
instance ExchangeablePair Dollar Franc where
EDIT: I still want the safety of something like this: buyAmericanBigMac :: Dollar -> (BigMac, Dollar)
.
Once a bank or firm quotes the exchange rate, the customer chooses to accept the rate or not. If they accept, the transaction goes through. On top of the exchange rate conversion, certain transaction fees might also be applied to send or receive an international money transfer.
The formula is: Starting Amount (Original Currency) / Ending Amount (New Currency) = Exchange Rate. For example, if you exchange 100 U.S. Dollars for 80 Euros, the exchange rate would be 1.25. But if you exchange 80 Euros for 100 U.S. Dollars, the exchange rate would be 0.8. Calculate the foreign currency amount.
First note that to be safe, exchange
should have type
exchange :: (Money a, Money b) => [ExchangeRate] -> a -> Maybe b
because if you don't have a
or b
in your list of rates you can't return anything.
For ExchangeRate
we could use:
newtype ExchangeRate = Rate { unrate :: (TypeRep, Double) }
deriving Show
The TypeRep
is a unique "fingerprint" for a type. You can get a TypeRep
by calling typeOf
on something with a Typeable
instance. Using this class we can write a type safe lookup for exchange rates:
findRate :: Typeable a => [ExchangeRate] -> a -> Maybe Double
findRate rates a = lookup (typeOf a) (map unrate rates)
Then we can implement your exchange function:
exchange :: forall a b. (Money a, Money b) => [ExchangeRate] -> a -> Maybe b
exchange rates a = do
aRate <- findRate rates a
bRate <- findRate rates (undefined :: b)
return $ money (bRate * (amount a / aRate))
Here we use the ScopedTypeVariables
extension so we can write undefined :: b
(note we need to write forall a b.
as well for this to work)
Here's a minimal working example. Instead of [ExchangeRate]
I've used a HashMap
(it's faster and stops users from combining exchanges rates that don't belong together).
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DeriveDataTypeable #-}
module Exchange
( Dollar
, Franc
, exchange
, sampleRates
, sampleDollars
) where
import Data.HashMap.Strict as HM
import Data.Typeable
class Typeable m => Money m where
money :: Money m => Double -> m
amount :: Money m => m -> Double
add :: Money m => m -> m -> m
add a b = money $ amount a + amount b
newtype Dollar = Dollar Double
deriving (Show, Eq, Typeable)
instance Money Dollar where
money = Dollar
amount (Dollar a) = a
newtype Franc = Franc Double
deriving (Show, Eq, Typeable)
instance Money Franc where
money = Franc
amount (Franc a) = a
newtype ExchangeRates = Exchange (HashMap TypeRep Double)
deriving Show
findRate :: Typeable a => ExchangeRates -> a -> Maybe Double
findRate (Exchange m) a = HM.lookup (typeOf a) m
exchange :: forall a b. (Money a, Money b) => ExchangeRates -> a -> Maybe b
exchange rates a = do
aRate <- findRate rates a
bRate <- findRate rates (undefined :: b)
return $ money (bRate * (amount a / aRate))
sampleRates :: ExchangeRates
sampleRates = Exchange $ HM.fromList
[ (typeOf (Dollar 0), 1)
, (typeOf (Franc 0) , 1.2)
]
sampleDollars :: Dollar
sampleDollars = Dollar 5
Then you can write
> exchange sampleRates sampleDollars :: Maybe Franc
Just (Franc 6.0)
As other other people have mentioned, Double
isn't really suitable because you can get floating point errors. If you're doing anything with real money I'd recommend using scientific.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With