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

kog*_*oia 4 c# haskell interface typeclass

我知道这里有一个类似的问题,但我希望看到一个例子,它清楚地表明,你不能做什么,interface可以使用Type Class

为了比较,我将给你一个示例代码:

class Eq a where 
    (==) :: a -> a -> Bool
instance Eq Integer where 
    x == y  =  x `integerEq` y
Run Code Online (Sandbox Code Playgroud)

C#代码:

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

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

Lee*_*Lee 9

根据类型解析类型类,而接口调度则针对显式接收器对象进行解析.类型类参数隐式提供给函数,而C#中的对象是显式提供的.例如,您可以编写以下使用Read该类的Haskell函数:

readLine :: Read a => IO a
readLine = fmap read getLine
Run Code Online (Sandbox Code Playgroud)

您可以将其用作:

readLine :: IO Int
readLine :: IO Bool
Run Code Online (Sandbox Code Playgroud)

并拥有read编译器提供的适当实例.

您可以尝试Read使用接口模拟C#中的类

public interface Read<T>
{
    T Read(string s);
}
Run Code Online (Sandbox Code Playgroud)

但是然后执行ReadLine需要一个Read<T>你想要的'实例' 的参数:

public static T ReadLine<T>(Read<T> r)
{
    return r.Read(Console.ReadLine());
}
Run Code Online (Sandbox Code Playgroud)

Eq类型类需要两个参数具有相同类型的,而你的Eq界面不会因为第一个参数是隐式接收器的类型.例如,您可以:

public class String : Eq<int>
{
    public bool Equal(int e) { return false; }
}
Run Code Online (Sandbox Code Playgroud)

你不能代表使用Eq.接口隐藏了接收器的类型,因此隐藏了其中一个参数的类型,这可能会导致问题.想象一下,你有一个不可变堆数据结构的类型类和接口:

class Heap h where
  merge :: Ord a => h a -> h a -> h a

public interface Heap<T>
{
    Heap<T> Merge(Heap<T> other);
}
Run Code Online (Sandbox Code Playgroud)

合并两个二进制堆可以在O(n)中完成,而在O(n log n)中合并两个二项式堆是可能的,而对于fibonacci来说它是O(1).Heap接口的实现者不知道其他堆的实际类型,因此被迫使用次优算法或使用动态类型检查来发现它.相反,实现Heap类型类的类型确实知道表示.

  • @kogoia - 是Haskell隐式为您提供类型类实例.Scala具有可用于模拟类型类的隐式参数,尽管Haskell仅允许每种类型使用一个类型类实例,而scala方法允许您拥有多个实例,即您可以拥有多个`Comparer [Int]`实例.Haskell要求您为要定义的每个实例定义一个新类型(例如参见[Data.Monoid]中的`Product`和`Sum`(https://hackage.haskell.org/package/base-4.9.1.0) /docs/Data-Monoid.html). (4认同)
  • 你能在 C# 上写伪代码,这行不通,但在 Haskell 中可以实现吗? (2认同)

Mat*_*hid 5

AC#接口定义了一组必须实现的方法.Haskell类型类定义了一组必须实现的方法(可能还有一些方法的默认实现).所以那里有很多相似之处.

(我猜一个重要的区别是,在C#中,接口一种类型,而Haskell将类型和类型类视为严格分离的东西.)

关键的区别是,在C#中,当你定义一个类型(即,写一个类),可以定义到底是什么接口它实现,这被冻结所有的时间.在Haskell中,您可以随时向现有类型添加新接口.

例如,如果我SerializeToXml在C#中编写一个新接口,那么我就无法制作doubleString实现该接口.但是在Haskell,我可以定义我的新SerializeToXml类型的类,然后让所有的标准,内置类型实现该接口(Bool,Double,Int...)

另一件事是多态在Haskell中是如何工作的.在OO语言中,您将调度对象被调用的方法的类型.在Haskell中,实现该方法的类型可以出现在类型签名中的任何位置.最重要的是,read调度你想要的返回类型 - 在OO语言中通常根本不能做的事情,甚至不能使用函数重载.

而且,在C#中,很难说"这两个参数必须具有相同的类型".然后,OO以Liskov替换委托人为基础; 两个下降的类Customer应该是可以互换的,那么为什么要将两个Customer对象约束为同一类型的客户呢?

想想看,OO语言在运行时进行方法查找,而Haskell在编译时进行方法查找.这并不是很明显,但Haskell多态实际上比通常的OO多态更像C++模板.(但这并不是特别与类型类有关,而是Haskell如何实现多态性.)


chi*_*chi 5

其他人已经提供了很好的答案。

我只想添加一个有关它们差异的实际示例。假设我们要建模一个“向量空间”类型类/接口,其中包含 2D、3D 等向量的常见操作。

在哈斯克尔:

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
Run Code Online (Sandbox Code Playgroud)

在 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);
   }
}
Run Code Online (Sandbox Code Playgroud)

