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

时间:2017-07-21 08:59:38

标签: c# haskell interface typeclass

我知道这里有一个类似的问题,但我想看一个例子,它清楚地表明,你不能用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;
     }
}

如果没有正确理解,请更正我的例子

4 个答案:

答案 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接口,那么我就无法使doubleString实现该接口。但是在Haskell中,我可以定义新的SerializeToXml类型类,然后使所有标准的内置类型实现该接口(BoolDoubleInt。 ..)

另一件事是多态在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:代码不会假装工作。