我不确定如何正确建模。考虑总和类型
data Choice
= A
| B
| C
和两种类型
data Foo = Foo
{ name : String
, choice : Choice
}
data Bar = Bar
{ name : String
, choice : Choice
}
因为它们是平等的。我想强制Foo
只能使用A
和B
作为Choice
,而Bar
只能使用B
和{{1} }}
目前我解决了这个分裂C
的问题,比如
Choice
但它看起来并不是一种可扩展的方法。是否有更好和/或更优雅的解决方案?
答案 0 :(得分:3)
可能的方法是使用GADT如下。但是,这不是一般性的,因为构造函数可以与单个类型(例如ForFoo,ForBar
)或任何类型(使用forAny
作为类型变量)关联。
data Purpose = ForFoo | ForBar
data Choice (p :: Purpose) where
A :: Choice 'ForFoo
B :: Choice forAny -- can be any Purpose
C :: Choice 'ForBar
data Foo where
Foo :: { nameF :: String , choiceF :: Choice 'ForFoo } -> Foo
data Bar where
Bar :: { nameB :: String , choiceB :: Choice 'ForBar } -> Bar
如果我们需要表示选择Foo,Bar
但不是Baz
,则无法使用此方法。
更通用的方法可能是:
data Choice2 (a::Bool) (b::Bool) (c::Bool) where
A2 :: Choice2 'True 'False 'False
B2 :: Choice2 'False 'True 'False
C2 :: Choice2 'False 'False 'True
data Foo2 where
Foo2 :: { nameF2 :: String
, choiceF2 :: Choice2 b1 b2 'False } -> Foo2
data Bar2 where
Bar2 :: { nameB2 :: String
, choiceB2 :: Choice2 'False b1 b2 } -> Bar2
data Baz2 where
Baz2 :: { nameZ2 :: String
, choiceZ2 :: Choice2 b1 'False b2 } -> Baz2
原则上,这允许人们表达任何“子集”要求。在我们不想要的情况下,我们只需要False
,并在其他情况下使用(存在量化的)变量。
它仍然不完全一般,并且有其缺点。例如,我们应该能够为
编写一个恒定时间的实现coerce :: [Choice2 'True b1 b2] -> [Choice2 'True 'False 'False]
但我们不能,因为我们没有真正的亚型。
答案 1 :(得分:1)
如何使用通用和类型Either
:
data A = A
data B = B
data C = C
type (+) = Either
data Foo = Foo
{ name :: String
, choice :: A + B
}
data Bar = Bar
{ name :: String
, choice = B + C
}
模块化很好,但在构造和模式匹配值时,必须记住使用Left
和Right
的正确组合并不是很方便。此外,添加案例需要更改以前存在的模式。
然而,这种技术的变体可以解决这个问题。
基本上,使用类型类来选择/注入一个变量。例如,参见乙烯基中的Data.Vinyl.CoRec
,其中match
函数可以对变体进行模式匹配,而无需担心顺序或嵌套。在评论中,我有一个与prism和total库类似的代码片段。
另一种似乎更接近你最初想法的方法是声明一个和类型,但是类型参数可以选择性地禁止某些构造函数。
chi已经举了一个使用GADT的例子。这是一个有常规ADT的人。
data Choice a b c
= A a
| B b
| C c
如果没有A
选项,只需设置a ~ Void
(来自Data.Void
),这是一个空类型。
data Foo = Foo
{ name :: String
, choice :: Choice () () Void
-- Two inhabitants: (A ()), (B ())
}
我们仍然必须在C
上进行模式匹配以避免出现警告,但使用-XEmptyCase
或Data.Void.absurd
系统地调度案例。
case c :: Choice () () Void of
A () -> 1
B () -> 2
C v -> case v of {}
-- or absurd v
这种技术也允许一些子类型。
A () :: forall b c. Choice () b c
:: Choice () () ()
:: Choice () Void Void
:: Choice () () Void
缺点是支持新的(可选)选择意味着更改类型参数的数量,这很麻烦。