如何将 DU/ADT 限制为某些大小写标识符/值构造函数



我将如何处理以下情况?
我有一个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(也许与高级类型相结合)如何解决这个问题会很有趣。

"直接"翻译可以是

{-# 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记住我,您可以使用幻影类型

-- 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}

你不能直接比较 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不局限于货币,但可以是任何类型的

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

对于这种特殊情况,正如您在问题中指出的那样,我会使用度量单位而不是受歧视的并集来解决 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的案例,我建议查看这个答案。

最新更新