Как моделировать валюты, деньги и банки, которые обменивают деньги между валютами?
Эй, поэтому я читал этот пост о разработке с использованием типов в Java. Мне не удалось разобраться с типами Java, поэтому я попытался написать его в Haskell. Однако у меня есть две проблемы:
- Я не знаю, как реализовать разницу между валютой и фактическим кусочком денег. Сначала я думал, что валюта - это всего лишь тип денег (и я думаю, что это имеет смысл), как это
data Dollar = Dollar Double
, где значение, подобное Dollar 4.0
, - это деньги, а Dollar
тип - валюта. И я думаю, что Dollar :: Double -> Dollar
будет чем-то не экспортированным.
- Это приводит к тому, что я не могу моделировать банк, который обменивает деньги. Я думал что-то вроде
exchange :: (Money a, Money b) =>[ExchangeRate] -> a -> b
. Тогда банк - это просто объект, который содержит коллекцию ExchangeRates, но я не знаю, какой тип ExchangeRate существует.
Код, который у меня есть до сих пор:
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
РЕДАКТИРОВАТЬ: Я все еще хочу сохранить что-то вроде этого: buyAmericanBigMac :: Dollar -> (BigMac, Dollar)
.
Ответы
Ответ 1
Прежде всего, убедитесь, что exchange
должен иметь тип
exchange :: (Money a, Money b) => [ExchangeRate] -> a -> Maybe b
потому что, если у вас нет a
или b
в вашем списке тарифов, вы ничего не можете вернуть.
Для ExchangeRate
мы могли бы использовать:
newtype ExchangeRate = Rate { unrate :: (TypeRep, Double) }
deriving Show
TypeRep
- уникальный "отпечаток пальца" для типа. Вы можете получить TypeRep
, вызвав typeOf
на что-то с экземпляром Typeable
. Используя этот класс, мы можем написать безопасный поиск типов для обменных курсов:
findRate :: Typeable a => [ExchangeRate] -> a -> Maybe Double
findRate rates a = lookup (typeOf a) (map unrate rates)
Затем мы можем реализовать вашу функцию обмена:
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))
Здесь мы используем расширение ScopedTypeVariables
, чтобы мы могли написать undefined :: b
(обратите внимание, что нам нужно написать forall a b.
, чтобы это работало)
Вот минимальный рабочий пример. Вместо [ExchangeRate]
я использовал HashMap
(он быстрее и не позволяет пользователям комбинировать ставки обмена, которые не принадлежат друг другу).
{-# 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
Затем вы можете написать
> exchange sampleRates sampleDollars :: Maybe Franc
Just (Franc 6.0)
Как отмечали другие люди, Double
не подходит, потому что вы можете получить ошибки с плавающей запятой. Если вы делаете что-либо с реальными деньгами, я бы рекомендовал использовать scientific.
Ответ 2
Нет, не использует класс. Давайте начнем с основ:
Итак, вы хотите представлять разные типы валют? Пусть используется простой тип алгебраических данных:
data CurrencyType = Dollar | Franc deriving (Show)
Вы хотите представить деньги, снова используйте простой тип данных:
data Money = Money {
amount :: Double,
mType :: CurrencyType
} deriving (Show)
Некоторая демонстрация в ghci:
*Main> let fiveDollars = Money 5 Dollar
*Main> fiveDollars
Money {amount = 5.0, mType = Dollar}
Теперь вам нужна возможность конвертировать деньги из одного типа валюты в
другой. Это снова может быть достигнуто простой функцией:
convertMoney :: CurrencyType -> Money -> Money
convertMoney Dollar money = undefined -- logic for Converting money to Dollar
convertMoney Franc money = undefined -- logic for converting money to Franc
Мое общее правило для перехода к классам типов - это когда я хочу представить некоторую конкретную абстракцию, которая имеет некоторые четко определенные законы. В большинстве случаев простые типы данных и функции, действующие на них, сделают хороший пример.
UPDATE на основе ваших комментариев: если вы хотите объявить свой собственный тип денег, вы можете следовать этому подходу:
data CurrencyType a = CurrencyType a deriving (Show)
data Dollar = Dollar deriving (Show)
data Money a = Money Double (CurrencyType a) deriving (Show)
Демо в ghci:
λ> let fiveDollars = Money 5 (CurrencyType Dollar)
λ> fiveDollars
Money 5.0 (CurrencyType Dollar)
Теперь скажем, вы хотите определить другую валюту Franc
. Затем просто определите для него тип данных:
data Franc = Franc deriving (Show)
И вы можете определить из этого деньги:
λ> let fiveFranc = Money 5 (CurrencyType Franc)
λ> fiveFranc
Money 5.0 (CurrencyType Franc)
>> I can't write a function that only takes Dollars at compile time.
Ну, ты можешь.
convertFromDollar :: Money Dollar -> Money Franc
convertFromDollar x = undefined -- Write your logic here
Ответ 3
Как я буду реализовывать его в Haskell на основе того, как я это сделал в PHP на работе:
module Money where
-- For instance Show Money
import Text.Printf
-- Should perhaps be some Decimal type
type Amount = Double
-- Currency type
data Currency = Currency { iso4217 :: String } deriving Eq
instance Show Currency where
show c = iso4217 c
-- Money type
data Money = Money { amount :: Amount, currency :: Currency }
instance Show Money where
show m = printf "%0.2f" (amount m) ++ " " ++ show (currency m)
-- Conversion between currencies
data BasedRates = BasedRates { base :: Currency, rate :: Currency -> Amount }
type CrossRates = Currency -> Currency -> Amount
makeCrossRatesFromBasedRates :: BasedRates -> CrossRates
makeCrossRatesFromBasedRates (BasedRates { base=base, rate=rate }) =
\ fromCurrency toCurrency -> rate toCurrency / rate fromCurrency
convert :: CrossRates -> Currency -> Money -> Money
convert crossRates toCurrency (Money { amount=amount, currency=fromCurrency })
= Money { amount = crossRates fromCurrency toCurrency * amount, currency=toCurrency }
-- Examples
sek = Currency { iso4217 = "SEK" }
usd = Currency { iso4217 = "USD" }
eur = Currency { iso4217 = "EUR" }
sekBasedRates = BasedRates {
base = sek,
rate = \currency -> case currency of
Currency { iso4217 = "SEK" } -> 1.0000
Currency { iso4217 = "USD" } -> 6.5432
Currency { iso4217 = "EUR" } -> 9.8765
}
crossRates = makeCrossRatesFromBasedRates sekBasedRates
usdPrice = Money { amount = 23.45, currency = usd }
sekPrice = convert crossRates sek usdPrice
eurPrice = convert crossRates eur usdPrice