Triển khai kiểu chữ Haskell với giao diện C #


13

Tôi đang cố gắng so sánh các lớp loại của Haskell và giao diện của C #. Giả sử có một Functor.

Haskell:

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

Làm thế nào để thực hiện lớp loại này như giao diện trong C #?

Những gì tôi đã thử:

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

Đây là triển khai không hợp lệ và tôi thực sự bị mắc kẹt với loại chung Fnên được trả về fmap. Nó nên được định nghĩa như thế nào và ở đâu?

Có thể thực hiện Functortrong C # và tại sao? Hoặc có thể có một cách tiếp cận khác?


8
Eric Lippert nói một chút về cách hệ thống loại của C # không thực sự đủ để hỗ trợ bản chất tốt hơn của Functor như được định nghĩa bởi Haskell trong câu trả lời này: stackoverflow.com/a/4412319/303940
KChaloux

1
Đây là khoảng 3 năm trước. Một cái gì đó đã thay đổi?
ДМИТРИЙ МАЛИКОВ

4
không có gì thay đổi để biến điều này thành có thể trong C #, tôi cũng không nghĩ nó có khả năng trong tương lai
jk.

Câu trả lời:


8

Hệ thống loại của C # thiếu một vài tính năng cần thiết để triển khai đúng các lớp loại như một giao diện.

Hãy bắt đầu với ví dụ của bạn, nhưng khóa đang hiển thị một tài khoản đầy đủ hơn về kiểu chữ là gì và làm gì, sau đó cố gắng ánh xạ chúng tới các bit C #.

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

Đây là định nghĩa lớp loại, hoặc tương tự như giao diện. Bây giờ hãy xem xét một định nghĩa về một loại và đó là việc thực hiện lớp loại đó.

data Awesome a = Awesome a a

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

Bây giờ chúng ta có thể thấy rất rõ ràng một thực tế khác biệt của các lớp loại mà bạn không thể có với các giao diện. Việc thực hiện lớp loại không phải là một phần của định nghĩa của loại. Trong C #, để triển khai một giao diện, bạn phải triển khai nó như là một phần của định nghĩa về loại thực hiện giao diện đó. Điều này có nghĩa là bạn không thể thực hiện giao diện cho loại mà bạn không tự thực hiện, tuy nhiên trong Haskell, bạn có thể triển khai lớp loại cho bất kỳ loại nào bạn có quyền truy cập.

Đó có thể là lớn nhất ngay lập tức, nhưng có một sự khác biệt khá quan trọng khác khiến cho tương đương C # thực sự không hoạt động tốt và bạn đang chạm vào nó trong câu hỏi của bạn. Đó là về đa hình. Ngoài ra, có một số điều tương đối chung Haskell cho phép bạn thực hiện với các lớp loại hoàn toàn không dịch đặc biệt là khi bạn bắt đầu xem xét số lượng chung trong các loại tồn tại hoặc các phần mở rộng GHC khác như ADT chung.

Bạn thấy đấy, với Haskell, bạn có thể xác định hàm functor

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)

Sau đó, trong tiêu dùng bạn có thể có một chức năng:

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

Đây là vấn đề. Trong C # làm thế nào để bạn viết chức năng này?

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>);
}

Vì vậy, có một vài điều xảy ra với phiên bản C #, cho một điều tôi thậm chí không chắc chắn nó sẽ cho phép bạn sử dụng các <b>vòng loại như tôi đã làm ở đó, nhưng mà không có nó tôi chắc chắn nó sẽ không cử Show<>một cách thích hợp (bạn có thể thử và biên dịch để tìm hiểu; tôi đã không).

Tuy nhiên, vấn đề lớn hơn ở đây là không giống như ở Haskell, nơi chúng tôi đã Terminalxác định là một phần của loại và sau đó có thể sử dụng thay thế cho loại này, do C # thiếu tính đa hình tham số thích hợp (trở nên siêu rõ ràng ngay khi bạn cố gắng xen vào F # với C #) bạn không thể phân biệt rõ ràng hay rõ ràng Phải hay Trái là Terminals. Điều tốt nhất bạn có thể làm là sử dụng null, nhưng nếu bạn đang cố gắng tạo một loại giá trị a Functorhoặc trong trường hợp Eitherbạn phân biệt hai loại mà cả hai đều mang một giá trị thì sao? Bây giờ bạn phải sử dụng một loại và có hai giá trị khác nhau để kiểm tra và chuyển đổi giữa để mô hình hóa sự phân biệt đối xử của bạn?

Việc thiếu các kiểu tổng hợp, loại kết hợp, ADT, bất cứ điều gì bạn muốn gọi chúng thực sự làm cho rất nhiều kiểu chữ sẽ khiến bạn bỏ đi vì vào cuối ngày, chúng cho phép bạn coi nhiều loại (hàm tạo) là một loại duy nhất, và hệ thống kiểu cơ bản của .NET đơn giản là không có khái niệm như vậy.


2
Tôi không rành về Haskell (chỉ ML tiêu chuẩn) nên tôi không biết điều này tạo ra sự khác biệt bao nhiêu, nhưng có thể mã hóa các loại tổng trong C # .
Doval

5

Những gì bạn cần là hai lớp, một để mô hình hóa chung chung bậc cao (functor) và một để mô hình functor kết hợp với giá trị A miễn phí

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);
}

Vì vậy, nếu chúng ta sử dụng Tùy chọn đơn nguyên (vì tất cả các đơn vị là functor)

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>();
       }
   } 
}

Sau đó, bạn có thể sử dụng các phương thức mở rộng tĩnh để chuyển đổi từ IF <Tùy chọn, B> sang Một số <A> khi bạn cần


Tôi gặp khó khăn với puregiao diện functor chung: trình biên dịch phàn nàn IF<Functor, A> pure<A>(A a);với "Loại Functorkhông thể được sử dụng làm tham số loại Functortrong loại phương thức chung IF<Functor, A>. Không có chuyển đổi quyền anh hoặc chuyển đổi tham số loại từ Functorsang F<Functor>." Điều đó có nghĩa là gì? Và tại sao chúng ta phải xác định pureở hai nơi? Hơn nữa, không purenên tĩnh?
Niriel

1
Chào. Tôi nghĩ bởi vì tôi đã gợi ý về các đơn nguyên và máy biến áp đơn nguyên khi thiết kế lớp. Một biến áp đơn nguyên, như biến áp đơn nguyên OptionT (Có thể trong Haskell) được định nghĩa trong C # là OptionT <M, A> trong đó M là một đơn nguyên chung khác. Các hộp biến áp của đơn vị Tùy chọn trong một đơn vị loại M <Tùy chọn <A >>, nhưng vì C # không có loại được phân loại cao hơn, bạn cần một cách để khởi tạo đơn vị M loại cao hơn khi gọi OptionT.map và OptionT.bind. Các phương thức tĩnh không hoạt động vì bạn không thể gọi M.pure (A a) cho bất kỳ đơn vị M.
DetriusXii
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.