我知道这里有一个类似的问题,但我想看一个例子,它清楚地表明,你不能用interface
做什么,可以用Type Class
为了进行比较,我将给出一个示例代码:
class Eq a where
(==) :: a -> a -> Bool
instance Eq Integer where
x == y = x `integerEq` y
C#代码:
interface Eq<T> { bool Equal(T elem); }
public class Integer : Eq<int>
{
public bool Equal(int elem)
{
return _elem == elem;
}
}
如果没有正确理解,请更正我的例子
答案 0 :(得分:9)
根据类型解析类型类,而接口调度则针对显式接收器对象进行解析。类型类参数隐式提供给函数,而C#中的对象是显式提供的。例如,您可以编写以下使用Read
类的Haskell函数:
readLine :: Read a => IO a
readLine = fmap read getLine
然后您可以将其用作:
readLine :: IO Int
readLine :: IO Bool
并拥有编译器提供的相应read
实例。
您可以尝试使用接口(例如
)模拟C#中的Read
类
public interface Read<T>
{
T Read(string s);
}
但是ReadLine
的实施需要您想要的Read<T>
'实例'的参数:
public static T ReadLine<T>(Read<T> r)
{
return r.Read(Console.ReadLine());
}
Eq
类型类要求两个参数具有相同的类型,而Eq
接口不具有,因为第一个参数隐式是接收器的类型。例如,您可以:
public class String : Eq<int>
{
public bool Equal(int e) { return false; }
}
使用Eq
无法表示的。接口隐藏了接收器的类型,因此隐藏了其中一个参数的类型,这可能会导致问题。想象一下,你有一个不可变heap datastructure的类型类和接口:
class Heap h where
merge :: Ord a => h a -> h a -> h a
public interface Heap<T>
{
Heap<T> Merge(Heap<T> other);
}
合并两个二进制堆可以在O(n)中完成,而在O(n log n)中合并两个二项式堆是可能的,而对于fibonacci来说它是O(1)。 Heap接口的实现者不知道其他堆的实际类型,因此被迫使用次优算法或使用动态类型检查来发现它。相反,实现Heap
类型类的类型确实知道表示。
答案 1 :(得分:5)
C#接口定义了一组必须实现的方法。 Haskell类型类定义了一组必须实现的方法(并且可能一些方法的默认实现)。所以那里有很多相似之处。
(我猜一个重要的区别是,在C#中,接口是类型,而Haskell将类型和类型视为严格分开的东西。)
key 的区别在于,在C#中,当您定义一个类型(即,编写一个类)时,您可以准确定义它实现的接口,并且这一切都是冻结的。在Haskell中,您可以随时将新接口添加到现有类型中。
例如,如果我在C#中编写一个新的SerializeToXml
接口,那么我就无法使double
或String
实现该接口。但是在Haskell中,我可以定义新的SerializeToXml
类型类,然后使所有标准的内置类型实现该接口(Bool
,Double
,Int
。 ..)
另一件事是多态在Haskell中是如何工作的。在OO语言中,您将调度对象被调用的方法的类型。在Haskell中,实现该方法的类型可以在类型签名中出现 anywhere 。最特别的是,read
调度你想要的返回类型 - 在OO语言中通常根本无法做到的事情,甚至是功能重载都没有。
另外,在C#中,很难说“这两个参数必须具有相同的类型”。然后,OO以Liskov替换委托人为基础;两个都来自Customer
的类应该是可以互换的,那么为什么要将两个Customer
对象约束为相同的类型的客户?
想想看,OO语言在运行时进行方法查找,而Haskell在编译时进行方法查找。这并不是很明显,但Haskell多态实际上比通常的OO多态更像C ++模板。 (但这并不是特别与类型类有关,而是Haskell如何实现多态性。)
答案 2 :(得分:3)
其他人已经提供了很好的答案。
我只想添加一个关于他们差异的实际例子。假设我们想要建模一个“向量空间”类型类/接口,它包含2D,3D等矢量的常见操作。
在Haskell:
class Vector a where
scale :: a -> Double -> a
add :: a -> a -> a
data Vec2D = V2 Double Double
instance Vector (Vec2D) where
scale s (V2 x y) = V2 (s*x) (s*y)
add (V2 x1 y1) (V2 x2 y2) = V2 (x1+x2) (y2+y2)
-- the same for Vec3D
在C#中,我们可能会尝试以下错误的方法(我希望我的语法正确)
interface IVector {
IVector scale(double s);
IVector add(IVector v);
}
class Vec2D : IVector {
double x,y;
// constructor omitted
IVector scale(double s) {
return new Vec2D(s*x, s*y);
}
IVector add(IVector v) {
return new Vec2D(x+v.x, y+v.y);
}
}
我们在这里有两个问题。
首先,scale
仅返回IVector
,即实际Vec2D
的超类型。这很糟糕,因为缩放不会保留类型信息。
其次,add
输入错误!我们无法使用v.x
,因为v
是可能没有IVector
字段的任意x
。
实际上,界面本身是错误的:add
方法承诺任何向量必须与任何其他向量相加,因此我们必须能够对2D和3D向量求和,这是无意义的。
通常的解决方案是切换到F-bounded quantification AKA CRTP或者这些天被称为的任何内容:
interface IVector<T> {
T scale(double s);
T add(T v);
}
class Vec2D : IVector<Vec2D> {
double x,y;
// constructor omitted
Vec2D scale(double s) {
return new Vec2D(s*x, s*y);
}
Vec2D add(Vec2D v) {
return new Vec2D(x+v.x, y+v.y);
}
}
程序员第一次遇到这种情况时,他们常常被看似“递归”的行Vec2D : IVector<Vec2D>
弄糊涂了。我当然是:)然后我们习惯了这个并接受它作为惯用的解决方案。
类型类可以说在这里有更好的解决方案。
答案 3 :(得分:0)
经过长时间的研究,我找到了一个简单的解释方法。至少对我来说很清楚。
想象一下,我们有像这样的签名方法
public static T[] Sort(T[] array, IComparator<T> comparator)
{
...
}
IComparator
的实施:
public class IntegerComparator : IComparator<int> { }
然后我们可以编写这样的代码:
var sortedIntegers = Sort(integers, new IntegerComparator());
我们可以改进此代码,首先我们创建Dictionary<Type, IComparator>
并填写它:
var comparators = new Dictionary<Type, IComparator>()
{
[typeof(int)] = new IntegerComparator(),
[typeof(string)] = new StringComparator()
}
重新设计的IComparator接口,以便我们可以像上面那样写
public interface IComparator {} public interface IComparator<T> : IComparator {}
在此之后,让我们重新设计Sort
方法签名
public class SortController
{
public T[] Sort(T[] array, [Injectable]IComparator<T> comparator = null)
{
...
}
}
如您所知,我们将注入IComparator<T>
,并编写如下代码:
new SortController().Sort<int>(integers, (IComparator<int>)_somparators[typeof(int)])
正如您已经猜到的那样,在我们概述实现并添加Dictionary<Type, IComparator>
注意,我们只在运行时看到的异常
现在想象一下,如果这个工作是由编译器在构建期间为我们完成的,如果它找不到具有相应类型的比较器,它会抛出异常。
为此,我们可以帮助编译器并添加新关键字而不是使用属性。 Out Sort
方法将如下所示:
public static T[] Sort(T[] array, implicit IComparator<T> comparator)
{
...
}
实现具体的代码比较器:
public class IntegerComparator : IComparator<int> implicit { }
注意,我们使用关键字&#39; implicit&#39;,在此编译器之后将能够做到 我们上面写的日常工作,将在异常期间抛出异常 编译时
var sortedIntegers = Sort(integers);
// this gives us compile-time error
// because we don't have implementation of IComparator<string>
var sortedStrings = Sort(strings);
并将此名称命名为 Type Class
public class IntegerComparator : IComparator<int> implicit { }
我希望我理解正确并且可以理解地解释。
PS:代码不会假装工作。