根据返回类型选择类型类

时间:2021-11-09 17:03:17

I want to be able to have a function, which implementation will choose a typeclass based on the manual type specification of it's return type.

我希望能够有一个函数,该实现将根据它的返回类型的手动类型规范选择类型类。

Here's a contrived example: a typeclass and two instances:

这是一个人为的例子:类型类和两个实例:

class ToString a where
  toString :: a -> String

instance ToString Double where
  toString = const "double"

instance ToString Int where
  toString = const "int"

I'm able to choose the instance by calling the toString with Int type:

我可以通过使用Int类型调用toString来选择实例:

function :: String
function = toString (undefined :: Int)

So far so good. What I'd like to do is to be able to write the function, so it will work for any a if there exists a typeclass for it:

到现在为止还挺好。我想要做的是能够编写函数,因此如果存在类型类,它将适用于任何a:

function' :: (ToString a) => String
function' = toString (undefined :: a)

Here, the function' doesn't compile, because the a type parameter is not mentioned anywhere in the signature and it's not possible to specify the typeclass upon calling.

这里,函数'不编译,因为签名中的任何地方都没有提到类型参数,并且在调用时无法指定类型类。

So it looks like the only option is to pass the type information about the type of a into the return type:

所以看起来唯一的选择是将关于a类型的类型信息传递给返回类型:

data Wrapper a = Wrapper {
  field :: String }

function'' :: (ToString a) => Wrapper a
function'' = Wrapper $ toString (undefined :: a)

showToString :: String
showToString = field (function'' :: Wrapper Int)

I define a Wrapper type just for carrying the type information. In the showToString, my hope is that since I specify the exact type of Wrapper, then the typechecker can infer that the a in function'' is and Int and pick the Int instance of the ToString typeclass.

我定义了一个Wrapper类型只是为了携带类型信息。在showToString中,我希望因为我指定了Wrapper的确切类型,所以typechecker可以推断函数''中的a是和Int并选择ToString类型类的Int实例。

But the reality doesn't correspond with my hopes, this is the message from compiler

但现实并不符合我的希望,这是来自编译器的信息

Could not deduce (ToString a0) arising from a use of `toString'

无法推断(ToString a0)因使用`toString'而产生

Is there a way, how to convince the compiler, that he can pick the right typeclass in the function'', because I specify the it by having the type declaration of :: Wrapper Int?

有没有办法,如何说服编译器,他可以在函数''中选择正确的类型类,因为我通过具有:: Wrapper Int的类型声明来指定它?

2 个解决方案

#1


First, let me suggest that instead of an own Wrapper type, you use Data.Tagged.Tagged, whose purpose is exactly this kind of stuff.

首先,让我建议您使用Data.Tagged.Tagged代替自己的Wrapper类型,其目的正是这种东西。

Apart from that, you need to turn on the -XScopedTypeVariables extension, otherwise the a type variable only exists in the type signature itself, but not in signatures of local bindings.

除此之外,您需要打开-XScopedTypeVariables扩展,否则类型变量仅存在于类型签名本身中,但不存在于本地绑定的签名中。

