我试图使用一个类型类,它对它定义的函数之一返回的类型强制执行约束。但是函数的返回类型没有捕获其类型变量中的约束。我想知道代码有什么问题,或者正确的编码方法是什么。下面给出了一个示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| data State a = State {
uniform :: a
}
class Renderable a where
render :: (Uniform b ) => Int -> a -> State b
library :: (Uniform a ) => a -> IO ()
-- some implementation
draw :: (Renderable a ) => a -> IO ()
draw renderable = do
let state = render 0 renderable
_ <- library (uniform state ) |
在上面的代码片段中,render函数试图强制状态中的uniform属性遵循类约束统一。当我运行代码时,我得到一个错误
1 2 3 4 5 6
| Could not deduce (Uniform a5 ) arising from a use of ‘draw’
from the context : (Renderable r, Uniform a )
bound by the type signature for :
draw :: forall r a.
(Renderable r, Uniform a ) =>
Int -> Renderable r -> IO () |
号
考虑到这一点,我有点理解,由于draw的类型只使用Renderable和Renderable的类型签名中没有uniform类型的参数,所以编译器无法完全验证流。但是我想知道,为什么编译器在测试draw的类型签名时不能忽略这个问题,而主要取决于这样一个事实:它将知道实现Renderable的类型是否一定要为uniform提供一个值,作为State的一部分,并且它可以在实现站点验证类型的正确性。而不是使用。
PS:这是从OpenGL代码中提取的一个片段,uniform,Library是OpenGL术语。
- 这是一个很常见的错误。你给(Uniform b) => Int -> a -> State b的签名意味着它必须对所有b有效。因此,如果一些调用程序请求State Elephant,函数必须知道如何返回它。我认为这里的正确设计是从render返回一个单态(无类型变量)数据类型——尽管很难给出建议,因为我不能真正告诉您打算如何处理渲染结果。
- @Luqui我知道你说的问题。但正如我所说,编译器仍然可以验证在返回大象的执行位置,Elephant是否坚持Uniform,不是吗?对于您的第二个问题,正如我在示例中所给出的,我的意图是将遵循&;niform'的类型传递给一个函数:library :: (Uniform a) => a -> IO ()。
- 在我看来,您只需要指定调用Uniform的结果应该是什么类型。当前,该值的生产者和使用者都是多态的,因此编译器不知道要使用什么实例。
这是一种技巧。很多年前我就写过这个(在一个稍微不同的背景下,但想法是一样的),我仍然坚持它。
首先,框架。如果我们明确写出render的签名,我们有:
1
| render :: forall b. Uniform b => Int -> a -> State b |
也就是说,render的调用者选择b类型。在我看来,你的意图更像这个伪哈斯克尔*:
1
| render :: exists b. (Uniform b ) & Int -> a -> State b |
号
被叫方可以在其中选择类型。也就是说,render的不同实现可以选择不同类型的b返回,只要它们是一致的。
这可能是一个很好的表达方式,除了哈斯克尔不支持直接存在量化。可以创建包装数据类型来模拟它
1 2
| data SomeUniform where
SomeUniform :: Uniform a => a -> SomeUniform |
签名
1
| render :: Int -> a -> SomeUniform |
。
我想它有你要找的属性。但是,SomeUniform类型和Uniform类型类很可能是多余的。您在评论中说,Uniform类型类如下:
1 2
| class Uniform a where
library :: a -> IO () |
我们来考虑这个问题:假设我们有一个SomeUniform,也就是说,我们有一个a类型的值,除了它是Uniform类型类的一个实例外,我们什么都不知道。我们可以用x做什么?只有一种方法可以从x中获取任何信息,即调用library。因此,本质上,SomeUniform类型所做的唯一一件事就是在后面调用library方法。整个存在主义/类型类是没有意义的,我们最好将其分解为一个简单的数据类型:
1
| data Uniform = Uniform { library :: IO () } |
。
你的render方法变成:
1
| render :: Int -> a -> Uniform |
。
它是如此美丽的年轻,不是吗?如果Uniformtypeclass中有更多的方法,它们将成为此数据类型的附加字段(其类型可能是函数,需要一些习惯)。其中有类型和类型类实例,例如
1 2 3 4 5
| data Thingy = Thingy String
-- note the constructor type Thingy :: String -> Thingy
instance Uniform String where
library (Thingy s ) = putStrLn $"thingy" ++ s |
现在您还可以摆脱数据类型,只需使用一个函数来代替构造函数。
1 2
| thingy :: String -> Uniform
thingy s = Uniform { library = putStrLn $"thingy" ++ s } |
。
(如果由于其他原因无法去掉数据类型,可以提供转换函数uniformThingy :: Thingy -> Uniform)
这里的原则是,你可以用它的观察结果来代替一个存在的类型,如果你这样做的话,它通常是很好的。
*我的伪haskell &与=>是双重的,在本质上起着相同的作用,但存在于量化词典中。c => t表示调用者提供字典c后,返回类型t,而c & t表示被调用者同时提供字典c和类型t。
- 根据op所说的,我觉得Uniform、library等是从现有的OpenGL模块导入的,所以我怀疑他是否可以这样修改它们。也就是说,我认为这是一个很好的方法。实际上,几个月前我读了你的博客,它确实影响了我设计这种界面的方法,所以干杯。^^
- 啊,我不明白。在这种情况下,SomeUniform是一个相当不错的中间地带。
- 谢谢@luqui的解释。将类型类转换为数据类型似乎是一种有趣的方法。对我来说似乎更灵活
似乎您希望能够定义render,为Renderable的每个实现返回不同的不同类型,只要该类型是Uniform:
1 2 3 4 5 6 7 8
| instance Renderable Foo where
render _ _ = State True
instance Renderable Bar where
render _ _ = State"mothman"
instance Renderable Baz where
render _ _ = State 19 |
因此,如果用Foo调用render,它将返回State Bool,但如果用Bar调用,它将返回State String(假设Bool和String都是Uniform)。这不是它的工作方式,如果您尝试这样实例化,您将得到一个类型不匹配错误。
render :: (Uniform b) => Int -> a -> State b表示返回Uniform b => State b。如果这就是您的类型签名,那么您的实现不能或多或少地是特定的;您的实现必须能够返回任何类型的值Uniform b => State b。如果不能这样做,任何请求特定类型的返回值的代码都将无法得到正确的类型,并且事情将以类型系统应该能够防止的方式中断。
让我们看一个不同的例子:
1 2
| class Collection t where
size :: Num i => t a -> i |
号
假设有人想运行这个size函数,得到一个Double的结果。它们可以这样做,因为size的任何实现都必须能够返回任何类型的类Num,因此调用方始终可以指定他们想要的类型。如果允许您编写一个始终返回Integer的实现,这将不再可能。
我认为要做你想做的事情,你需要像FunctionalDependencies这样的东西。有了这个,你的课程可以是:
1 2
| class Uniform b => Renderable a b | a -> b where
render :: Int -> a -> State b |
"| a -> b告诉类型检查器,b类型应根据调用方提供的a类型来决定。这不允许调用者选择自己的b,这意味着实现应该强制使用更具体的类型。请注意,现在您需要在您的实例中同时指定a和b,因此:
1 2
| instance Renderable Foo Bool where ...
instance Renderable Bar String where ... |
。
我确信还有其他有效的方法来解决这个问题。
- 谢谢你的解释。当我理解你的答案时,是否认为"呼叫者总是可以指定他们想要的类型"?typeclass提供的保证是结果将遵循约束Uniform。就这些。所以在Collection示例中,实现总是返回整数,这难道不是完全可以吗?因为它仍然坚持一个数字。我无法理解你的解释背后的完整推理。也许我错过了什么。你能解释更多吗?
- @是啊,你不正确。这只是签名中类型变量的含义,调用方选择。当我调用read :: (Read a) => String -> a时,我决定要读取的是什么类型——我可以读取String或[Int]或其他类型,并且read必须准备好将字符串转换为该类型。因此,在实现render时,只要所请求的类型满足约束,就必须准备好提供所请求的任何类型。我知道,它和OO语言感觉非常不同。
- @Aravindhs在类型签名的约束内选择类型是调用者的工作,因此被调用者需要能够处理可能适合签名的所有类型。类型检查器并没有真正区分"参数类型"和"返回类型",它对它们的处理是相同的。一个Collection实例总是返回一个Integer并不比fromIntegral :: (Integral a, Num b) => a -> b只接受一个Int64更罚款。
- 谢谢。我现在明白了。我认为字体族也可以用来表达这种关系。这是正确的吗?
- @阿拉文德斯,我认为那是另一回事。类型族允许您定义一种特殊类型的参数化同义词,根据其类型参数解析为完全不同的类型。因此,如果您定义一个类型族Foo a,您可以将Foo Char实例化为String的类型同义词,而Foo Bool表示Int,无论您想要什么。当您在类内定义一个类型族时,这特别有用,然后类的每个实例都可以用它自己的特殊类型实例化该族。