使用功能依赖的关联参数限制

Posted

技术标签:

【中文标题】使用功能依赖的关联参数限制【英文标题】:Associated Parameter Restriction using Functional Dependency 【发布时间】:2012-09-06 03:49:27 【问题描述】:

下面的函数 f,对于给定的类型“a”,接受类型为“c”的参数。对于不同类型的“a”,“c”以不同的方式进行限制。具体来说,当“a”是任何 Integral 类型时,应该允许“c”是任何“Real”类型。当 'a' 是 Fl​​oat 时,'c' 只能是 Float。

一种尝试是:

-# LANGUAGE
MultiParamTypeClasses,
FlexibleInstances,
FunctionalDependencies,
UndecidableInstances #-

class AllowedParamType a c | a -> c

class Foo a where
    f :: (AllowedParamType a c) => c -> a

fIntegral :: (Integral a, Real c) => c -> a
fIntegral = error "implementation elided"

instance (Integral i, AllowedParamType i d, Real d) => Foo i where
    f = fIntegral

出于某种原因,GHC 7.4.1 抱怨它“无法推断出使用 fIntegral 产生的 (Real c)”。在我看来,功能依赖应该允许这种推论。在实例中,a 与 i 统一,因此通过功能依赖,d 应该与 c 统一,在实例中声明为“Real”。我在这里错过了什么?

抛开功能依赖不谈,这种方法的表达能力是否足以强制执行上述限制,还是有更好的方法?我们只为 'a' 使用了几个不同的值,所以会有这样的例子:

instance (Integral i, Real c) => AllowedParamType i c
instance AllowedParamType Float Float

谢谢

【问题讨论】:

当你说class AllowedParamType a c | a -> c时,你说给定任何类型a作为第一个参数,最多有一个类型c可以用作第二个参数。但是你说当第一个类型是Integral 类型时,any Real 类型可以用作第二个参数。理想情况下,GHC 会给您一条指出这一点的错误消息。 这不是真的。这只是意味着 a 唯一确定 b 因此允许上述类型的实例。 Satvik 是对的,fundep 的独特性不应该是导致代码无法编译的原因。但是,dave4420 也有正确的想法:从长远来看,fundep 是行不通的,因为我不想将整个程序的 Int 参数类型限制为一种 Real。 @dave4420:但实例不是这么说的。它实际上说“当第一种类型是任何类型时,第二种类型也是任何类型,哦顺便确保他们有Integral和@分别为 987654329@ 个实例”。选择实例时会忽略 Integral 约束。 'r' 并不总是一个 Real 类型,它还可以包含我们声明的一些特定的 Data 值(都将是 Num),尽管 f 的具体实现取决于具体的 Num类型。为了清楚 'a' 和 'c' 之间的关系,我想为 (Num a) 做一些 Foo 的实例。对于每个实例,c 有一个(唯一)可能的类型(如 Float),或整个类型类(如 Real)。 'c' 不必与 'a' 相关,在某些情况下它们可以是“独立”类型(对于我们的用户定义类型)。 【参考方案1】:

好吧,这个一直在唠叨我。鉴于各种各样的实例, 让我们全力以赴,摆脱两者之间的任何关系 实例的存在以外的源和目标类型:

-# LANGUAGE OverlappingInstances, FlexibleInstances,TypeSynonymInstances,MultiParamTypeClasses #-

class Foo a b where f :: a -> b

现在我们可以根据自己的喜好将一对类型与它们之间的f 匹配,例如:

instance Foo Int Int where f = (+1)
instance Foo Int Integer where f = toInteger.((7::Int) -)
instance Foo Integer Int where f = fromInteger.(^ (2::Integer))
instance Foo Integer Integer where f = (*100)
instance Foo Char Char where f = id
instance Foo Char String where f = (:[])  -- requires TypeSynonymInstances
instance (Foo a b,Functor f) => Foo (f a) (f b) where f = fmap f -- requires FlexibleInstances
instance Foo Float Int where f = round
instance Foo Integer Char where f n = head $ show n

这确实意味着要使用大量显式类型注释来避免No instance for...Ambiguous type 错误消息。 比如你不能做main = print (f 6),但你可以做main = print (f (6::Int)::Int)

您可以列出 所有 具有所需标准类型的实例, 这可能会导致大量重复,我们可以点亮蓝色触摸纸并执行以下操作:

instance Integral i => Foo Double i where f = round -- requires FlexibleInstances
instance Real r => Foo Integer r where f = fromInteger -- requires FlexibleInstances

注意:这不是的意思是“嘿,如果你有一个整数类型i, 你可以使用这个方便的圆形函数免费拥有一个实例Foo Double i", 这意味着:“每次你有任何类型i,它绝对是一个实例 Foo Double i。顺便说一句,我为此使用round,所以除非你的类型iIntegral, 我们会闹翻的。”例如,对于 Foo Integer Char 实例来说,这是一个大问题。

