Terapkan typeclass Haskell dengan antarmuka C #

13

Saya mencoba membandingkan kelas tipe Haskell dan antarmuka C #. Misalkan ada a Functor.

Haskell:

class Functor f where
  fmap :: (a -> b) -> f a -> f b

Bagaimana menerapkan kelas tipe ini sebagai antarmuka dalam C #?

Apa yang saya coba:

interface Functor<A, B>
{
    F<B> fmap(Func<A, B> f, F<A> x);
}

Ini adalah implementasi yang tidak valid dan saya sebenarnya terjebak dengan Ftipe generik yang harus dikembalikan oleh fmap. Bagaimana itu harus didefinisikan dan dimana?

Apakah tidak mungkin diimplementasikan Functordalam C # dan mengapa? Atau mungkin ada pendekatan lain?

ДМИТРИЙ МАЛИКОВ
sumber
8
Eric Lippert berbicara sedikit tentang bagaimana sistem tipe C # tidak benar-benar cukup untuk mendukung sifat Functors yang lebih tinggi seperti yang didefinisikan oleh Haskell dalam jawaban ini: stackoverflow.com/a/4412319/303940
KChaloux
1
Ini sekitar 3 tahun yang lalu. Sesuatu berubah?
ДМИТРИЙ МАЛИКОВ
4
tidak ada yang berubah untuk memungkinkan hal ini dalam C #, saya juga tidak berpikir kemungkinannya di masa depan
jk.

Jawaban:

8

Sistem tipe C # tidak memiliki beberapa fitur yang diperlukan untuk mengimplementasikan kelas tipe dengan benar sebagai antarmuka.

Mari kita mulai dengan contoh Anda, tetapi kuncinya menunjukkan akun yang lebih lengkap tentang apa itu typeclass dan apa yang dilakukan, dan kemudian mencoba memetakannya ke bit C #.

class Functor f where
  fmap :: (a -> b) -> f a -> f b

Ini adalah definisi kelas tipe, atau mirip dengan antarmuka. Sekarang mari kita lihat definisi dari suatu tipe dan implementasi dari kelas tipe itu.

data Awesome a = Awesome a a

instance Functor Awesome where
  fmap f (Awesome a1 a2) = Awesome (f a1) (f a2)

Sekarang kita bisa melihat dengan jelas satu fakta berbeda dari kelas tipe yang tidak bisa Anda lakukan miliki dengan antarmuka. Implementasi kelas tipe bukan bagian dari definisi tipe. Di C #, untuk mengimplementasikan antarmuka, Anda harus mengimplementasikannya sebagai bagian dari definisi tipe yang mengimplementasikannya. Ini berarti Anda tidak dapat mengimplementasikan antarmuka untuk jenis yang tidak Anda laksanakan sendiri, namun di Haskell Anda dapat menerapkan kelas jenis untuk jenis apa pun yang Anda akses.

Itu mungkin yang terbesar dengan segera, tetapi ada perbedaan lain yang cukup signifikan yang membuat padanan C # benar-benar tidak berfungsi, dan Anda menyentuhnya dalam pertanyaan Anda. Ini tentang Polimorfisme. Juga ada beberapa hal yang relatif umum yang Haskell memungkinkan Anda lakukan dengan kelas tipe yang langsung tidak menerjemahkan terutama ketika Anda mulai melihat jumlah genericism dalam tipe eksistensial atau ekstensi GHC lainnya seperti Generic ADTs.

Anda lihat, dengan Haskell Anda dapat mendefinisikan functors

data List a = List a (List a) | Terminal
data Tree a = Tree val (Tree a) (Tree a) | Terminal

instance Functor List where
  fmap :: (a -> b) -> List a -> List b
  fmap f (List a Terminal) = List (f a) Terminal
  fmap f (List a rest) = List (f a) (fmap f rest)

instance Functor Tree where
  fmap :: (a -> b) -> Tree a -> Tree b
  fmap f (Tree val Terminal Terminal) = Tree (f val) Terminal Terminal
  fmap f (Tree val Terminal right) = Tree (f val) Terminal (fmap f right)
  fmap f (Tree val left Terminal) = Tree (f val) (fmap f left) Terminal
  fmap f (Tree val left right) = Tree (f val) (fmap f left) (fmap f right)

Kemudian dalam konsumsi Anda dapat memiliki fungsi:

mapsSomething :: Functor f, Show a => f a -> f String
mapsSomething rar = fmap show rar

Di sinilah letak masalahnya. Dalam C # bagaimana Anda menulis fungsi ini?

public Tree<a> : Functor<a>
{
    public a Val { get; set; }
    public Tree<a> Left { get; set; }
    public Tree<a> Right { get; set; }

    public Functor<b> fmap<b>(Func<a,b> f)
    {
        return new Tree<b>
        {
            Val = f(val),
            Left = Left.fmap(f);
            Right = Right.fmap(f);
        };
    }
}
public string Show<a>(Showwable<a> ror)
{
    return ror.Show();
}

