简体   繁体   English

C#接口和Haskell类型类的区别

[英]Difference between C# interface and Haskell Type Class

I know that there is a similar question here, but I would like to see an example, which clearly shows, what you can not do with interface and can with Type Class 我知道这里有一个类似的问题,但我想看一个例子,它清楚地表明,你不能用interface做什么,可以用Type Class

For comparison I'll give you an example code: 为了比较,我将给你一个示例代码:

class Eq a where 
    (==) :: a -> a -> Bool
instance Eq Integer where 
    x == y  =  x `integerEq` y

C# code: C#代码:

interface Eq<T> { bool Equal(T elem); }
public class Integer : Eq<int> 
{
     public bool Equal(int elem) 
     {
         return _elem == elem;
     }
}

Correct my example, if not correctly understood 如果没有正确理解,请纠正我的例子

Typeclasses are resolved based on a type, while interface dispatch happens against an explicit receiver object. 根据类型解析类型类,而接口调度则针对显式接收器对象进行解析。 Type class arguments are implicitly provided to a function while objects in C# are provided explicitly. 类型类参数隐式提供给函数,而C#中的对象是显式提供的。 As an example, you could write the following Haskell function which uses the Read class: 例如,您可以编写以下使用Read类的Haskell函数:

readLine :: Read a => IO a
readLine = fmap read getLine

which you can then use as: 您可以将其用作:

readLine :: IO Int
readLine :: IO Bool

and have the appropriate read instance provided by the compiler. 并具有编译器提供的适当read实例。

You could try to emulate the Read class in C# with an interface eg 您可以尝试使用接口模拟C#中的Read

public interface Read<T>
{
    T Read(string s);
}

but then the implementation of ReadLine would need a parameter for the Read<T> 'instance' you want: 但是ReadLine的实现需要一个你想要的Read<T> '实例'的参数:

public static T ReadLine<T>(Read<T> r)
{
    return r.Read(Console.ReadLine());
}

The Eq typeclass requires both arguments have the same type, whereas your Eq interface does not since the first argument is implicitly the type of the receiver. Eq类型类要求两个参数具有相同的类型,而您的Eq接口不需要,因为第一个参数隐式地是接收器的类型。 You could for example have: 例如,您可以:

public class String : Eq<int>
{
    public bool Equal(int e) { return false; }
}

which you cannot represent using Eq . 你无法使用Eq Interfaces hide the type of the receiver and hence the type of one of the arguments, which can cause problems. 接口隐藏了接收器的类型,因此隐藏了其中一个参数的类型,这可能会导致问题。 Imagine you have a typeclass and interface for an immutable 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);
}

Merging two binary heaps can be done in O(n) while merging two binomial heaps is possible in O(n log n) and for fibonacci heaps it's O(1). 合并两个二进制堆可以在O(n)中完成,而在O(n log n)中合并两个二项式堆是可能的,而对于fibonacci来说它是O(1)。 Implementors of the Heap interface do not know the real type of the other heap so is forced to either use a sub-optimal algorithm or use dynamic type checks to discover it. Heap接口的实现者不知道其他堆的实际类型,因此被迫使用次优算法或使用动态类型检查来发现它。 In contrast, types implementing the Heap typeclass do know the representation. 相反,实现Heap类型类的类型确实知道表示。

AC# interface defines a set of methods that must be implemented. AC#接口定义了一组必须实现的方法。 A Haskell type class defines a set of methods that must be implemented (and possibly a set of default implementations for some of the methods). Haskell类型类定义了一组必须实现的方法( 可能还有一些方法的默认实现)。 So there's a lot of similarities there. 所以那里有很多相似之处。

(I guess an important difference is that in C#, an interface is a type, whereas Haskell regards types and type classes as strictly separate things.) (我猜一个重要的区别是,在C#中,接口一种类型,而Haskell将类型和类型类视为严格分离的东西。)

The key difference is that in C#, when you define a type (ie, write a class), you define exactly what interfaces it implements, and this is frozen for all time. 关键的区别在于,在C#中,当您定义一个类型(即,编写一个类)时,您确切地定义了它实现的接口,并且这一切都被冻结了。 In Haskell, you can add new interfaces to an existing type at any time. 在Haskell中,您可以随时向现有类型添加新接口。

For example, if I write a new SerializeToXml interface in C#, I cannot then make double or String implement that interface. 例如,如果我在C#中编写一个新的SerializeToXml接口,那么我就不能使用doubleString实现该接口。 But in Haskell, I can define my new SerializeToXml type class, and then make all the standard, built-in types implement that interface ( Bool , Double , Int ...) 但是在Haskell中,我可以定义我的新SerializeToXml类型类,然后使所有标准的内置类型实现该接口( BoolDoubleInt ......)

The other thing is how polymorphism works in Haskell. 另一件事是多态在Haskell中是如何工作的。 In an OO language, you dispatch on the type of the method the object is being invoked on. 在OO语言中,您将调度对象被调用的方法的类型。 In Haskell, the type that the method is implemented for can appear anywhere in the type signature. 在Haskell中,实现该方法的类型可以出现在类型签名中的任何位置 Most particularly, read dispatches on the return type you want — something you usually can't do at all in OO languages, not even with function overloading. 最重要的是, read您想要的返回类型的调度 - 在OO语言中通常根本不能执行的操作,甚至不能使用函数重载。

Also, in C# it's kind of hard to say "these two arguments must have the same type". 而且,在C#中,很难说“这两个参数必须具有相同的类型”。 Then again, OO is predicated on the Liskov substitution principal; 然后,OO以Liskov替换委托人为基础; two classes that both descend from Customer should be interchangeable, so why would you want to constrain two Customer objects to both be the same type of customer? 两个都来自Customer应该是可以互换的,那么为什么要将两个Customer对象约束为同一类型的客户呢?

Come to think of it, OO languages do method lookup at run-time , whereas Haskell does method lookup at compile-time . 想想看,OO语言在运行时进行方法查找,而Haskell在编译时进行方法查找。 This isn't immediately obvious, but Haskell polymorphism actually works more like C++ templates than usual OO polymorphism. 这并不是很明显,但Haskell多态实际上比通常的OO多态更像C ++模板。 (But that's not especially to do with type classes, it's just how Haskell does polymorphism as such.) (但这并不是特别与类型类有关,而是Haskell如何实现多态性。)

Others have already provided excellent answers. 其他人已经提供了很好的答案。

I only want to add a practical example about their differences. 我只想添加一个关于他们差异的实际例子。 Suppose we want to model a "vector space" typeclass/interface, which contains the common operations of 2D, 3D, etc. vectors. 假设我们想要建模一个“向量空间”类型类/接口,它包含2D,3D等矢量的常见操作。

In Haskell: 在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

In C#, we might try the following wrong approach (I hope I get the syntax right) 在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);
   }
}

