将字典变成约束
Turning a Dict into a constraint
我有一个 class Cyc c r
,它具有 c m r
形式的数据函数,其中 m
是幻像类型。例如,
class Cyc c r where
cyc :: (Foo m, Foo m') => c m r -> c m' r
我有充分的理由不将 m
设为 class 参数。出于本示例的目的,主要原因是它减少了对函数的约束数量。在我的实际示例中,对这个接口的一个更迫切的需求是我使用更改和隐藏的幻像类型,所以这个接口让我可以为任何幻像类型获得 Cyc
约束。
该选择的一个缺点是我无法使 Num (c m r)
成为 Cyc
的超级class 约束。我的意图是,只要 (Cyc c r, Foo m)
,c m r
就应该是 Num
。当前的解决方案非常烦人:我将方法添加到 class Cyc
witNum :: (Foo m) => c m r -> Dict (Num (c m r))
哪一种完成同样的事情。现在,当我有一个采用通用 Cyc
并需要 Num (c m r)
约束的函数时,我可以写:
foo :: (Cyc c r, Foo m) => c m r -> c m r
foo c = case witNum c of
Dict -> c*2
当然,我 可以 添加 Num (c m r)
约束到 foo
,但我正在尝试减少约束的数量,还记得吗? (Cyc c r, Foo m)
应该暗示 Num (c m r)
约束(我需要 Cyc c r
和 Foo m
用于其他目的),所以我不想写出 Num
约束也。
在写这个问题的过程中,我发现了一个更好的(?)方法来完成这个,但是它有它自己的缺点。
模块 Foo:
{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, ScopedTypeVariables #-}
module Foo where
import Data.Constraint
class Foo m
class Cyc c r where
cyc :: (Foo m, Foo m') => c m r -> c m' r
witNum :: (Foo m) => c m r -> Dict (Num (c m r))
instance (Foo m, Cyc c r) => Num (c m r) where
a * b = case witNum a of
Dict -> a * b
fromInteger a = case witNum (undefined :: c m r) of
Dict -> fromInteger a
-- no Num constraint and no Dict, best of both worlds
foo :: (Foo m, Cyc c r) => c m r -> c m r
foo = (*2)
模块栏:
{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, OverlappingInstances #-}
module Bar where
import Foo
import Data.Constraint
data Bar m r = Bar r deriving (Show)
instance (Num r) => Cyc Bar r where
witNum _ = Dict
instance (Num r, Foo m) => Num (Bar m r) where
(Bar a) * (Bar b) = Bar $ a*b
fromInteger = Bar . fromInteger
instance Foo ()
bar :: Bar () Int
bar = foo 3
虽然这种方法可以满足我的所有需求,但它似乎很脆弱。我主要担心的是:
- 我对模块
Foo
中 Num
的通用实例头持谨慎态度。
- 如果将任何重叠实例导入
Foo
,我突然需要 IncoherentInstances
或 foo
上的 Num
约束以将实例选择推迟到运行时。
是否有其他方法可以避免在每个需要 Num (c m r)
的函数中使用 Dict
,从而避免这些缺点?
经过 6 个月的思考,我终于对上面悬而未决的评论有了答案:添加一个 newtype
包装器!
我将 Cyc
class 一分为二:
class Foo m
class Cyc c where
cyc :: (Foo m, Foo m') => c m r -> c m' r
class EntailCyc c where
entailCyc :: Tagged (c m r) ((Foo m, Num r) :- (Num (c m r)))
然后我定义我的 Cyc
实例如上:
data Bar m r = ...
instance Cyc Bar where ...
instance (Num r, Foo m) => Num (Bar m r) where ...
instance EntailCyc Bar where
witNum _ = Dict
然后我定义一个新类型包装器并为其提供一个通用 Cyc
实例:
newtype W c m r = W (c m r)
instance Cyc (W c m r) where cyc (W a) = W $ cyc a
instance (EntailCyc c, Foo m, Num r) => Num (W c m r) where
(W a) + (W b) = a + b \ witness entailCyc a
最后,我将所有使用通用 c m r
类型的函数更改为使用 W c m r
类型:
foo :: (Cyc c, EntailCyc c, Foo m, Num r) => W c m r -> W c m r
foo = (*2)
这里的要点是 foo
可能需要 许多 约束(例如,Eq (W c m r)
、Show (W c m r)
等)需要自己的约束。但是,Eq
、Show
等的 W c m r
的通用实例都有 完全 约束 (EntailCyc c, Foo m, Eq/Show/... a)
,因此约束foo
以上是我需要写的只有个约束!
我有一个 class Cyc c r
,它具有 c m r
形式的数据函数,其中 m
是幻像类型。例如,
class Cyc c r where
cyc :: (Foo m, Foo m') => c m r -> c m' r
我有充分的理由不将 m
设为 class 参数。出于本示例的目的,主要原因是它减少了对函数的约束数量。在我的实际示例中,对这个接口的一个更迫切的需求是我使用更改和隐藏的幻像类型,所以这个接口让我可以为任何幻像类型获得 Cyc
约束。
该选择的一个缺点是我无法使 Num (c m r)
成为 Cyc
的超级class 约束。我的意图是,只要 (Cyc c r, Foo m)
,c m r
就应该是 Num
。当前的解决方案非常烦人:我将方法添加到 class Cyc
witNum :: (Foo m) => c m r -> Dict (Num (c m r))
哪一种完成同样的事情。现在,当我有一个采用通用 Cyc
并需要 Num (c m r)
约束的函数时,我可以写:
foo :: (Cyc c r, Foo m) => c m r -> c m r
foo c = case witNum c of
Dict -> c*2
当然,我 可以 添加 Num (c m r)
约束到 foo
,但我正在尝试减少约束的数量,还记得吗? (Cyc c r, Foo m)
应该暗示 Num (c m r)
约束(我需要 Cyc c r
和 Foo m
用于其他目的),所以我不想写出 Num
约束也。
在写这个问题的过程中,我发现了一个更好的(?)方法来完成这个,但是它有它自己的缺点。
模块 Foo:
{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, ScopedTypeVariables #-}
module Foo where
import Data.Constraint
class Foo m
class Cyc c r where
cyc :: (Foo m, Foo m') => c m r -> c m' r
witNum :: (Foo m) => c m r -> Dict (Num (c m r))
instance (Foo m, Cyc c r) => Num (c m r) where
a * b = case witNum a of
Dict -> a * b
fromInteger a = case witNum (undefined :: c m r) of
Dict -> fromInteger a
-- no Num constraint and no Dict, best of both worlds
foo :: (Foo m, Cyc c r) => c m r -> c m r
foo = (*2)
模块栏:
{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, OverlappingInstances #-}
module Bar where
import Foo
import Data.Constraint
data Bar m r = Bar r deriving (Show)
instance (Num r) => Cyc Bar r where
witNum _ = Dict
instance (Num r, Foo m) => Num (Bar m r) where
(Bar a) * (Bar b) = Bar $ a*b
fromInteger = Bar . fromInteger
instance Foo ()
bar :: Bar () Int
bar = foo 3
虽然这种方法可以满足我的所有需求,但它似乎很脆弱。我主要担心的是:
- 我对模块
Foo
中Num
的通用实例头持谨慎态度。 - 如果将任何重叠实例导入
Foo
,我突然需要IncoherentInstances
或foo
上的Num
约束以将实例选择推迟到运行时。
是否有其他方法可以避免在每个需要 Num (c m r)
的函数中使用 Dict
,从而避免这些缺点?
经过 6 个月的思考,我终于对上面悬而未决的评论有了答案:添加一个 newtype
包装器!
我将 Cyc
class 一分为二:
class Foo m
class Cyc c where
cyc :: (Foo m, Foo m') => c m r -> c m' r
class EntailCyc c where
entailCyc :: Tagged (c m r) ((Foo m, Num r) :- (Num (c m r)))
然后我定义我的 Cyc
实例如上:
data Bar m r = ...
instance Cyc Bar where ...
instance (Num r, Foo m) => Num (Bar m r) where ...
instance EntailCyc Bar where
witNum _ = Dict
然后我定义一个新类型包装器并为其提供一个通用 Cyc
实例:
newtype W c m r = W (c m r)
instance Cyc (W c m r) where cyc (W a) = W $ cyc a
instance (EntailCyc c, Foo m, Num r) => Num (W c m r) where
(W a) + (W b) = a + b \ witness entailCyc a
最后,我将所有使用通用 c m r
类型的函数更改为使用 W c m r
类型:
foo :: (Cyc c, EntailCyc c, Foo m, Num r) => W c m r -> W c m r
foo = (*2)
这里的要点是 foo
可能需要 许多 约束(例如,Eq (W c m r)
、Show (W c m r)
等)需要自己的约束。但是,Eq
、Show
等的 W c m r
的通用实例都有 完全 约束 (EntailCyc c, Foo m, Eq/Show/... a)
,因此约束foo
以上是我需要写的只有个约束!