如何将DU / ADT限制为某些案例标识符/值构造函数

时间:2016-07-15 07:50:36

标签: haskell design-patterns f#

我将如何处理以下情况? 我有一个DU(例如货币)和一些记录类型。 现在对于记录类型字段,我要求给定实例的实际值应该是相同的案例标识符(或者在Haskell中使用相同的值构造函数)

type Currency =
    | USD of decimal
    | EUR of decimal

type PositionalData = {
    grossAmount: Currency;
    pos1: Currency;
    pos2: Currency;
}

例如以下内容有效

let valid = {
    grossAmount = USD 10.0m;
    pos1 = USD 7.0m;
    pos2 = USD 3.0m;
}

此示例应该无效

let wrong = {
    grossAmount = USD 10.0m;
    pos1 = USD 7.0m;
    pos2 = EUR 3.0m;
    ^^^^^^^^^^^^^^^^
}

我知道这个特定的例子可以使用测量单位在F#中解决。但很容易想象一个通过该机制无法解决的例子。 所以我想请你考虑一个更通用的答案,而不一定只是解决了代码示例。

期待你的大脑转储; - )

PS:对于所有Haskeleers来说 - 看看ADT(可能与更高级别的kinded类型组合)如何解决这个问题会很有趣。

3 个答案:

答案 0 :(得分:4)

“直接”翻译可以

{-# LANGUAGE GADTs, DataKinds, KindSignatures #-}

data Currency = USD | EUR deriving Show

-- We use `Currency` values to create `Amount` types
--   read about types in Haskell ([Kinds][1]: *, * -> *, ...)
--   here we fix one * to be Currency
data Amount :: Currency -> * where
     -- Data constructor, take one float and return some Amount
     Amount :: Float -> Amount a

-- Extract the specific currency symbol require extra effort
instance Show (Amount a) where
    show (Amount k) = show k

-- Many amounts (same currency)
-- `a` restrict `a1` and `a1` to have the same type => the same currency
data PData a = PData { a1 :: Amount a
                     , a2 :: Amount a
                     } deriving Show

-- Helpers
usd :: Float -> Amount USD
usd = Amount

eur :: Float -> Amount EUR
eur = Amount

main = do

    print $ PData (usd 3) (usd 4)  -- OK
    print $ PData (eur 3) (eur 4)  -- OK
    print $ PData (eur 3) (usd 4)  -- KO, Couldn't match type 'USD with 'EUR

(1)https://wiki.haskell.org/Kind

另一方面,@ TheInnerLight记住我可以使用phantom types

-- NOTE: this is not a "direct translation" since currencies are not
--       enumerated and is slightly different
data USD = USD
data EUR = EUR

data Amount c = Amount { amount :: Float }

instance Show (Amount c) where
    show (Amount a) = show a

data PData c = PData { c1 :: Amount c
                     , c2 :: Amount c }
                       deriving Show

usd :: Float -> Amount USD
usd = Amount

eur :: Float -> Amount EUR
eur = Amount

main = do

    print $ PData (usd 3) (usd 4)  -- OK
    print $ PData (eur 3) (eur 4)  -- OK
    print $ PData (eur 3) (usd 4)  -- KO, Couldn't match type 'USD with 'EUR

提取货币符号(或任何其他数据)的一种方法可能是

class    Symbol c   where symbol :: c -> String
instance Symbol USD where symbol _ = "USD"
instance Symbol EUR where symbol _ = "EUR"

instance Symbol c => Show (Amount c) where
    show s@(Amount a) = sym undefined s ++ " " ++ show a
                        where sym :: Symbol c => c -> Amount c -> String
                              sym k _ = symbol k

印刷

PData {c1 = USD 3.0, c2 = USD 4.0}
PData {c1 = EUR 3.0, c2 = EUR 4.0}

答案 1 :(得分:3)

您无法直接比较F#中的DU子类型(另请参阅此答案:https://stackoverflow.com/a/30841893/3929902),但您可以使用这种迂回方式实现它:

type USD =
    Amount of decimal
type EUR =
    Amount of decimal

type Currency = 
    | USD of USD
    | EUR of EUR

type PositionalData<'T> =
  {
      grossAmount: 'T
      pos1: 'T
      pos2: 'T
  }        

let valid = {
    grossAmount = USD.Amount 10.0m;
    pos1 = USD.Amount 7.0m;
    pos2 = USD.Amount 3.0m;
}

let wrong = {
    grossAmount = USD.Amount 10.0m;
    pos1 = USD.Amount 7.0m;
    pos2 = EUR.Amount 3.0m;
}

这个答案的一个明显问题是,PositionalData不限于Currency,但可以是任何类型

答案 2 :(得分:3)

没有办法限制特定的联合案例,因为它们本身不是类型。这意味着特定值所属的联合案例在编译时不可用。

对于这个特殊情况,正如你在问题中指出的那样,我会使用度量单位而不是受歧视的联盟来解决F#中的问题。

为了更普遍地解决这个问题,通常最好将关系转换为这样的事情:

type PositionalData = {
    grossAmount: decimal;
    pos1: decimal;
    pos2: decimal;
}

type Currency =
    | USD of PositionalData
    | EUR of PositionalData

您可以使用类似以下类型的PositionalData使其适用:

type Currency<'a> =
    | USD of 'a
    | EUR of 'a

对于Haskell案例,我建议您查看this answer