这很容易破坏你的其他实例,所以如果你现在输入 f (5::Integer) :: Integer 你会得到

Overlapping instances for Foo Integer Integer
  arising from a use of `f'
Matching instances:
  instance Foo Integer Integer
  instance Real r => Foo Integer r

您可以更改您的编译指示以包含 OverlappingInstances:

-# LANGUAGE OverlappingInstances, FlexibleInstances,TypeSynonymInstances,MultiParamTypeClasses #-

所以现在f (5::Integer) :: Integer 返回 500,很明显它使用了更具体的 Foo Integer Integer 实例。

我认为这种方法可能对您有用,手动定义许多实例,仔细考虑何时完全疯狂 从标准类型类中创建实例。 (或者,标准类型并不多,众所周知,notMany choose 2 = notIntractablyMany,因此您可以将它们全部列出。)

【讨论】:

【参考方案2】:

这里有一个解决更普遍问题的建议,而不是您的具体问题(我首先需要更多细节 - 我保证稍后再检查)。我正在写它以防其他人正在寻找与您类似的问题的解决方案,我当然在过去,在我发现 SO 之前。当 SO 可以帮助您尝试一种全新的方法时,它尤其出色。

我以前有工作习惯:

    引入一个多参数类型类(类型到处都是,所以...) 引入函数依赖(应该整理一下,但我最终需要...) 添加 FlexibleInstances(警钟开始响起。编译器默认关闭此功能是有原因的...) 添加 UndecidableInstances(GHC 告诉您您只能靠自己了,因为它不相信它可以应对您设置的挑战。) 一切都炸了。以某种方式重构。

然后我发现了type families 的乐趣(类型的函数式编程(万岁)——多参数类型类(有点像)类型的逻辑编程)。我的工作流程更改为:

    引入包含关联类型的类型类,即替换

    class MyProblematicClass a b | a -> b where
      thing :: a -> b
      thang :: b -> a -> b
    

    class MyJustWorksClass a where
      type Thing a :: * -- Thing a is a type (*), not a type constructor (* -> *)
      thing :: a -> Thing a
      thang :: Thing a -> a -> Thing a
    

    紧张地添加 FlexibleInstances。一点问题都没有。

    有时通过使用 (MyJustWorksClass j,j~a)=> 而不是 (MyJustWorksClass a)=>(Show t,t ~ Thing a,...)=> 而不是 (Show (Thing a),...) => 等约束来解决问题,以帮助 ghc。 (~ 本质上的意思是“与类型相同”) 紧张地添加 FlexibleContexts。完全没有问题。 一切正常。

“没有任何问题”的原因是 ghc 计算类型 Thing a 使用我的类型函数 Thang 而不是尝试推断它使用只是一堆断言那里有一个函数,它应该能够解决它。

试一试!阅读Fun with Type Functions之前阅读manual!

【讨论】:

我为我的解决方案尝试了一个新类型类的原因是因为我知道关联类型不允许我表达约束是一个类而不是具体类型(因为关联类型必须是具体的)。相反,我使用了fundep,但这与关联的类型同义词存在相同的问题。 @Eric:问题是没有函数依赖?您在 cmets 中的回答表明不是。您描述的源类型和目标类型之间的关系似乎是多对多的。【参考方案3】:

一种可能更好的方法是使用约束类型和类型族(我认为 GHC 扩展,需要 GHC 7.4)。这允许您将约束指定为类实例的一部分。

-# LANGUAGE ConstraintKinds, TypeFamilies, FlexibleInstances, UndecidableInstances #-

import GHC.Exts (Constraint)

class Foo a where
   type ParamConstraint a b :: Constraint
   f :: ParamConstraint a b => b -> a

instance Integral i => Foo i where
   type ParamConstraint i b = Real b
   f = fIntegral

编辑:经过进一步的实验,有一些细微之处意味着这不能按预期工作,特别是type ParamConstraint i b = Real b 太笼统了。我现在不知道解决方案(或者是否存在)。

【讨论】:

我想知道约束类型是否可以解决问题。这些微妙之处是什么? 好吧,只要你不添加任何其他实例,这个解决方案就会很好地工作......

以上是关于使用功能依赖的关联参数限制的主要内容,如果未能解决你的问题,请参考以下文章

限制 npm 依赖模块对 require(隔离)的使用

jmeter的参数关联(参数依赖)

合并 PR 后如何限制 Azure DevOps YAML 管道中的关联工作项?

Sequelize hasOne 关联限制

在select查询中解决SQLite参数限制

续集限制包括关联