试图建立一个处理不同货币货币金额的应用实例,我是否走错了路?

时间:2016-01-16 13:21:46

标签: haskell

我又在Haskell再次上课(阅读了令人敬畏的learnyouahaskell教程),我拼命寻找钉子来应用这个壮观的锤子。

我的日常工作是电子商务,我们有大量的软件设计问题来操纵价格。所以我想,使用Haskell类型系统抽象出货币之间的转换或税前/税后价格同时保证正确性可能会很好。

这是我想要实现的一个粗略概念(显然不是编译,否则不会在这里):

data Money a = Amount String a | ConversionError
    deriving Show

convert :: String -> Money Double -> Money Double
convert toCurrency (Amount fromCurrency 0) = Amount toCurrency 0
convert "EUR" (Amount "USD" x) = Amount "EUR" (0.92 * x)
convert "USD" (Amount "EUR" x) = Amount "USD" (1.09 * x)
convert _ _ = ConversionError

instance Functor Money where
    fmap f (Amount currency number) = Amount currency (f number)

instance Applicative Money where
    ConversionError <*> _ = ConversionError
    Amount x f <*> amount = let Amount _ n = convert x amount
                            in
                                Amount x (f n)
    pure = Amount "EUR"

eur5 = Amount "EUR" 5
usd5 = Amount "USD" 5


main = do
    print $ fmap (+1) eur5 -- Amount "EUR" 6
    print $ convert "EUR" usd5 -- Amount "EUR" 4.6000000000000005
    print $ (+) <$> eur5 <*> usd5 -- I'd like it to print: Amount "EUR" 9.58

这不会编译,因为:

Couldn't match type ‘a’ with ‘Double’
  ‘a’ is a rigid type variable bound by
      the type signature for
        (<*>) :: Money (a -> b) -> Money a -> Money b
      at src/Main.hs:14:21
Expected type: Money Double
  Actual type: Money a

我理解为什么。

我认为可能的解决方法是让Money类型带有转换功能,但它会使整个事情变得不那么优雅,我必须声明我的价格是这样的:

data Money a = Amount (String -> (Money a) -> (Money a)) a
eur5 = Amount convert "EUR" 5

我真正喜欢的是,只有在计算发生时才开始担心转换,而不需要将转换函数嵌入Money值中。

那么,我是否走上了正确的道路,但却误解了一些事情,或者是否真的不是解决方案来简化这类问题?

2 个答案:

答案 0 :(得分:5)

  

应用实际上不是解决这类问题的解决方案吗?

是的,事实并非如此。正如你已经说过的那样,问题在于

(<*>) :: Applicative f => f (a -> b) -> f a -> f b

需要与ab合作,但您只允许a ~ Doubleb ~ Double

但是,如果我们将任何Applicative f视为知道如何应用函数的容器,那么在那里存储某种Money似乎相当奇怪。此外,您似乎只想要Applicative (+) <$> a <*> b实例,这是一个误用。相反,写一个小帮手:

($+) :: Money a -> Money a -> Money a
(Amount c a) $+ (Amount c' b) = ...

这也为您以后的更改提供了更多的力量。例如,您可以使用幻像类型而不是当前的方法来使用货币标记Money:

newtype Money c a = Amount a deriving Show

data USD
data EUR

usd :: Num a => a -> Money USD a
usd = Amount

eur :: Num a => a -> Money EUR a
eur = Amount

($+) :: Money c a -> Money c a -> Money c a
(Amount a) $+ (Amount b) = Amount $ a + b

-- eur 5 $+ usd 4 = type error

但是,请记住,货币对于编译时转换来说是一种相当糟糕的类型,因为费率一直在变化。更现实的方法是

($+) :: Fractional a => CurrencyEnv -> Money a -> Money a -> Maybe (Money a)

您在程序开头加载CurrencyEnv。请记住,您不应该使用Double来执行与货币相关的任务,除非您想向老板解释为什么您错过了一些便士。 Data.Ratio.Rational更适合。

答案 1 :(得分:1)

我认为你想要的是一个&#34; unit of measure&#34;,这是一个代数理论的类型商,统一说,dollar.eur ^ 2 with eur.dollar.eur < / p>

Zeta的解决方案使用phantom types来确保来自内部&#34;一些一致性,同时让人们使用你的类型&#34;来自外部&#34;

这是第一步,但缺少等同部分,这可能是一个问题。

我认为需要一个环解算器(不是这个术语吗?)来处理类型的指数之间的等价。