{-# LANGUAGE ScopedTypeVariables #-}

import Data.Tagged

function''' :: forall a. ToString a => Tagged a String
function''' = Tagged $ toString (undefined :: a)

The explicit forall is necessary for a to actually become a scoped variable, otherwise the extension doesn't kick in.

显式forall是实际成为范围变量的必要条件,否则扩展不会启动。

However....

Actually, the best thing would probably to have the class method produce a tagged value in the first place:

实际上,最好的方法可能是让class方法首先产生一个标记值:

class NamedType a where
  typeName :: Tagged a String

instance NamedType Double where
  typeName = Tagged "double"
instance NamedType Int where
  typeName = Tagged "int"
...

Or don't write your own class at all but use typeable:

或者根本不写自己的类,而是使用typeable:

import Data.Typeable

typeName' :: Typeable a => Tagged a String
typeName' = fmap show $ unproxy typeRep

Of course, that will give you the actual uppercase type names, and it might work for types you don't actually want it to.

当然,这将为您提供实际的大写类型名称,它可能适用于您实际不想要它的类型。

#2


leftaroundabout's answer is probably the one you want. But for completeness, here's one more thing you can do:

leftaroundabout的答案可能就是你想要的答案。但为了完整起见,您还可以做以下事情:

unwrap :: Wrapper a -> a
unwrap = error "can't actually unwrap"

function'' :: (ToString a) => Wrapper a
function'' = x
  where
    x = Wrapper (toString (unwrap x))

The idea is that I want an a to pass to toString but only Wrapper a shows up in my type. So I just define a function which takes Wrapper a and produces a – such a function can't have a real implementation, but we're not using it for its return value anyway – and apply it to the Wrapper a thing.

我的想法是,我希望a传递给toString,但只有Wrapper a显示在我的类型中。所以我只是定义一个函数,它接受Wrapper a并生成一个 - 这样的函数不能有真正的实现,但我们还没有将它用作返回值 - 并将它应用于Wrapper。

There's a bit of additional awkwardness because Wrapper a shows up in the result instead of an argument, but this (slightly silly) recursion takes care of that.

还有一些额外的尴尬,因为Wrapper a出现在结果中而不是参数中,但是这个(稍微愚蠢的)递归会解决这个问题。

#1


First, let me suggest that instead of an own Wrapper type, you use Data.Tagged.Tagged, whose purpose is exactly this kind of stuff.

首先,让我建议您使用Data.Tagged.Tagged代替自己的Wrapper类型,其目的正是这种东西。

Apart from that, you need to turn on the -XScopedTypeVariables extension, otherwise the a type variable only exists in the type signature itself, but not in signatures of local bindings.

除此之外,您需要打开-XScopedTypeVariables扩展,否则类型变量仅存在于类型签名本身中,但不存在于本地绑定的签名中。

{-# LANGUAGE ScopedTypeVariables #-}

import Data.Tagged

function''' :: forall a. ToString a => Tagged a String
function''' = Tagged $ toString (undefined :: a)

The explicit forall is necessary for a to actually become a scoped variable, otherwise the extension doesn't kick in.

显式forall是实际成为范围变量的必要条件,否则扩展不会启动。

However....

Actually, the best thing would probably to have the class method produce a tagged value in the first place:

实际上,最好的方法可能是让class方法首先产生一个标记值:

class NamedType a where
  typeName :: Tagged a String

instance NamedType Double where
  typeName = Tagged "double"
instance NamedType Int where
  typeName = Tagged "int"
...

Or don't write your own class at all but use typeable:

或者根本不写自己的类,而是使用typeable:

import Data.Typeable

typeName' :: Typeable a => Tagged a String
typeName' = fmap show $ unproxy typeRep

Of course, that will give you the actual uppercase type names, and it might work for types you don't actually want it to.

当然,这将为您提供实际的大写类型名称,它可能适用于您实际不想要它的类型。

#2


leftaroundabout's answer is probably the one you want. But for completeness, here's one more thing you can do:

leftaroundabout的答案可能就是你想要的答案。但为了完整起见,您还可以做以下事情:

unwrap :: Wrapper a -> a
unwrap = error "can't actually unwrap"

function'' :: (ToString a) => Wrapper a
function'' = x
  where
    x = Wrapper (toString (unwrap x))

The idea is that I want an a to pass to toString but only Wrapper a shows up in my type. So I just define a function which takes Wrapper a and produces a – such a function can't have a real implementation, but we're not using it for its return value anyway – and apply it to the Wrapper a thing.

我的想法是,我希望a传递给toString,但只有Wrapper a显示在我的类型中。所以我只是定义一个函数,它接受Wrapper a并生成一个 - 这样的函数不能有真正的实现,但我们还没有将它用作返回值 - 并将它应用于Wrapper。

There's a bit of additional awkwardness because Wrapper a shows up in the result instead of an argument, but this (slightly silly) recursion takes care of that.

还有一些额外的尴尬,因为Wrapper a出现在结果中而不是参数中,但是这个(稍微愚蠢的)递归会解决这个问题。