public Functor<String> mapsSomething<a,b>(Functor<a> rar) where a : Showwable<b>
{
    return rar.fmap(Show<b>);
}

Jadi ada beberapa hal yang salah dengan versi C #, untuk satu hal yang saya bahkan tidak yakin itu akan memungkinkan Anda untuk menggunakan <b>kualifikasi seperti saya lakukan di sana, tapi tanpa itu saya saya yakin itu tidak akan mengirimkan Show<>tepat (merasa bebas untuk mencoba dan kompilasi untuk mencari tahu; saya tidak).

Namun masalah yang lebih besar di sini adalah bahwa tidak seperti di atas di Haskell di mana kita memiliki Terminaldefinisi kita sebagai bagian dari tipe dan kemudian dapat digunakan sebagai pengganti tipe, karena C # kurang polimorfisme parametrik yang sesuai (yang menjadi sangat jelas segera setelah Anda mencoba untuk interop F # dengan C #) Anda tidak dapat dengan jelas atau bersih membedakan apakah Kanan atau Kiri adalah Terminals. Yang terbaik yang dapat Anda lakukan adalah menggunakan null, tetapi bagaimana jika Anda mencoba membuat tipe nilai a Functoratau dalam kasus di Eithermana Anda membedakan dua tipe yang sama-sama membawa nilai? Sekarang Anda harus menggunakan satu jenis dan memiliki dua nilai yang berbeda untuk memeriksa dan beralih antara untuk memodelkan diskriminasi Anda?

Kurangnya jumlah jumlah yang tepat, jenis serikat, ADT, apa pun yang Anda ingin menyebutnya benar-benar membuat banyak jenis kacamata memberi Anda murtad karena pada akhirnya mereka membiarkan Anda memperlakukan beberapa jenis (konstruktor) sebagai satu jenis, dan sistem tipe .NET yang mendasarinya tidak memiliki konsep seperti itu.

Jimmy Hoffa
sumber
2
Saya tidak terlalu berpengalaman dalam Haskell (hanya Standard ML) jadi saya tidak tahu berapa banyak perbedaan yang terjadi, tetapi dimungkinkan untuk menyandikan tipe jumlah dalam C # .
Doval
5

Yang Anda butuhkan adalah dua kelas, satu untuk memodelkan orde tinggi generik (functor) dan satu lagi untuk memodelkan functor gabungan dengan nilai bebas A

interface F<Functor> {
   IF<Functor, A> pure<A>(A a);
}

interface IF<Functor, A> where Functor : F<Functor> {
   IF<Functor, B> pure<B>(B b);
   IF<Functor, B> map<B>(Func<A, B> f);
}

Jadi jika kita menggunakan Option monad (karena semua monad adalah functors)

class Option : F<Option> {
   IF<Option, A> pure<A>(A a) { return new Some<A>(a) };
}

class OptionF<A> : IF<Option, A> {
   IF<Option, B> pure<B>(B b) {
      return new Some<B>(b);
   }

   IF<Option, B> map<B>(Func<A, B> f) {
       var some = this as Some<A>;
       if (some != null) {
          return new Some<B>(f(some.value));
       } else {
          return new None<B>();
       }
   } 
}

Anda kemudian dapat menggunakan metode ekstensi statis untuk mengkonversi dari IF <Option, B> ke Some <A> saat Anda perlu

DetriusXii
sumber
Saya mengalami kesulitan dengan pureantarmuka functor generik: kompiler mengeluh pada IF<Functor, A> pure<A>(A a);dengan "Tipe Functortidak dapat digunakan sebagai tipe parameter Functordalam tipe metode generik IF<Functor, A>. Tidak ada konversi tinju atau konversi parameter tipe dari Functorke F<Functor>." Apa artinya ini? Dan mengapa kita harus mendefinisikan puredi dua tempat? Selain itu, tidak pureboleh statis?
Niriel
1
Hai. Saya pikir karena saya mengisyaratkan ke arah monad dan transformer monad ketika merancang kelas. Transformator monad, seperti transformator monad OptionT (MaybeT dalam Haskell) didefinisikan dalam C # sebagai OptionT <M, A> di mana M adalah monad generik lain. Kotak transformator monad OptionT dalam monad tipe M <Opsi <A>>, tetapi karena C # tidak memiliki tipe yang lebih tinggi, Anda perlu cara untuk membuat instance monad M yang lebih tinggi ketika memanggil OptionT.map dan OptionT.bind. Metode statis tidak berfungsi karena Anda tidak dapat memanggil M.pure (A) untuk monad M.
DetriusXii