簡體   English   中英

C#接口和Haskell類型類的區別

[英]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接口,那么我就不能使用doubleString實現該接口。 但是在Haskell中,我可以定義我的新SerializeToXml類型類,然后使所有標准的內置類型實現該接口( BoolDoubleInt ......)

另一件事是多態在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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM