[英]Difference between C# interface and Haskell Type Class
我知道這里有一個類似的問題,但我想看一個例子,它清楚地表明,你不能用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;
}
}
如果沒有正確理解,請糾正我的例子
根據類型解析類型類,而接口調度則針對顯式接收器對象進行解析。 類型類參數隱式提供給函數,而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
。 接口隱藏了接收器的類型,因此隱藏了其中一個參數的類型,這可能會導致問題。 想象一下,你有一個不可變堆數據結構的類型類和接口:
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
類型類的類型確實知道表示。
AC#接口定義了一組必須實現的方法。 Haskell類型類定義了一組必須實現的方法( 可能還有一些方法的默認實現)。 所以那里有很多相似之處。
(我猜一個重要的區別是,在C#中,接口是一種類型,而Haskell將類型和類型類視為嚴格分離的東西。)
關鍵的區別在於,在C#中,當您定義一個類型(即,編寫一個類)時,您確切地定義了它實現的接口,並且這一切都被凍結了。 在Haskell中,您可以隨時向現有類型添加新接口。
例如,如果我在C#中編寫一個新的SerializeToXml
接口,那么我就不能使用double
或String
實現該接口。 但是在Haskell中,我可以定義我的新SerializeToXml
類型類,然后使所有標准的內置類型實現該接口( Bool
, Double
, Int
......)
另一件事是多態在Haskell中是如何工作的。 在OO語言中,您將調度對象被調用的方法的類型。 在Haskell中,實現該方法的類型可以出現在類型簽名中的任何位置 。 最重要的是, read
您想要的返回類型的調度 - 在OO語言中通常根本不能執行的操作,甚至不能使用函數重載。
而且,在C#中,很難說“這兩個參數必須具有相同的類型”。 然后,OO以Liskov替換委托人為基礎; 兩個都來自Customer
應該是可以互換的,那么為什么要將兩個Customer
對象約束為同一類型的客戶呢?
想想看,OO語言在運行時進行方法查找,而Haskell在編譯時進行方法查找。 這並不是很明顯,但Haskell多態實際上比通常的OO多態更像C ++模板。 (但這並不是特別與類型類有關,而是Haskell如何實現多態性。)
其他人已經提供了很好的答案。
我只想添加一個關於他們差異的實際例子。 假設我們想要建模一個“向量空間”類型類/接口,它包含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
是錯誤的類型! 我們不能使用vx
因為v
是一個可能沒有x
字段的任意IVector
。
實際上,接口本身是錯誤的: add
方法承諾任何向量必須與任何其他向量相加,因此我們必須能夠對2D和3D向量求和,這是無意義的。
通常的解決方案是切換到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);
}
}
程序員第一次遇到這種情況時,他們常常被看似“遞歸”的行Vec2D : IVector<Vec2D>
。 我當然是:)然后我們習慣了這個並接受它作為慣用的解決方案。
類型類可以說在這里有更好的解決方案。
經過對這個問題的長期研究,我找到了一個簡單的解釋方法。 至少對我來說很清楚。
想象一下,我們有像這樣的簽名方法
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 { }
注意,我們使用關鍵字'implicit',在此編譯器之后將能夠執行上面編寫的例行工作,並且在編譯期間將拋出異常
var sortedIntegers = Sort(integers);
// this gives us compile-time error
// because we don't have implementation of IComparator<string>
var sortedStrings = Sort(strings);
並給出這種實現類型類的名稱
public class IntegerComparator : IComparator<int> implicit { }
我希望我理解正確,理解可解釋。
PS:代碼不會假裝工作。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.