关于haskell:强制类型类中的类约束未在实现类型的类型签名中捕获

Enforce class constraints in type class that is not captured in the type signature of implementing type

我试图使用一个类型类,它对它定义的函数之一返回的类型强制执行约束。但是函数的返回类型没有捕获其类型变量中的约束。我想知道代码有什么问题,或者正确的编码方法是什么。下面给出了一个示例代码:

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的类型只使用RenderableRenderable的类型签名中没有uniform类型的参数,所以编译器无法完全验证流。但是我想知道,为什么编译器在测试draw的类型签名时不能忽略这个问题,而主要取决于这样一个事实:它将知道实现Renderable的类型是否一定要为uniform提供一个值,作为State的一部分,并且它可以在实现站点验证类型的正确性。而不是使用。

PS:这是从OpenGL代码中提取的一个片段,uniformLibrary是OpenGL术语。


这是一种技巧。很多年前我就写过这个(在一个稍微不同的背景下,但想法是一样的),我仍然坚持它。

首先,框架。如果我们明确写出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


似乎您希望能够定义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(假设BoolString都是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,这意味着实现应该强制使用更具体的类型。请注意,现在您需要在您的实例中同时指定ab,因此:

1
2
instance Renderable Foo Bool where ...
instance Renderable Bar String where ...

我确信还有其他有效的方法来解决这个问题。