We have two issues here. 我们这里有两个问题。

First, scale returns only an IVector , a supertype of the actual Vec2D . 首先, scale只返回一个IVector ,一个实际Vec2D的超类型。 This is bad, because scaling does not preserve the type information. 这很糟糕,因为缩放不会保留类型信息。

Second, add is ill-typed! 第二, add是错误的类型! We can't use vx since v is an arbitrary IVector which might not have the x field. 我们不能使用vx因为v是一个可能没有x字段的任意IVector

Indeed, the interface itself is wrong: the add method promises that any vector must be summable with any other vector, so we must be able to sum 2D and 3D vectors, which is nonsense. 实际上,接口本身是错误的: add方法承诺任何向量必须与任何其他向量相加,因此我们必须能够对2D和3D向量求和,这是无意义的。

The usual solution is to switch to F-bounded quantification AKA CRTP or whatever it's being called these days: 通常的解决方案是切换到F-bounded量化 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);
   }
}

The first time a programmer meets this, they are usually puzzled by the seemingly "recursive" line Vec2D : IVector<Vec2D> . 程序员第一次遇到这种情况时,他们常常被看似“递归”的行Vec2D : IVector<Vec2D> I surely was :) Then we get used to this and accept it as an idiomatic solution. 我当然是:)然后我们习惯了这个并接受它作为惯用的解决方案。

Type classes arguably have a nicer solution here. 类型类可以说在这里有更好的解决方案。

After a long study of this issue, I came to an easy method of explaining. 经过对这个问题的长期研究,我找到了一个简单的解释方法。 At least for me it's clear. 至少对我来说很清楚。

Imagine we have method with signature like this 想象一下,我们有像这样的签名方法

public static T[] Sort(T[] array, IComparator<T> comparator) 
{
    ...
}

And implementation of IComparator : 并实现IComparator

public class IntegerComparator : IComparator<int> { }

Then we can write code like this: 然后我们可以编写如下代码:

var sortedIntegers = Sort(integers, new IntegerComparator());

We can improve this code, first we create Dictionary<Type, IComparator> and fill it: 我们可以改进这个代码,首先我们创建Dictionary<Type, IComparator>并填充它:

var comparators = new Dictionary<Type, IComparator>() 
{
    [typeof(int)]    = new IntegerComparator(),
    [typeof(string)] = new StringComparator() 
}

Redesigned IComparator interface so that we could write like above 重新设计的IComparator接口,以便我们可以像上面那样编写

 public interface IComparator {} public interface IComparator<T> : IComparator {} 

And after this let's redesign Sort method signature 在此之后让我们重新设计Sort方法签名

public class SortController
{
    public T[] Sort(T[] array, [Injectable]IComparator<T> comparator = null) 
    {
        ...
    }
}

As you understand we are going to inject IComparator<T> , and write code like this: 如您所知,我们将注入IComparator<T> ,并编写如下代码:

new SortController().Sort<int>(integers, (IComparator<int>)_somparators[typeof(int)])

As you already guessed this code will not work for other types until we outline the implementation and add in Dictionary<Type, IComparator> 正如您已经猜到的那样,在我们概述实现并添加到Dictionary<Type, IComparator>之前,此代码将不适用于其他类型。

Notice, the exception we will see only on runtime 请注意,我们将仅在运行时看到的异常

And now imagine if this work was done for us by the compiler during build and it threw exception if it could not find the comparator with corresponding types. 现在想象一下,如果这个工作是由编译器在构建期间为我们完成的,并且如果它找不到具有相应类型的比较器则抛出异常。

For this, we could help the compiler and add a new keyword instead of usage attribute. 为此,我们可以帮助编译器并添加新关键字而不是使用属性。 Out Sort method will be look like this: Out Sort方法将如下所示:

public static T[] Sort(T[] array, implicit IComparator<T> comparator) 
{
    ...
}

And code of realization concrete Comparator: 和具体实施代码比较器:

public class IntegerComparator : IComparator<int> implicit { }

Note, we use the keyword 'implicit', after this compiler will be able to do routine work, which we wrote above, and the exception will be thrown during compile-time 注意,我们使用关键字'implicit',在此编译器之后将能够执行上面编写的例行工作,并且在编译期间将抛出异常

var sortedIntegers = Sort(integers);

// this gives us compile-time error
// because we don't have implementation of IComparator<string> 
var sortedStrings = Sort(strings); 

And give the name to this style of implementation Type Class 并给出这种实现类型类的名称

public class IntegerComparator : IComparator<int> implicit { }

I hope that I understood correctly and understandably explained. 我希望我理解正确,理解可解释。

PS: The code does not pretend to work. PS:代码不会假装工作。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM