Làm thế nào để các đơn vị miễn phí và phần mở rộng phản ứng tương quan?


14

Tôi đến từ nền tảng C #, nơi LINQ phát triển thành Rx.NET, nhưng luôn có hứng thú với FP. Sau một số giới thiệu về các đơn nguyên và một số dự án phụ trong F #, tôi đã sẵn sàng để thử và bước lên cấp độ tiếp theo.

Bây giờ, sau một vài cuộc nói chuyện về đơn nguyên miễn phí từ những người từ Scala và nhiều bài viết trong Haskell, hoặc F #, tôi đã tìm thấy các ngữ pháp với các thông dịch viên để hiểu khá giống với IObservablechuỗi.

Trong FRP, bạn soạn một định nghĩa hoạt động từ các khối cụ thể của miền nhỏ hơn bao gồm các tác dụng phụ và lỗi nằm trong chuỗi và mô hình hóa ứng dụng của bạn như một tập hợp các hoạt động và tác dụng phụ. Trong đơn nguyên miễn phí, nếu tôi hiểu chính xác, bạn cũng làm như vậy bằng cách thực hiện các thao tác của mình như functor và nâng chúng bằng coyoneda.

Điều gì sẽ là sự khác biệt giữa cả hai nghiêng kim về bất kỳ phương pháp tiếp cận nào? Sự khác biệt cơ bản khi xác định dịch vụ hoặc chương trình của bạn là gì?


2
Đây là một cách thú vị để suy nghĩ về vấn đề ... FRP có thể được xem như là một đơn nguyên, ngay cả khi nó thường không được xây dựng theo cách đó . Hầu hết (mặc dù không phải tất cả) các đơn nguyênđẳng cấu với đơn nguyên tự do . Như Contlà đơn nguyên duy nhất tôi đã thấy đề xuất rằng không thể được thể hiện thông qua đơn nguyên miễn phí, có lẽ người ta có thể cho rằng FRP có thể. Như có thể hầu hết mọi thứ khác .
Jules

2
Theo Erik Meijer, người thiết kế cả LINQ và Rx.NET, IObservablelà một ví dụ của đơn nguyên tiếp tục.
Jörg W Mittag

1
Tôi không có thời gian để tìm hiểu chi tiết ngay bây giờ, nhưng tôi đoán là cả hai phần mở rộng RX và cách tiếp cận đơn nguyên miễn phí đều thực hiện các mục tiêu rất giống nhau nhưng có thể có cấu trúc hơi khác nhau. Có thể bản thân các đài quan sát của RX là các đơn nguyên và sau đó bạn có thể ánh xạ một tính toán đơn nguyên miễn phí sang một người sử dụng các vật thể quan sát được, đó chính là ý nghĩa của "đơn vị tự do" trong "đơn vị tự do". Hoặc có thể mối quan hệ không phải là trực tiếp và bạn chỉ cần chọn cách chúng được sử dụng cho các mục đích tương tự.
Tikhon Jelvis

Câu trả lời:


6

Đơn nguyên

Một đơn nguyên bao gồm

  • Một endofunctor . Trong thế giới kỹ thuật phần mềm của chúng tôi, chúng tôi có thể nói điều này tương ứng với một kiểu dữ liệu với một tham số loại duy nhất, không bị hạn chế. Trong C #, đây sẽ là một cái gì đó có dạng:

    class M<T> { ... }
    
  • Hai hoạt động được xác định trên kiểu dữ liệu đó:

    • return/ purelấy một giá trị "thuần" (nghĩa là một Tgiá trị) và "bọc" nó vào đơn nguyên (nghĩa là nó tạo ra một M<T>giá trị). Vì returnlà một từ khóa dành riêng trong C #, nên tôi sẽ sử dụng puređể tham khảo thao tác này kể từ bây giờ. Trong C #, puresẽ là một phương thức có chữ ký như:

      M<T> pure(T v);
      
    • bind/ flatmaplấy một giá trị đơn trị ( M<A>) và một hàm f. fnhận một giá trị thuần túy và trả về một giá trị đơn trị ( M<B>). Từ những điều này, bindtạo ra một giá trị đơn âm mới ( M<B>). bindcó chữ ký C # sau:

      M<B> bind(M<A> mv, Func<A, M<B>> f);
      

Ngoài ra, để trở thành một đơn nguyên, purebindđược yêu cầu phải tuân theo ba luật đơn nguyên.

Bây giờ, một cách để mô hình hóa các đơn nguyên trong C # sẽ là xây dựng giao diện:

interface Monad<M> {
  M<T> pure(T v);
  M<B> bind(M<A> mv, Func<A, M<B>> f);
}

(Lưu ý: Để giữ cho mọi thứ ngắn gọn và diễn cảm, tôi sẽ thực hiện một số quyền tự do với mã trong suốt câu trả lời này.)

Bây giờ chúng ta có thể triển khai các đơn nguyên cho các bảng dữ liệu cụ thể bằng cách triển khai các triển khai cụ thể của Monad<M>. Chẳng hạn, chúng tôi có thể triển khai đơn nguyên sau cho IEnumerable:

class IEnumerableM implements Monad<IEnumerable> {
  IEnumerable<T> pure(T v) {
    return (new List<T>(){v}).AsReadOnly();
  }

  IEnumerable<B> bind(IEnumerable<A> mv, Func<A, IEnumerable<B>> f) {
    ;; equivalent to mv.SelectMany(f)
    return (from a in mv
            from b in f(a)
            select b);
  }
}

(Tôi cố tình sử dụng cú pháp LINQ để gọi ra mối quan hệ giữa cú pháp LINQ và các đơn nguyên. Nhưng lưu ý rằng chúng ta có thể thay thế truy vấn LINQ bằng một cuộc gọi đến SelectMany.)

Bây giờ, chúng ta có thể định nghĩa một đơn nguyên cho IObservable? Có vẻ như vậy:

class IObservableM implements Monad<IObservable> {
  IObservable<T> pure(T v){
    Observable.Return(v);
  }

  IObservable<B> bind(IObservable<A> mv, Func<A, IObservable<B>> f){
    mv.SelectMany(f);
  }
}

Để chắc chắn rằng chúng ta có một đơn nguyên, chúng ta cần chứng minh các luật đơn nguyên. Điều này có thể không tầm thường (và tôi không đủ quen thuộc với Rx.NET để biết liệu chúng có thể được chứng minh chỉ từ thông số kỹ thuật không), nhưng đó là một khởi đầu đầy hứa hẹn. Để tạo điều kiện cho phần còn lại của cuộc thảo luận này, chúng ta hãy giả sử các luật đơn nguyên giữ trong trường hợp này.

Đơn nguyên miễn phí

Không có "đơn nguyên" duy nhất. Thay vào đó, các đơn vị tự do là một lớp các đơn vị được xây dựng từ functor. Đó là, được đưa ra một functor F, chúng ta có thể tự động lấy ra một đơn nguyên cho F(nghĩa là đơn vị tự do F).

Chức năng

Giống như các đơn nguyên, functor có thể được xác định bởi ba mục sau:

  • Một kiểu dữ liệu, được tham số hóa qua một biến loại đơn, không giới hạn.
  • Hai hoạt động:

    • purekết thúc một giá trị thuần túy vào functor. Điều này là tương tự purecho một đơn nguyên. Trong thực tế, đối với các functor cũng là một đơn nguyên, cả hai nên giống hệt nhau.
    • fmapánh xạ các giá trị trong đầu vào thành các giá trị mới trong đầu ra thông qua một chức năng nhất định. Chữ ký của nó là:

      F<B> fmap(Func<A, B> f, F<A> fv)
      

Giống như các đơn nguyên, functor được yêu cầu phải tuân theo luật functor.

Tương tự như monads, chúng ta có thể mô hình functor thông qua giao diện sau:

interface Functor<F> {
  F<T> pure(T v);
  F<B> fmap(Func<A, B> f, F<A> fv);
}

Bây giờ, vì các đơn nguyên là một lớp con của functor, chúng ta cũng có thể cấu trúc lại Monadmột chút:

interface Monad<M> extends Functor<M> {
  M<T> join(M<M<T>> mmv) {
    Func<T, T> identity = (x => x);
    return mmv.bind(x => x); // identity function
  }

  M<B> bind(M<A> mv, Func<A, M<B>> f) {
    join(fmap(f, mv));
  }
}

Ở đây tôi đã thêm một phương thức bổ sung joinvà cung cấp các cài đặt mặc định của cả hai joinbind. Lưu ý, tuy nhiên, đây là những định nghĩa tròn. Vì vậy, bạn phải ghi đè ít nhất một hoặc khác. Ngoài ra, lưu ý rằng purebây giờ được kế thừa từ Functor.

IObservable và đơn nguyên miễn phí

Bây giờ, vì chúng ta đã định nghĩa một đơn nguyên cho IObservablevà vì các đơn vị là một lớp con của hàm functor, do đó chúng ta phải có thể định nghĩa một thể hiện functor cho IObservable. Đây là một định nghĩa:

class IObservableF implements Functor<IObservable> {
  IObservable<T> pure(T v) {
    return Observable.Return(v);
  }

  IObservable<B> fmap(Func<A, B> f, IObservable<A> fv){
    return fv.Select(f);
  }
}

Bây giờ chúng ta có một functor được xác định cho IObservable, chúng ta có thể xây dựng một đơn nguyên miễn phí từ functor đó. Và đó chính xác là cách IObservableliên quan đến các đơn vị tự do - cụ thể là, chúng ta có thể xây dựng một đơn vị tự do từ đó IObservable.


Hiểu biết sâu sắc về lý thuyết thể loại! Tôi đã theo dõi một cái gì đó không nói về cách chúng được tạo ra, nhiều hơn về sự khác biệt khi xây dựng một kiến ​​trúc chức năng và mô hình hiệu ứng thành phần với một trong số chúng. FreeMonad có thể được sử dụng để xây dựng DSL cho các hoạt động hợp nhất, trong khi IObservables thiên về các giá trị rời rạc theo thời gian.
MLProgrammer-CiM

1
@ MLProgrammer-CiM, tôi sẽ xem liệu tôi có thể thêm một số thông tin chi tiết về điều đó trong vài ngày tới không.
Nathan Davis

Tôi thích một ví dụ thực tế về các đơn nguyên miễn phí
l --'''''--------- '' '' '' '' '' '' '
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.