我正在尝试理解Haskell中的类型类。
我使用一个简单的例子提出问题:
让我们考虑两种数据类型,A
和B
。
让我们假设a1
和a2
类型为A
,b1
和b2
类型为B
。
现在让我们假设我们要定义函数来检查a1
是否等于a2
以及b1
是否与b2
相同。
为此我们有两个选择:
选项1:
使用不同的名称定义两个相等函数,如下所示:
eqA :: A -> A -> Bool
eqA = ...
eqB :: B -> B -> Bool
eqB = ...
现在,如果我们要检查a1
是否等于a2
,那么我们会写eqA a1 a2
,类似于B
:eqB b1 b2
。
选项2:
我们制作A
类型类的B
和Eq
个实例。
在这种情况下,我们可以使用==
符号来检查相等性,如下所示。
a1==a2
b1==b2
所以,现在问题:据我所知,存在类型类的唯一目的是我们可以重用函数名==
作为等价检查函数,如如果我们使用类型类,我们不必为了基本相同的目的使用不同的函数名(即检查相等性)。
这是对的吗?名称重用是类型类提供的唯一优势吗?或者还有更多类型的类型?
换句话说,给定任何使用类型类的程序,我可以通过给出单独的名称(即eqA
和{{}将该程序重写为不使用类型类的同构程序1)})到类型类实例声明中定义的重载函数名(即选项2中的eqB
)。
在简单示例中,此转换将使用选项1替换选项2.
这样的机械改造总是可行的吗?
这不是编译器在幕后做的吗?
感谢您的阅读。
编辑:
感谢所有有启发性的答案!
答案 0 :(得分:4)
关于类型类的这篇非常好的文章显示了他们的贬低过程:https://www.fpcomplete.com/user/jfischoff/instances-and-dictionaries
所以,是的,您可以不使用它们,但是您需要为每个类型设置不同的函数,就像您在问题中所做的那样,或者您需要按照文章中的描述显式传递实例字典,而type class隐式执行,所以你不需要打扰。在许多情况下,您最终会得到许多具有不同名称的函数!
尽管如此,您的问题还会导致类型类通常在其他模式解决问题时过多使用: http://lukepalmer.wordpress.com/2010/01/24/haskell-antipattern-existential-typeclass/
答案 1 :(得分:2)
类型类是语法糖,所以没有它们不能没有它们。这只是如果没有它们会有多么艰难的问题。
我最喜欢的例子是QuickCheck,它使用类型类来递归函数参数。 (是的,你可以这样做。)QuickCheck接受一个具有任意数量参数的函数,并为每个参数自动生成随机测试数据,并检查该函数是否总是返回true。它使用类型类来弄清楚如何“随机”生成每种类型的数据。它使用聪明的类型技巧来支持具有任意数量参数的函数。
你不需要这个;你可以有一个test1
函数,然后是一个test2
函数,然后是一个test3
函数,等等。但是能够拥有一个自动计算的“一个函数”真好有多少论据。实际上,没有类型类,你最终不得不写类似
test_Int
test_Double
test_Char
test_Int_Int
test_Int_Double
test_Int_Char
test_Double_Int
test_Double_Double
test_Double_Char
...
...对于每种可能的输入组合。这真的很烦人。 (并祝你在自己的代码中扩展它。祝你好运。)
实际上,类型类使您的代码更高级和声明性。这样可以更轻松地查看代码尝试执行的操作。 (大多数时候。)有很多高级案例,我开始认为设计可能不太正确,但对于绝大多数的东西,类型类工作得非常好。
答案 2 :(得分:1)
在大多数情况下,答案是类型类可以重复使用名称。有时,您获得的名称重用次数比您想象的要多。
类型类有点类似于面向对象语言中的接口。有时候有人可能会问,#是否是接口在API中强制执行合同的唯一目的?"。我们可能会在java中看到这段代码
public <A> void foo (List<A> list) {
list.clear();
}
List<A> list = new ArrayList<>();
List<A> list2 = new LinkedList<>();
foo(list);
foo(list2);
ArrayList和LinkedList都实现了List接口。因此,我们可以编写一个foo的通用函数,它可以在任何List上运行。这与您的Eq示例类似。在Haskell中,我们有许多这样的高阶函数。例如,foldl
可以通过不仅仅是列表来实现,我们可以对使用foldl
Foldable
的任何内容使用Data.Foldable
。
在OO中,您可以使用泛型来进行一些非常有趣的抽象和继承(例如,在诸如Foo<A extends Foo<A>>
之类的Java中)。在Haskell中,我们可以使用Type Classes来执行不同类型的抽象。
让我们参加Monad课程,这需要你实施&#34; return&#34;。 Maybe
实现return
这样的内容:
instance Monad Maybe where
return a = Just a
虽然List
实现了这样的返回:
instance Monad [] where
return a = [a]
返回,就像在Haskell中所做的那样,在Java这样的语言中是不可能的。在Java中,如果要在容器中创建某个新实例(比如新的List<String>
),则必须使用new ArrayList<String>();
。您无法创建某个实例,因为它符合合同。在Haskell中,我们可以编写在Java中无法实现的代码:
foobar :: (Monad m) => (m a -> m b) -> a -> m b
foobar g a = g $ return a
现在我可以将任何东西传递给foobar。我可以传递函数Maybe a -> Maybe b
或[a] -> [b]
,而foobar仍会接受。
当然,现在这一切都可以分解,功能可以逐一编写。就像Java可以忽略接口并在每个List类上从头开始实现一切。但就像你在OO中考虑的遗传一样,在Haskell中我们可以使用语言扩展在类型类中做一些有趣的事情。例如,请查看http://www.haskell.org/haskellwiki/Functional_dependencies_vs._type_families