Подтвердить что ты не робот

Как моделировать валюты, деньги и банки, которые обменивают деньги между валютами?

Эй, поэтому я читал этот пост о разработке с использованием типов в 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).

4b9b3361

Ответ 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