我们这里有两个问题。

首先,scale只返回 an IVector,它是实际Vec2D. 这很糟糕,因为缩放不会保留类型信息。

其次,add是类型错误!我们不能使用v.x因为v是一个IVector可能没有该x字段的任意值。

事实上,接口本身是错误的:该add方法承诺任何向量必须与任何其他向量相加,因此我们必须能够对 2D 和 3D 向量求和,这是无稽之谈。

通常的解决方案是切换到F 有界量化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);
   }
}
Run Code Online (Sandbox Code Playgroud)

程序员第一次遇到这种情况时,通常会对看似“递归”的行感到困惑Vec2D : IVector<Vec2D>。我当然是:) 然后我们习惯了这一点,并接受它作为一个惯用的解决方案。

可以说类型类在这里有一个更好的解决方案。


kog*_*oia 0

经过对这个问题的长期研究,我找到了一种简单的解释方法。至少对我来说很清楚。

想象一下我们有这样的签名方法

public static T[] Sort(T[] array, IComparator<T> comparator) 
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

并实施IComparator

public class IntegerComparator : IComparator<int> { }
Run Code Online (Sandbox Code Playgroud)

然后我们可以编写这样的代码:

var sortedIntegers = Sort(integers, new IntegerComparator());
Run Code Online (Sandbox Code Playgroud)

我们可以改进这段代码,首先我们创建Dictionary<Type, IComparator>并填充它:

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

重新设计了 IComparator 接口,以便我们可以像上面那样编写

public interface IComparator {}
public interface IComparator<T> : IComparator {}
Run Code Online (Sandbox Code Playgroud)

之后让我们重新设计Sort方法签名

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

如您所知,我们将注入IComparator<T>并编写如下代码:

new SortController().Sort<int>(integers, (IComparator<int>)_somparators[typeof(int)])
Run Code Online (Sandbox Code Playgroud)

正如您已经猜到的,在我们概述实现并添加之前,此代码不适用于其他类型Dictionary<Type, IComparator>

注意,我们只会在运行时看到异常

现在想象一下,如果编译器在构建期间为我们完成了这项工作,并且如果找不到具有相应类型的比较器,则会抛出异常。

为此,我们可以帮助编译器添加一个新的关键字而不是使用属性。OutSort方法将如下所示:

public static T[] Sort(T[] array, implicit IComparator<T> comparator) 
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

以及具体Comparator的实现代码:

public class IntegerComparator : IComparator<int> implicit { }
Run Code Online (Sandbox Code Playgroud)

注意,我们使用关键字“implicit”,之后编译器将能够执行我们上面编写的例行工作,并且将在编译时抛出异常

var sortedIntegers = Sort(integers);

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

并将这种实现风格命名为Type Class

public class IntegerComparator : IComparator<int> implicit { }
Run Code Online (Sandbox Code Playgroud)

我希望我理解正确并且解释得通俗易懂。

PS:该代码不会假装工作。