【问题标题】:How to restrict a DU/ADT to certain case identifiers/value constructors如何将 DU/ADT 限制为某些案例标识符/值构造函数
【发布时间】:2016-07-15 07:50:36
【问题描述】:

我将如何处理以下情况?
我有一个 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(可能与更高种类的类型结合使用)如何解决这个问题会很有趣。

【问题讨论】:

  • 我想如果你在 F# 中使用一些 C#,利用 DU 的编译方式,你可以作弊。
  • @JohnPalmer 啊,不允许作弊和 C#。对不起。 ;-)

标签: haskell design-patterns f#


【解决方案1】:

“直接”翻译可以是

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

【讨论】:

  • 感谢@josejuan。你能解释一下从第 6 行开始的代码是做什么的(Amount 的类型构造函数)或者告诉我在哪里阅读它。
  • 请注意,第一个 data Amount 并不是真正的 GADT,而只是一个幻像类型,与第二个类似。这里没有使用 GADT,只使用了它们的语法。
【解决方案2】:

您无法直接比较 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 不受货币限制,但可以是任何类型

【讨论】:

  • 感谢@klasske。实际上,您的解决方案非常接近在 F# 中使用测量单位。如果你走那条路,可能应该简单地使用我认为的测量,因为你可以轻松地将参数限制为仅测量类型。
【解决方案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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-30
    • 1970-01-01
    • 1970-01-01
    • 2013-03-04
    相关资源
    最近更新 更多