Monad trong tiếng Anh đơn giản? (Dành cho lập trình viên OOP không có nền FP)


743

Theo thuật ngữ mà một lập trình viên OOP sẽ hiểu (không có bất kỳ nền tảng lập trình chức năng nào), một đơn nguyên là gì?

Vấn đề gì nó giải quyết và những nơi phổ biến nhất nó được sử dụng là gì?

BIÊN TẬP:

Để làm rõ loại hiểu biết mà tôi đang tìm kiếm, giả sử bạn đang chuyển đổi một ứng dụng FP có đơn vị thành ứng dụng OOP. Bạn sẽ làm gì để chuyển trách nhiệm của các đơn vị sang ứng dụng OOP?




10
@Pavel: Câu trả lời chúng tôi đã có dưới đây từ Eric là nhiều hơn những người thân trong những khác đề nghị Q cho những người có một nền OO (như trái ngược với một nền FP).
Donal Fellows

5
@Donal: Nếu đây bản dupe (về điều mà tôi không có ý kiến), câu trả lời hay nên được thêm vào bản gốc. Đó là: một câu trả lời tốt không loại trừ việc đóng như một bản sao. Nếu nó là một bản sao đủ gần, điều này có thể được người điều hành thực hiện dưới dạng hợp nhất.
dmckee --- ex-moderator mèo con

Câu trả lời:


732

CẬP NHẬT: Câu hỏi này là chủ đề của một loạt blog dài vô cùng, mà bạn có thể đọc tại Monads - cảm ơn vì câu hỏi tuyệt vời!

Theo thuật ngữ mà một lập trình viên OOP sẽ hiểu (không có bất kỳ nền tảng lập trình chức năng nào), một đơn nguyên là gì?

Một đơn nguyên là một "khuếch đại" của các loạituân theo quy tắc nhất địnhtrong đó có một số hoạt động cung cấp .

Đầu tiên, "bộ khuếch đại các loại" là gì? Điều đó có nghĩa là một số hệ thống cho phép bạn lấy một loại và biến nó thành một loại đặc biệt hơn. Ví dụ, trong C # xem xét Nullable<T>. Đây là một bộ khuếch đại của các loại. Nó cho phép bạn lấy một loại, nói intvà thêm một khả năng mới cho loại đó, cụ thể là bây giờ nó có thể là null khi không thể trước đây.

Như một ví dụ thứ hai, hãy xem xét IEnumerable<T>. Nó là một bộ khuếch đại của các loại. Nó cho phép bạn lấy một loại, giả sử stringvà thêm một khả năng mới cho loại đó, cụ thể là bây giờ bạn có thể tạo một chuỗi các chuỗi trong số bất kỳ chuỗi nào.

"Quy tắc nhất định" là gì? Tóm lại, có một cách hợp lý để các chức năng trên loại cơ bản hoạt động trên loại được khuếch đại sao cho chúng tuân theo các quy tắc thông thường của thành phần chức năng. Ví dụ: nếu bạn có hàm trên số nguyên, hãy nói

int M(int x) { return x + N(x * 2); }

sau đó, chức năng tương ứng trên Nullable<int>có thể làm cho tất cả các toán tử và các cuộc gọi trong đó hoạt động cùng nhau "theo cùng một cách" mà chúng đã làm trước đó.

(Điều đó cực kỳ mơ hồ và thiếu chính xác; bạn đã yêu cầu một lời giải thích mà không thừa nhận bất cứ điều gì về kiến ​​thức về thành phần chức năng.)

"Hoạt động" là gì?

  1. Có một hoạt động "đơn vị" (đôi khi khó hiểu được gọi là hoạt động "trả lại") lấy một giá trị từ một loại đơn giản và tạo ra giá trị đơn trị tương đương. Về bản chất, điều này cung cấp một cách để lấy giá trị của một loại không xác định và biến nó thành một giá trị của loại được khuếch đại. Nó có thể được thực hiện như là một hàm tạo trong ngôn ngữ OO.

  2. Có một hoạt động "liên kết" nhận một giá trị đơn trị và một hàm có thể biến đổi giá trị và trả về một giá trị đơn trị mới. Bind là hoạt động chính xác định ngữ nghĩa của đơn nguyên. Nó cho phép chúng ta chuyển đổi các hoạt động trên loại không thay đổi thành các hoạt động trên loại được khuếch đại, tuân theo các quy tắc của thành phần chức năng được đề cập trước đó.

  3. Thường có một cách để đưa loại không thay đổi trở lại loại được khuếch đại. Nói đúng ra, thao tác này không bắt buộc phải có một đơn nguyên. (Mặc dù điều đó là cần thiết nếu bạn muốn có một comonad . Chúng tôi sẽ không xem xét những điều đó hơn nữa trong bài viết này.)

Một lần nữa, lấy Nullable<T>một ví dụ. Bạn có thể biến một intthành một Nullable<int>với hàm tạo. Trình biên dịch C # đảm nhiệm hầu hết việc "nâng" vô giá trị cho bạn, nhưng nếu không, việc chuyển đổi nâng rất đơn giản: một thao tác, giả sử,

int M(int x) { whatever }

được chuyển thành

Nullable<int> M(Nullable<int> x) 
{ 
    if (x == null) 
        return null; 
    else 
        return new Nullable<int>(whatever);
}

Và biến một Nullable<int>trở lại thành một intđược thực hiện với Valuetài sản.

Đây là phép biến đổi hàm là bit chính. Lưu ý cách ngữ nghĩa thực tế của hoạt động nullable - rằng một hoạt động trên một nulltuyên truyền null- được nắm bắt trong chuyển đổi. Chúng ta có thể khái quát điều này.

Giả sử bạn có một chức năng từ intđến int, như bản gốc của chúng tôi M. Bạn có thể dễ dàng biến nó thành một hàm lấy intvà trả về Nullable<int>vì bạn chỉ có thể chạy kết quả thông qua hàm tạo nullable. Bây giờ giả sử bạn có phương pháp bậc cao này:

static Nullable<T> Bind<T>(Nullable<T> amplified, Func<T, Nullable<T>> func)
{
    if (amplified == null) 
        return null;
    else
        return func(amplified.Value);
}

Xem những gì bạn có thể làm với điều đó? Bất kỳ phương thức nào lấy intvà trả về int, hoặc lấy intvà trả về một Nullable<int>giờ đây có thể có ngữ nghĩa không thể áp dụng được cho nó .

Hơn nữa: giả sử bạn có hai phương pháp

Nullable<int> X(int q) { ... }
Nullable<int> Y(int r) { ... }

và bạn muốn soạn chúng:

Nullable<int> Z(int s) { return X(Y(s)); }

Đó là, Zlà thành phần của XY. Nhưng bạn không thể làm điều đó bởi vì Xmất một int, và Ytrả về a Nullable<int>. Nhưng vì bạn có thao tác "liên kết", bạn có thể thực hiện công việc này:

Nullable<int> Z(int s) { return Bind(Y(s), X); }

Hoạt động liên kết trên một đơn nguyên là những gì làm cho thành phần của các chức năng trên các loại khuếch đại hoạt động. Các "quy tắc" tôi viết tay ở trên là đơn nguyên bảo tồn các quy tắc của thành phần chức năng bình thường; việc soạn thảo với các chức năng nhận dạng dẫn đến chức năng ban đầu, thành phần đó là kết hợp, v.v.

Trong C #, "Bind" được gọi là "Chọn". Hãy xem cách nó hoạt động trên chuỗi đơn nguyên. Chúng ta cần có hai điều: biến một giá trị thành một chuỗi và liên kết các hoạt động trên các chuỗi. Như một phần thưởng, chúng tôi cũng có "biến một chuỗi trở lại thành một giá trị". Những hoạt động đó là:

static IEnumerable<T> MakeSequence<T>(T item)
{
    yield return item;
}
// Extract a value
static T First<T>(IEnumerable<T> sequence)
{
    // let's just take the first one
    foreach(T item in sequence) return item; 
    throw new Exception("No first item");
}
// "Bind" is called "SelectMany"
static IEnumerable<T> SelectMany<T>(IEnumerable<T> seq, Func<T, IEnumerable<T>> func)
{
    foreach(T item in seq)
        foreach(T result in func(item))
            yield return result;            
}

Quy tắc đơn nguyên nullable là "kết hợp hai hàm tạo nullable với nhau, kiểm tra xem liệu hàm bên trong có null hay không, nếu có, tạo null, nếu không, thì gọi hàm ngoài với kết quả". Đó là ngữ nghĩa mong muốn của nullable.

Quy tắc đơn nguyên chuỗi là "kết hợp hai hàm tạo ra các chuỗi với nhau, áp dụng hàm ngoài cho mọi phần tử được tạo bởi hàm bên trong và sau đó ghép tất cả các chuỗi kết quả lại với nhau". Các ngữ nghĩa cơ bản của các đơn nguyên được nắm bắt trong Bind/ SelectManyphương thức; đây là phương pháp mà sẽ cho bạn biết những gì các đơn nguyên thực sự có nghĩa .

Chúng ta có thể làm tốt hơn nữa. Giả sử bạn có một chuỗi ints và một phương thức lấy ints và kết quả là chuỗi chuỗi. Chúng ta có thể khái quát hóa hoạt động liên kết để cho phép cấu thành các hàm lấy và trả về các loại khuếch đại khác nhau, miễn là đầu vào của một khớp với đầu ra của hàm kia:

static IEnumerable<U> SelectMany<T,U>(IEnumerable<T> seq, Func<T, IEnumerable<U>> func)
{
    foreach(T item in seq)
        foreach(U result in func(item))
            yield return result;            
}

Vì vậy, bây giờ chúng ta có thể nói "khuếch đại bó số nguyên riêng lẻ này thành một chuỗi các số nguyên. Biến đổi số nguyên cụ thể này thành một chuỗi các chuỗi, được khuếch đại thành một chuỗi các chuỗi. Bây giờ đặt cả hai phép toán: khuếch đại bó số nguyên này thành nối của tất cả các chuỗi của chuỗi. " Monads cho phép bạn soạn thảo các khuếch đại của bạn.

Vấn đề gì nó giải quyết và những nơi phổ biến nhất nó được sử dụng là gì?

Điều đó giống như hỏi "mô hình đơn lẻ giải quyết vấn đề gì?", Nhưng tôi sẽ thử.

Monads thường được sử dụng để giải quyết các vấn đề như:

  • Tôi cần tạo các khả năng mới cho loại này và vẫn kết hợp các chức năng cũ trên loại này để sử dụng các khả năng mới.
  • Tôi cần phải nắm bắt một loạt các hoạt động trên các loại và biểu diễn các hoạt động đó như là các đối tượng có thể kết hợp, xây dựng các tác phẩm lớn hơn và lớn hơn cho đến khi tôi có một loạt các hoạt động phù hợp, và sau đó tôi cần bắt đầu nhận được kết quả
  • Tôi cần thể hiện các hoạt động tác dụng phụ một cách sạch sẽ bằng ngôn ngữ ghét tác dụng phụ

C # sử dụng các đơn nguyên trong thiết kế của nó. Như đã đề cập, mô hình nullable rất giống với "có thể là đơn nguyên". LINQ hoàn toàn được xây dựng từ các đơn nguyên; các SelectManyphương pháp là những gì hiện các công việc ngữ nghĩa của thành phần hoạt động. (Erik Meijer thích chỉ ra rằng mọi chức năng LINQ thực sự có thể được thực hiện bởi SelectMany; mọi thứ khác chỉ là sự tiện lợi.)

Để làm rõ loại hiểu biết mà tôi đang tìm kiếm, giả sử bạn đang chuyển đổi một ứng dụng FP có đơn vị thành ứng dụng OOP. Bạn sẽ làm gì để chuyển trách nhiệm của các đơn vị vào ứng dụng OOP?

Hầu hết các ngôn ngữ OOP không có hệ thống loại đủ phong phú để thể hiện trực tiếp mẫu đơn nguyên; bạn cần một hệ thống loại hỗ trợ các loại là loại cao hơn các loại chung. Vì vậy, tôi sẽ không cố gắng để làm điều đó. Thay vào đó, tôi sẽ triển khai các loại chung đại diện cho từng đơn nguyên và triển khai các phương thức đại diện cho ba thao tác bạn cần: biến một giá trị thành giá trị khuếch đại, (có thể) biến một giá trị được khuếch đại thành một giá trị và biến một hàm trên các giá trị không thể thay đổi thành một hàm trên các giá trị khuếch đại.

Một nơi tốt để bắt đầu là cách chúng tôi triển khai LINQ trong C #. Nghiên cứu SelectManyphương pháp; đó là chìa khóa để hiểu cách thức chuỗi đơn vị hoạt động trong C #. Đó là một phương pháp rất đơn giản, nhưng rất mạnh mẽ!


Đề nghị, đọc thêm:

  1. Để giải thích sâu hơn và lý thuyết hợp lý hơn về các đơn vị trong C #, tôi rất khuyến nghị bài viết của đồng nghiệp Wes Lippert của tôi về chủ đề này. Bài viết này là những gì giải thích các đơn nguyên cho tôi khi cuối cùng họ "nhấp chuột" cho tôi.
  2. Một minh họa tốt về lý do tại sao bạn có thể muốn có một đơn nguyên xung quanh (sử dụng Haskell trong các ví dụ của nó) .
  3. Sắp xếp, "dịch" bài viết trước sang JavaScript.


17
Đây là một câu trả lời tuyệt vời, nhưng đầu tôi đã đi lên. Tôi sẽ theo dõi và nhìn chằm chằm vào nó vào cuối tuần này và hỏi bạn những câu hỏi nếu mọi thứ không lắng xuống và có ý nghĩa trong đầu tôi.
Paul Nathan

5
Giải thích tuyệt vời như bình thường Eric. Để thảo luận nhiều hơn về lý thuyết (nhưng vẫn rất thú vị), tôi đã thấy bài đăng trên blog của Bart De Smet trên MinLINQ hữu ích trong việc liên quan đến một số cấu trúc lập trình chức năng trở lại C #. Community.bartdesmet.net/bloss/bart/archive/2010/01/01/ từ
Ron Warholic

41
Nó có ý nghĩa hơn đối với tôi để nói rằng nó làm tăng các loại hơn là khuếch đại chúng.
Gabe

61
@slomojo: và tôi đã thay đổi nó trở lại những gì tôi đã viết và dự định viết. Nếu bạn và Gabe muốn viết câu trả lời của riêng bạn, bạn hãy tiếp tục.
Eric Lippert

24
@Eric, Tất nhiên tùy thuộc vào bạn, nhưng Bộ khuếch đại ngụ ý rằng các thuộc tính hiện có được tăng cường, gây hiểu lầm.
ocodo

341

Tại sao chúng ta cần các đơn nguyên?

  1. Chúng tôi muốn lập trình chỉ sử dụng các chức năng . ("lập trình chức năng" sau tất cả -FP).
  2. Sau đó, chúng tôi có một vấn đề lớn đầu tiên. Đây là một chương trình:

    f(x) = 2 * x

    g(x,y) = x / y

    Làm thế nào chúng ta có thể nói những gì sẽ được thực hiện đầu tiên ? Làm thế nào chúng ta có thể hình thành một chuỗi các hàm theo thứ tự (tức là một chương trình ) bằng cách sử dụng không nhiều hơn các hàm?

    Giải pháp: soạn các hàm . Nếu bạn muốn đầu tiên gvà sau đó f, chỉ cần viết f(g(x,y)). Được rồi nhưng ...

  3. Nhiều vấn đề hơn: một số chức năng có thể không thành công (nghĩa là g(2,0)chia cho 0). Chúng tôi không có "ngoại lệ" trong FP . Làm thế nào để chúng ta giải quyết nó?

    Giải pháp: Chúng ta hãy cho phép các hàm trả về hai loại : thay vì có g : Real,Real -> Real(hàm từ hai thực thành thực), hãy cho phép g : Real,Real -> Real | Nothing(hàm từ hai thực thành (thực hoặc không có gì)).

  4. Nhưng các hàm nên (đơn giản hơn) chỉ trả về một thứ .

    Giải pháp: chúng ta hãy tạo ra một loại dữ liệu mới sẽ được trả về, một " kiểu đấm bốc " có thể là thật hoặc đơn giản là không có gì. Do đó, chúng ta có thể có g : Real,Real -> Maybe Real. Được rồi nhưng ...

  5. Điều gì xảy ra bây giờ f(g(x,y))? fchưa sẵn sàng để tiêu thụ a Maybe Real. Và, chúng tôi không muốn thay đổi mọi chức năng mà chúng tôi có thể kết nối gđể tiêu thụ a Maybe Real.

    Giải pháp: chúng ta hãy có một chức năng đặc biệt để "kết nối" / "soạn thảo" / "liên kết" các chức năng . Bằng cách đó, chúng ta có thể, đằng sau hậu trường, điều chỉnh đầu ra của một chức năng để cung cấp chức năng sau.

    Trong trường hợp của chúng tôi: g >>= f(kết nối / soạn gthành f). Chúng tôi muốn >>=nhận gđầu ra, kiểm tra nó và, trong trường hợp đó Nothingchỉ là không gọi fvà trả lại Nothing; hoặc ngược lại, giải nén hộp Realvà cho fnó ăn . (Thuật toán này chỉ là việc thực hiện >>=cho Maybeloại).

  6. Nhiều vấn đề khác phát sinh có thể được giải quyết bằng cách sử dụng cùng một mẫu này: 1. Sử dụng "hộp" để mã hóa / lưu trữ các ý nghĩa / giá trị khác nhau và có các hàm như gtrả về các "giá trị được đóng hộp" đó. 2. Có nhà soạn nhạc / trình liên kết g >>= fđể giúp kết nối gđầu ra fcủa đầu vào với đầu vào, vì vậy chúng tôi không phải thay đổi fgì cả.

  7. Các vấn đề đáng chú ý có thể được giải quyết bằng kỹ thuật này là:

    • có trạng thái toàn cầu mà mọi chức năng trong chuỗi chức năng ("chương trình") có thể chia sẻ: giải pháp StateMonad.

    • Chúng tôi không thích "hàm không tinh khiết": các hàm mang lại đầu ra khác nhau cho cùng một đầu vào. Do đó, hãy đánh dấu các hàm đó, làm cho chúng trả về giá trị được gắn thẻ / đóng hộp: IOđơn nguyên.

Hạnh phúc trọn vẹn !!!!


2
@DmitriZaitsev Các ngoại lệ chỉ có thể xảy ra trong "mã không tinh khiết" (đơn vị IO) theo như tôi biết.
cibercitizen1

3
@DmitriZaitsev Vai trò của Không có gì có thể được chơi bởi bất kỳ loại nào khác (khác với Real dự kiến). Đó không phải là vấn đề. Trong ví dụ, vấn đề là làm thế nào để điều chỉnh các chức năng trong chuỗi khi loại trước có thể trả về loại giá trị không mong muốn cho loại sau, mà không xâu chuỗi thứ hai (chỉ chấp nhận Real làm đầu vào).
cibercitizen1

3
Một điểm nhầm lẫn khác là từ "monad" chỉ xuất hiện hai lần trong câu trả lời của bạn và chỉ kết hợp với các thuật ngữ khác - StateIO, không có nghĩa nào trong số chúng cũng như ý nghĩa chính xác của "monad" được đưa ra
Dmitri Zaitsev

31
Đối với tôi, một người đến từ nền tảng OOP, câu trả lời này thực sự giải thích rõ động lực đằng sau việc có một đơn nguyên và cũng là những gì đơn vị thực sự là (nhiều hơn một câu trả lời được chấp nhận). Vì vậy, tôi thấy nó rất hữu ích. Cảm ơn rất nhiều @ cibercitizen1 và +1
không có

3
Tôi đã đọc về lập trình chức năng trong khoảng một năm. Câu trả lời này, và đặc biệt là hai điểm đầu tiên, cuối cùng đã khiến tôi hiểu lập trình mệnh lệnh thực sự có nghĩa là gì, và tại sao lập trình chức năng lại khác. Cảm ơn bạn!
jrahhali

82

Tôi muốn nói rằng sự tương tự OO gần nhất với các đơn nguyên là " mẫu lệnh ".

Trong mẫu lệnh bạn bao bọc một câu lệnh hoặc biểu thức thông thường trong một đối tượng lệnh . Đối tượng lệnh phơi bày một thực hiện phương pháp đó thực hiện báo cáo kết quả bọc. Vì vậy, câu lệnh được biến thành các đối tượng hạng nhất có thể chuyển qua và thực hiện theo ý muốn. Các lệnh có thể được soạn thảo để bạn có thể tạo một đối tượng chương trình bằng cách xâu chuỗi và lồng các đối tượng lệnh.

Các lệnh được thực thi bởi một đối tượng riêng biệt, invoker . Lợi ích của việc sử dụng mẫu lệnh (thay vì chỉ thực hiện một loạt các câu lệnh thông thường) là những người gọi khác nhau có thể áp dụng logic khác nhau cho cách thực hiện các lệnh.

Mẫu lệnh có thể được sử dụng để thêm (hoặc xóa) các tính năng ngôn ngữ không được ngôn ngữ máy chủ hỗ trợ. Ví dụ: trong ngôn ngữ OO giả định không có ngoại lệ, bạn có thể thêm ngữ nghĩa ngoại lệ bằng cách hiển thị các phương thức "thử" và "ném" vào các lệnh. Khi một lệnh gọi ném, kẻ xâm lược sẽ quay lại danh sách (hoặc cây) của lệnh cho đến lần gọi "thử" cuối cùng. Ngược lại, bạn có thể loại bỏ ngữ nghĩa ngoại lệ khỏi một ngôn ngữ (nếu bạn cho rằng ngoại lệ là xấu ) bằng cách bắt tất cả các ngoại lệ được ném bởi mỗi lệnh riêng lẻ và biến chúng thành mã lỗi sau đó được chuyển sang lệnh tiếp theo.

Thậm chí các ngữ nghĩa thực thi ưa thích hơn như giao dịch, thực thi không xác định hoặc tiếp tục có thể được thực hiện như thế này bằng ngôn ngữ không hỗ trợ nguyên bản. Đó là một mô hình khá mạnh mẽ nếu bạn nghĩ về nó.

Bây giờ trong thực tế, các mẫu lệnh không được sử dụng như một tính năng ngôn ngữ chung như thế này. Chi phí hoạt động của việc biến mỗi câu lệnh thành một lớp riêng biệt sẽ dẫn đến một lượng mã soạn sẵn không chịu nổi. Nhưng về nguyên tắc, nó có thể được sử dụng để giải quyết các vấn đề tương tự như các đơn nguyên được sử dụng để giải quyết trong fp.


15
Tôi tin rằng đây là lời giải thích đơn nguyên đầu tiên tôi thấy không dựa vào các khái niệm lập trình chức năng và đưa nó vào các thuật ngữ OOP thực sự. Câu trả lời thực sự tốt.
David K. Hess

đây là rất gần với những gì các đơn vị thực sự có trong FP / Haskell, ngoại trừ việc các đối tượng lệnh "biết" chúng thuộc về "logic gọi" nào (và chỉ những chuỗi tương thích mới có thể được nối với nhau); invoker chỉ cung cấp giá trị đầu tiên. Nó không giống như lệnh "In" có thể được thực thi bởi "logic thực thi không xác định". Không, nó phải là "logic I / O" (tức là đơn nguyên IO). Nhưng khác hơn, nó rất gần. Bạn thậm chí có thể nói rằng Monads chỉ là Chương trình (được xây dựng bởi Báo cáo mã, sẽ được thực hiện sau đó). Trong những ngày đầu, "ràng buộc" được gọi là "dấu chấm phẩy lập trình" .
Will Ness

1
@ DavidK. Tôi thực sự rất hoài nghi về các câu trả lời sử dụng FP để giải thích các khái niệm cơ bản của FP và đặc biệt là các câu trả lời sử dụng ngôn ngữ FP như Scala. Làm tốt lắm, JacquesB!
Phục hồi lại

62

Theo thuật ngữ mà một lập trình viên OOP sẽ hiểu (không có bất kỳ nền tảng lập trình chức năng nào), một đơn nguyên là gì?

Vấn đề nào nó giải quyết và những nơi phổ biến nhất mà nó được sử dụng là những nơi phổ biến nhất mà nó được sử dụng?

Về mặt lập trình OO, một đơn vị là một giao diện (hoặc nhiều khả năng là một mixin), được tham số hóa bằng một loại, với hai phương thức returnbindmô tả:

  • Làm thế nào để tiêm một giá trị để có được một giá trị đơn trị của loại giá trị được tiêm đó;
  • Làm thế nào để sử dụng một hàm tạo ra một giá trị đơn âm từ một hàm không đơn trị, trên một giá trị đơn âm.

Vấn đề mà nó giải quyết là cùng một loại vấn đề mà bạn mong đợi từ bất kỳ giao diện nào, cụ thể là, "Tôi có một nhóm các lớp khác nhau làm những việc khác nhau, nhưng dường như làm những điều khác nhau theo cách có sự tương đồng cơ bản. Tôi có thể mô tả sự tương đồng giữa chúng không, ngay cả khi bản thân các lớp không thực sự là kiểu con của bất cứ thứ gì gần hơn so với chính lớp 'Đối tượng'? "

Cụ thể hơn, Monad"giao diện" tương tự IEnumeratorhoặc IIteratortrong đó nó có một loại mà chính nó có một loại. "Điểm" chính Monadmặc dù là có thể kết nối các hoạt động dựa trên loại bên trong, thậm chí đến mức có một "loại nội bộ" mới, trong khi vẫn giữ - hoặc thậm chí tăng cường - cấu trúc thông tin của lớp chính.


1
returnthực sự sẽ không phải là một phương thức trên đơn nguyên, bởi vì nó không lấy một thể hiện đơn nguyên làm đối số. (tức là: không có cái này / cái tôi)
Laurence Gonsalves

@LaurenceGonsalves: Vì tôi hiện đang xem xét vấn đề này cho luận án cử nhân của tôi, tôi nghĩ điều chủ yếu hạn chế là thiếu phương thức tĩnh trong các giao diện trong C # / Java. Bạn có thể có được một cách xa trong hướng thực hiện toàn bộ câu chuyện đơn nguyên, ít nhất là bị ràng buộc tĩnh thay vì dựa trên các kiểu chữ. Thật thú vị, điều này thậm chí sẽ hoạt động mặc dù thiếu các loại tốt hơn.
Sebastian Graf

42

Bạn có một bài thuyết trình gần đây " Monadologie - trợ giúp chuyên nghiệp về loại lo âu " của Christopher League (ngày 12 tháng 7 năm 2010), khá thú vị về các chủ đề tiếp tục và đơn nguyên.
Video đi kèm với bài thuyết trình (trình chiếu) này thực sự có sẵn tại vimeo .
Phần Monad bắt đầu khoảng 37 phút, trên video một giờ này và bắt đầu với slide 42 trong số 58 bản trình bày slide của nó.

Nó được trình bày là "mẫu thiết kế hàng đầu cho lập trình chức năng", nhưng ngôn ngữ được sử dụng trong các ví dụ là Scala, cả OOP và chức năng.
Bạn có thể đọc thêm về Monad in Scala trong bài đăng trên blog " Monads - Một cách khác để tính toán trừu tượng trong Scala ", từ Debasish Ghosh (27 tháng 3 năm 2008).

Một constructor loại M là một đơn nguyên nếu nó hỗ trợ các hoạt động này:

# the return function
def unit[A] (x: A): M[A]

# called "bind" in Haskell 
def flatMap[A,B] (m: M[A]) (f: A => M[B]): M[B]

# Other two can be written in term of the first two:

def map[A,B] (m: M[A]) (f: A => B): M[B] =
  flatMap(m){ x => unit(f(x)) }

def andThen[A,B] (ma: M[A]) (mb: M[B]): M[B] =
  flatMap(ma){ x => mb }

Vì vậy, ví dụ (trong Scala):

  • Option là một đơn nguyên
    đơn vị def [A] (x: A): Tùy chọn [A] = Một số (x)

    def FlatMap [A, B] (m: Tùy chọn [A]) (f: A => Tùy chọn [B]): Tùy chọn [B] =
      m khớp {
       trường hợp Không => Không
       trường hợp Một số (x) => f (x)
      }
  • List là đơn nguyên
    đơn vị def [A] (x: A): Danh sách [A] = Danh sách (x)

    def FlatMap [A, B] (m: List [A]) (f: A => List [B]): List [B] =
      m khớp {
        trường hợp Nil => Không
        trường hợp x :: xs => f (x) ::: FlatMap (xs) (f)
      }

Monad là một vấn đề lớn trong Scala vì cú pháp thuận tiện được xây dựng để tận dụng các cấu trúc Monad:

forhiểu trong Scala :

for {
  i <- 1 to 4
  j <- 1 to i
  k <- 1 to j
} yield i*j*k

được dịch bởi trình biên dịch sang:

(1 to 4).flatMap { i =>
  (1 to i).flatMap { j =>
    (1 to j).map { k =>
      i*j*k }}}

Sự trừu tượng hóa chính là flatMap, liên kết tính toán thông qua chuỗi.
Mỗi lệnh gọi flatMaptrả về cùng một kiểu cấu trúc dữ liệu (nhưng có giá trị khác nhau), đóng vai trò là đầu vào cho lệnh tiếp theo trong chuỗi.

Trong đoạn trích trên, FlatMap lấy đầu vào là một bao đóng (SomeType) => List[AnotherType]và trả về a List[AnotherType]. Điểm quan trọng cần lưu ý là tất cả các FlatMaps đều có cùng kiểu đóng như đầu vào và trả về cùng loại với đầu ra.

Đây là những gì "liên kết" chuỗi tính toán - mọi mục của chuỗi trong phần hiểu để phải tôn trọng ràng buộc cùng loại này.


Nếu bạn thực hiện hai thao tác (có thể thất bại) và chuyển kết quả cho lần thứ ba, như:

lookupVenue: String => Option[Venue]
getLoggedInUser: SessionID => Option[User]
reserveTable: (Venue, User) => Option[ConfNo]

nhưng không tận dụng Monad, bạn sẽ nhận được mã OOP phức tạp như:

val user = getLoggedInUser(session)
val confirm =
  if(!user.isDefined) None
  else lookupVenue(name) match {
    case None => None
    case Some(venue) =>
      val confno = reserveTable(venue, user.get)
      if(confno.isDefined)
        mailTo(confno.get, user.get)
      confno
  }

trong khi đó với Monad, bạn có thể làm việc với các loại thực tế ( Venue, User) giống như tất cả các thao tác hoạt động và giữ công cụ xác minh Tùy chọn ẩn, tất cả chỉ vì các bản đồ phẳng của cú pháp:

val confirm = for {
  venue <- lookupVenue(name)
  user <- getLoggedInUser(session)
  confno <- reserveTable(venue, user)
} yield {
  mailTo(confno, user)
  confno
}

Phần năng suất sẽ chỉ được thực hiện nếu cả ba hàm có Some[X]; bất kỳ Nonesẽ trực tiếp trở lại confirm.


Vì thế:

Monads cho phép tính toán theo thứ tự trong Lập trình chức năng, cho phép chúng ta mô hình hóa trình tự các hành động theo một cấu trúc đẹp, hơi giống như DSL.

Và sức mạnh lớn nhất đi kèm với khả năng kết hợp các đơn vị phục vụ các mục đích khác nhau, thành các bản tóm tắt mở rộng trong một ứng dụng.

Trình tự và xâu chuỗi các hành động này của một đơn nguyên được thực hiện bởi trình biên dịch ngôn ngữ thực hiện chuyển đổi thông qua phép thuật của các bao đóng.


Nhân tiện, Monad không chỉ là mô hình tính toán được sử dụng trong FP:

Lý thuyết phạm trù đề xuất nhiều mô hình tính toán. Trong số đó

  • mô hình mũi tên tính toán
  • mô hình tính toán Monad
  • mô hình ứng dụng tính toán

2
Tôi thích lời giải thích này! Ví dụ bạn đã đưa ra minh họa khái niệm tuyệt đẹp và cũng thêm những gì IMHO còn thiếu trong lời trêu ghẹo của Eric về ChọnMany () là một Monad. Thx cho điều này!
aoven

1
IMHO đây là câu trả lời tao nhã nhất
Polymerase

và trước mọi thứ khác, Functor.
Will Ness

34

Để tôn trọng người đọc nhanh, trước tiên tôi bắt đầu với định nghĩa chính xác, tiếp tục với phần giải thích "tiếng Anh đơn giản" nhanh hơn và sau đó chuyển sang các ví dụ.

Dưới đây là một định nghĩa ngắn gọn và chính xác được điều chỉnh lại một chút:

Một đơn nguyên (trong khoa học máy tính) chính thức là một bản đồ:

  • gửi mọi loại Xngôn ngữ lập trình đã cho sang một loại mới T(X)(được gọi là "loại tính Ttoán có giá trị trong X");

  • được trang bị một quy tắc để soạn hai chức năng của biểu mẫu f:X->T(Y)g:Y->T(Z)cho một chức năng g∘f:X->T(Z);

  • theo cách liên kết theo nghĩa hiển nhiên và unital đối với một hàm đơn vị nhất định được gọi pure_X:X->T(X), được coi là lấy một giá trị cho phép tính thuần túy trả về giá trị đó.

Vì vậy, nói một cách đơn giản, một đơn nguyên là một quy tắc để chuyển từ bất kỳ loại nào Xsang loại khácT(X) và một quy tắc để chuyển từ hai chức năng f:X->T(Y)g:Y->T(Z)(mà bạn muốn soạn nhưng không thể) cho một chức năng mớih:X->T(Z) . Mà, tuy nhiên, không phải là thành phần trong ý nghĩa toán học nghiêm ngặt. Về cơ bản chúng ta là "uốn" thành phần của hàm hoặc xác định lại cách các hàm được tạo.

Thêm vào đó, chúng tôi yêu cầu quy tắc sáng tác của đơn nguyên để đáp ứng các tiên đề toán học "hiển nhiên":

  • Tính kết hợp : Soạn fvới gvà sau đó với h(từ bên ngoài) phải giống như sáng tác gvới hvà sau đó với f(từ bên trong).
  • Tài sản Unital : Sáng tác fvới chức năng nhận dạng ở hai bên sẽ mang lại f.

Một lần nữa, nói một cách đơn giản, chúng ta không thể điên cuồng định nghĩa lại thành phần chức năng của mình như chúng ta muốn:

  • Trước tiên chúng ta cần tính kết hợp để có thể soạn một số hàm liên tiếp f(g(h(k(x))), ví dụ , và không phải lo lắng về việc chỉ định các cặp hàm tổng hợp thứ tự. Vì quy tắc đơn nguyên chỉ quy định cách tạo một cặp hàm , không có tiên đề đó, chúng ta sẽ cần biết cặp nào được tạo trước, v.v. (Lưu ý rằng khác với thuộc tính giao hoán được fcấu thành ggiống như được gcấu thành với f, không bắt buộc).
  • Và thứ hai, chúng ta cần tài sản unital, đơn giản chỉ cần nói rằng danh tính sáng tác tầm thường theo cách chúng ta mong đợi. Vì vậy, chúng ta có thể tái cấu trúc một cách an toàn bất cứ khi nào những danh tính đó có thể được trích xuất.

Vì vậy, một lần nữa tóm tắt: Một đơn nguyên là quy tắc mở rộng loại và các hàm tổng hợp thỏa mãn hai tiên đề - tính kết hợp và thuộc tính unital.

Trong điều kiện thực tế, bạn muốn đơn nguyên được triển khai cho bạn bằng ngôn ngữ, trình biên dịch hoặc khung sẽ đảm nhiệm việc soạn thảo các hàm cho bạn. Vì vậy, bạn có thể tập trung vào việc viết logic của hàm thay vì lo lắng về cách thực thi của chúng.

Đó là bản chất của nó, một cách ngắn gọn.


Là nhà toán học chuyên nghiệp, tôi thích tránh gọi h"thành phần" của fg. Bởi vì về mặt toán học, nó không phải là. Gọi nó là "thành phần" giả định không chính xác đó hlà thành phần toán học thực sự, mà nó không phải là. Nó thậm chí không được xác định duy nhất bởi fg. Thay vào đó, nó là kết quả của "quy tắc sáng tác" mới của đơn nguyên của chúng tôi. Mà có thể hoàn toàn khác với thành phần toán học thực tế ngay cả khi cái sau tồn tại!


Để làm cho nó bớt khô hơn, hãy để tôi thử minh họa bằng ví dụ rằng tôi đang chú thích với các phần nhỏ, vì vậy bạn có thể bỏ qua ngay đến điểm.

Ngoại lệ ném như ví dụ Monad

Giả sử chúng ta muốn soạn hai hàm:

f: x -> 1 / x
g: y -> 2 * y

Nhưng f(0)không được xác định, vì vậy một ngoại lệ eđược ném. Sau đó, làm thế nào bạn có thể xác định giá trị thành phần g(f(0))? Ném một ngoại lệ một lần nữa, tất nhiên! Có lẽ giống nhau e. Có thể một ngoại lệ mới được cập nhật e1.

Chính xác thì chuyện gì xảy ra ở đây? Đầu tiên, chúng ta cần (các) giá trị ngoại lệ mới (khác nhau hoặc giống nhau). Bạn có thể gọi chúng nothinghoặc nullbất cứ điều gì nhưng bản chất vẫn giống nhau - chúng phải là các giá trị mới, ví dụ: nó không nên là một numberví dụ của chúng tôi ở đây. Tôi không muốn gọi cho họ nullđể tránh nhầm lẫn với cách nullthực hiện bằng bất kỳ ngôn ngữ cụ thể nào. Tương tự, tôi thích tránh nothingvì nó thường được liên kết với null, về nguyên tắc, đó là điều nullnên làm, tuy nhiên, nguyên tắc đó thường bị bẻ cong vì bất kỳ lý do thực tế nào.

Chính xác ngoại lệ là gì?

Đây là một vấn đề không quan trọng đối với bất kỳ lập trình viên có kinh nghiệm nào nhưng tôi muốn bỏ vài từ chỉ để dập tắt bất kỳ con sâu nào của sự nhầm lẫn:

Ngoại lệ là một đối tượng đóng gói thông tin về cách kết quả thực hiện không hợp lệ xảy ra.

Điều này có thể bao gồm từ bỏ đi bất kỳ chi tiết nào và trả về một giá trị toàn cầu duy nhất (như NaNhoặc null) hoặc tạo danh sách nhật ký dài hoặc chính xác những gì đã xảy ra, gửi nó đến cơ sở dữ liệu và sao chép tất cả trên lớp lưu trữ dữ liệu phân tán;)

Sự khác biệt quan trọng giữa hai ví dụ cực đoan về ngoại lệ này là trong trường hợp đầu tiên không có tác dụng phụ . Trong cái thứ hai có. Điều này đưa chúng ta đến câu hỏi (ngàn đô la):

Là ngoại lệ được phép trong các chức năng thuần túy?

Câu trả lời ngắn hơn : Có, nhưng chỉ khi chúng không dẫn đến tác dụng phụ.

Câu trả lời dài hơn. Để được thuần túy, đầu ra của hàm của bạn phải được xác định duy nhất bởi đầu vào của nó. Vì vậy, chúng tôi sửa đổi chức năng fcủa mình bằng cách gửi 0đến giá trị trừu tượng mới emà chúng tôi gọi là ngoại lệ. Chúng tôi đảm bảo rằng giá trị ekhông chứa thông tin bên ngoài không được xác định duy nhất bởi đầu vào của chúng tôi x. Vì vậy, đây là một ví dụ về ngoại lệ mà không có tác dụng phụ:

e = {
  type: error, 
  message: 'I got error trying to divide 1 by 0'
}

Và đây là một tác dụng phụ:

e = {
  type: error, 
  message: 'Our committee to decide what is 1/0 is currently away'
}

Trên thực tế, nó chỉ có tác dụng phụ nếu thông điệp đó có thể thay đổi trong tương lai. Nhưng nếu nó được đảm bảo không bao giờ thay đổi, giá trị đó sẽ trở thành dự đoán duy nhất và do đó không có tác dụng phụ.

Để làm cho nó thậm chí sillier. Một chức năng trở lại 42bao giờ rõ ràng là tinh khiết. Nhưng nếu ai đó điên rồ quyết định tạo 42một biến có giá trị có thể thay đổi, thì chính hàm đó sẽ ngừng hoàn toàn trong các điều kiện mới.

Lưu ý rằng tôi đang sử dụng ký hiệu nghĩa đen cho đơn giản để thể hiện bản chất. Thật không may, mọi thứ bị rối tung trong các ngôn ngữ như JavaScript, errorkhông phải là loại hành xử theo cách chúng ta muốn ở đây đối với thành phần chức năng, trong khi các loại thực tế thích nullhoặc NaNkhông hành xử theo cách này mà chỉ đi qua một số giả tạo và không phải lúc nào cũng trực quan chuyển đổi loại.

Kiểu mở rộng

Vì chúng tôi muốn thay đổi thông điệp bên trong ngoại lệ của mình, chúng tôi thực sự đang khai báo một loại mới Echo toàn bộ đối tượng ngoại lệ và đó là những gì maybe numbernó làm, ngoài tên khó hiểu của nó, là loại numberhoặc loại ngoại lệ mới E, vì vậy nó thực sự là sự kết hợp number | Ecủa numberE. Cụ thể, nó phụ thuộc vào cách chúng tôi muốn xây dựng E, không được đề xuất cũng như không được phản ánh trong tên maybe number.

Thành phần chức năng là gì?

Đây là toán học chức năng hoạt động lấy f: X -> Yg: Y -> Zvà xây dựng thành phần của họ như là chức năng h: X -> Zthỏa mãn h(x) = g(f(x)). Vấn đề với định nghĩa này xảy ra khi kết quả f(x)không được phép làm đối số của g.

Trong toán học, các hàm này không thể được tạo mà không cần làm thêm. Giải pháp toán học nghiêm ngặt cho ví dụ trên của chúng tôi về fglà loại bỏ 0khỏi tập hợp định nghĩa của f. Với bộ định nghĩa mới (loại hạn chế mới hơn x), ftrở nên có thể kết hợp với g.

Tuy nhiên, nó không thực tế trong lập trình để hạn chế tập hợp định nghĩa fnhư thế. Thay vào đó, ngoại lệ có thể được sử dụng.

Hoặc như cách tiếp cận khác, các giá trị nhân tạo được tạo ra như NaN, undefined, null, Infinityvv Vì vậy, bạn đánh giá 1/0để Infinity1/-0để -Infinity. Và sau đó buộc giá trị mới trở lại vào biểu thức của bạn thay vì ném ngoại lệ. Dẫn đến kết quả bạn có thể hoặc không thể dự đoán được:

1/0                // => Infinity
parseInt(Infinity) // => NaN
NaN < 0            // => false
false + 1          // => 1

Và chúng tôi trở lại những con số thông thường sẵn sàng để tiếp tục;)

JavaScript cho phép chúng tôi tiếp tục thực hiện các biểu thức số bằng mọi giá mà không đưa ra các lỗi như trong ví dụ trên. Điều đó có nghĩa là, nó cũng cho phép soạn thảo các chức năng. Đó chính xác là những gì đơn nguyên nói về - đó là một quy tắc để soạn các hàm thỏa mãn các tiên đề như được định nghĩa ở đầu câu trả lời này.

Nhưng liệu quy tắc soạn thảo hàm, phát sinh từ việc triển khai JavaScript để xử lý các lỗi số, có phải là một đơn vị không?

Để trả lời câu hỏi này, tất cả những gì bạn cần là kiểm tra các tiên đề (còn lại là bài tập không phải là một phần của câu hỏi ở đây;).

Có thể ném ngoại lệ để xây dựng một đơn nguyên?

Thật vậy, một đơn nguyên hữu ích hơn thay vào đó sẽ là quy tắc quy định rằng nếu fném ngoại lệ cho một số người x, thì thành phần của nó với bất kỳ g. Cộng với việc tạo ra ngoại lệ Eduy nhất trên toàn cầu chỉ với một giá trị có thể có ( đối tượng đầu cuối trong lý thuyết danh mục). Bây giờ hai tiên đề có thể kiểm tra được ngay lập tức và chúng ta có được một đơn nguyên rất hữu ích. Và kết quả là những gì được biết đến như là đơn nguyên .


3
Đóng góp tốt. +1 Nhưng có lẽ bạn sẽ muốn xóa "đã tìm thấy hầu hết các giải thích quá lâu ..." là của bạn lâu nhất. Những người khác sẽ đánh giá nếu đó là "tiếng Anh đơn giản" theo yêu cầu của câu hỏi: "tiếng Anh đơn giản == bằng những từ đơn giản, theo cách đơn giản".
cibercitizen1

@ cibercitizen1 Cảm ơn! Nó thực sự ngắn, nếu bạn không đếm ví dụ. Điểm chính là bạn không cần phải đọc ví dụ để hiểu định nghĩa . Thật không may, nhiều lời giải thích buộc tôi phải đọc các ví dụ trước , điều này thường không cần thiết, nhưng, tất nhiên, có thể yêu cầu thêm công việc cho người viết. Với quá nhiều sự phụ thuộc vào các ví dụ cụ thể, có một mối nguy hiểm là các chi tiết không quan trọng che khuất bức tranh và khiến nó khó nắm bắt hơn. Có nói rằng, bạn có điểm hợp lệ, xem cập nhật.
Dmitri Zaitsev

2
quá dài và khó hiểu
sawimurugan

1
@seenimurugan Đề xuất cải tiến được chào đón;)
Dmitri Zaitsev

26

Một đơn vị là một kiểu dữ liệu đóng gói một giá trị và về cơ bản, hai thao tác có thể được áp dụng:

  • return x tạo ra một giá trị của loại đơn nguyên đóng gói x
  • m >>= f(đọc nó là "toán tử liên kết") áp dụng hàm fcho giá trị trong đơn nguyênm

Đó là những gì một đơn nguyên. Có một vài kỹ thuật nữa , nhưng về cơ bản hai thao tác đó xác định một đơn nguyên. Câu hỏi thực sự là, "Những gì một đơn vị làm gì?", Và điều đó phụ thuộc vào đơn vị - danh sách là các đơn vị, Maybes là các đơn vị, hoạt động IO là các đơn vị. Tất cả điều đó có nghĩa là khi chúng ta nói những điều đó là đơn nguyên là chúng có giao diện đơn nguyên return>>=.


Những gì một đơn vị làm, và điều đó phụ thuộc vào đơn vị: và chính xác hơn, điều đó phụ thuộc vào bindchức năng phải được xác định cho từng loại đơn nguyên, phải không? Đó sẽ là một lý do tốt để không nhầm lẫn liên kết với thành phần, vì có một định nghĩa duy nhất cho thành phần, trong khi không thể chỉ có một định nghĩa duy nhất cho chức năng liên kết, có một loại cho mỗi loại đơn âm, nếu tôi hiểu chính xác.
Hibou57

14

Từ wikipedia :

Trong lập trình chức năng, một đơn nguyên là một loại dữ liệu trừu tượng được sử dụng để biểu diễn các tính toán (thay vì dữ liệu trong mô hình miền). Các đơn vị cho phép lập trình viên xâu chuỗi các hành động lại với nhau để xây dựng một đường ống dẫn, trong đó mỗi hành động được trang trí với các quy tắc xử lý bổ sung do đơn vị cung cấp. Các chương trình được viết theo kiểu chức năng có thể sử dụng các đơn nguyên để cấu trúc các quy trình bao gồm các hoạt động được giải trình tự, 1 [2] hoặc để xác định các luồng điều khiển tùy ý (như xử lý đồng thời, tiếp tục hoặc ngoại lệ).

Chính thức, một đơn nguyên được xây dựng bằng cách xác định hai thao tác (liên kết và trả về) và một hàm tạo kiểu M phải đáp ứng một số thuộc tính để cho phép thành phần chính xác của các hàm đơn nguyên (nghĩa là các hàm sử dụng các giá trị từ đơn nguyên làm đối số của chúng). Hoạt động trả về lấy một giá trị từ một loại đơn giản và đặt nó vào một thùng chứa đơn loại loại M. Hoạt động liên kết thực hiện quá trình ngược lại, trích xuất giá trị ban đầu từ container và chuyển nó đến chức năng tiếp theo được liên kết trong đường ống.

Một lập trình viên sẽ soạn các hàm đơn âm để xác định đường ống xử lý dữ liệu. Các đơn nguyên hoạt động như một khung, vì đó là một hành vi có thể sử dụng lại để quyết định thứ tự các hàm đơn nguyên cụ thể trong đường ống được gọi và quản lý tất cả các công việc bí mật theo yêu cầu tính toán. [3] Các toán tử liên kết và trả về xen kẽ trong đường ống sẽ được thực thi sau khi mỗi hàm đơn nguyên trả về quyền điều khiển và sẽ xử lý các khía cạnh cụ thể được xử lý bởi đơn nguyên.

Tôi tin rằng nó giải thích nó rất tốt.


12

Tôi sẽ cố gắng đưa ra định nghĩa ngắn nhất mà tôi có thể quản lý bằng các thuật ngữ OOP:

Một lớp chung CMonadic<T>là một đơn nguyên nếu nó định nghĩa ít nhất các phương thức sau:

class CMonadic<T> { 
    static CMonadic<T> create(T t);  // a.k.a., "return" in Haskell
    public CMonadic<U> flatMap<U>(Func<T, CMonadic<U>> f); // a.k.a. "bind" in Haskell
}

và nếu các luật sau áp dụng cho tất cả các loại T và các giá trị có thể có của chúng t

danh tính bên trái:

CMonadic<T>.create(t).flatMap(f) == f(t)

đúng danh tính

instance.flatMap(CMonadic<T>.create) == instance

kết hợp:

instance.flatMap(f).flatMap(g) == instance.flatMap(t => f(t).flatMap(g))

Ví dụ :

Một danh sách đơn nguyên có thể có:

List<int>.create(1) --> [1]

Và FlatMap trong danh sách [1,2,3] có thể hoạt động như vậy:

intList.flatMap(x => List<int>.makeFromTwoItems(x, x*10)) --> [1,10,2,20,3,30]

Iterables và Observables cũng có thể được tạo ra một cách đơn điệu, cũng như các Promise và Nhiệm vụ.

Bình luận :

Monads không phức tạp lắm. Các flatMapchức năng rất giống như thường gặp hơn map. Nó nhận được một đối số hàm (còn được gọi là đại biểu), mà nó có thể gọi (ngay lập tức hoặc sau đó, không hoặc nhiều lần) với một giá trị đến từ lớp chung. Nó hy vọng rằng hàm đã qua cũng bao bọc giá trị trả về của nó trong cùng loại lớp chung. Để giúp với điều đó, nó cung cấp create, một hàm tạo có thể tạo một thể hiện của lớp chung đó từ một giá trị. Kết quả trả về của FlatMap cũng là một lớp chung cùng loại, thường đóng gói cùng các giá trị được chứa trong kết quả trả về của một hoặc nhiều ứng dụng của FlatMap với các giá trị được chứa trước đó. Điều này cho phép bạn xâu chuỗi FlatMap nhiều như bạn muốn:

intList.flatMap(x => List<int>.makeFromTwo(x, x*10))
       .flatMap(x => x % 3 == 0 
                   ? List<string>.create("x = " + x.toString()) 
                   : List<string>.empty())

Nó chỉ xảy ra rằng loại lớp chung này hữu ích như một mô hình cơ sở cho một số lượng lớn các thứ. Điều này (cùng với các thuật ngữ lý thuyết thể loại) là lý do tại sao Monads có vẻ rất khó hiểu hoặc giải thích. Chúng là một thứ rất trừu tượng và chỉ trở nên rõ ràng hữu ích một khi chúng là chuyên dụng.

Ví dụ: bạn có thể mô hình hóa các trường hợp ngoại lệ bằng cách sử dụng các thùng chứa đơn âm. Mỗi container sẽ chứa kết quả của hoạt động hoặc lỗi đã xảy ra. Hàm tiếp theo (ủy nhiệm) trong chuỗi các cuộc gọi lại FlatMap sẽ chỉ được gọi nếu hàm trước đó đóng gói một giá trị trong vùng chứa. Mặt khác, nếu một lỗi được đóng gói, lỗi sẽ tiếp tục lan truyền qua các thùng chứa chuỗi cho đến khi tìm thấy một thùng chứa có chức năng xử lý lỗi được đính kèm thông qua một phương thức được gọi .orElse()(phương thức đó sẽ là một phần mở rộng được phép)

Ghi chú : Các ngôn ngữ chức năng cho phép bạn viết các hàm có thể hoạt động trên bất kỳ loại lớp chung chung nào. Để làm việc này, người ta sẽ phải viết một giao diện chung cho các đơn nguyên. Tôi không biết có thể viết giao diện như vậy trong C # hay không, nhưng theo tôi biết thì không:

interface IMonad<T> { 
    static IMonad<T> create(T t); // not allowed
    public IMonad<U> flatMap<U>(Func<T, IMonad<U>> f); // not specific enough,
    // because the function must return the same kind of monad, not just any monad
}

7

Việc một đơn vị có cách giải thích "tự nhiên" trong OO hay không phụ thuộc vào đơn nguyên. Trong một ngôn ngữ như Java, bạn có thể dịch đơn nguyên có thể sang ngôn ngữ kiểm tra các con trỏ null, để các tính toán không thành công (nghĩa là tạo ra Không có gì trong Haskell) phát ra các con trỏ null làm kết quả. Bạn có thể dịch đơn nguyên trạng thái sang ngôn ngữ được tạo bằng cách tạo một biến và phương thức có thể thay đổi để thay đổi trạng thái của nó.

Một đơn nguyên là một monoid trong thể loại endofunctor.

Các thông tin mà câu đặt cùng nhau là rất sâu sắc. Và bạn làm việc trong một đơn nguyên với bất kỳ ngôn ngữ bắt buộc. Một đơn nguyên là một ngôn ngữ cụ thể miền "tuần tự". Nó thỏa mãn một số tính chất thú vị, được kết hợp lại tạo thành một mô hình toán học của "lập trình mệnh lệnh". Haskell giúp dễ dàng xác định các ngôn ngữ mệnh lệnh nhỏ (hoặc lớn), có thể được kết hợp theo nhiều cách khác nhau.

Là một lập trình viên OO, bạn sử dụng hệ thống phân cấp lớp của ngôn ngữ để tổ chức các loại chức năng hoặc quy trình có thể được gọi trong ngữ cảnh, thứ bạn gọi là đối tượng. Một đơn nguyên cũng là một sự trừu tượng hóa trong ý tưởng này, trong chừng mực vì các đơn vị khác nhau có thể được kết hợp theo những cách tùy tiện, "nhập khẩu" tất cả các phương pháp của đơn vị phụ vào phạm vi.

Về mặt kiến ​​trúc, người ta sau đó sử dụng chữ ký loại để thể hiện rõ ràng bối cảnh nào có thể được sử dụng để tính toán một giá trị.

Người ta có thể sử dụng máy biến áp đơn nguyên cho mục đích này và có một bộ sưu tập chất lượng cao của tất cả các đơn nguyên "tiêu chuẩn":

  • Danh sách (tính toán không xác định, bằng cách coi danh sách là một miền)
  • Có thể (các tính toán có thể thất bại, nhưng báo cáo không quan trọng)
  • Lỗi (tính toán có thể thất bại và yêu cầu xử lý ngoại lệ
  • Trình đọc (tính toán có thể được biểu diễn bằng các thành phần của các hàm Haskell đơn giản)
  • Trình ghi (tính toán với "kết xuất" / "ghi nhật ký" liên tiếp (vào chuỗi, html, v.v.)
  • Tiếp (tiếp tục)
  • IO (tính toán phụ thuộc vào hệ thống máy tính cơ bản)
  • Trạng thái (tính toán có bối cảnh chứa giá trị có thể sửa đổi)

với máy biến áp đơn nguyên tương ứng và các loại lớp. Các lớp loại cho phép một cách tiếp cận bổ sung để kết hợp các đơn nguyên bằng cách thống nhất các giao diện của chúng, để các đơn vị cụ thể có thể thực hiện một giao diện chuẩn cho "loại" đơn nguyên. Ví dụ: mô-đun Control.Monad.State chứa một lớp MonadState sm và (State s) là một thể hiện của biểu mẫu

instance MonadState s (State s) where
    put = ...
    get = ...

Câu chuyện dài là một đơn vị là một functor gắn "bối cảnh" với một giá trị, có cách để đưa một giá trị vào đơn vị, và có cách để đánh giá các giá trị liên quan đến bối cảnh gắn liền với nó, ít nhất là một cách hạn chế

Vì thế:

return :: a -> m a

là một hàm đưa một giá trị của loại a vào một "hành động" đơn loại của loại m a.

(>>=) :: m a -> (a -> m b) -> m b

là một hàm thực hiện một hành động đơn nguyên, đánh giá kết quả của nó và áp dụng một hàm cho kết quả. Điều gọn gàng về (>> =) là kết quả nằm trong cùng một đơn nguyên. Nói cách khác, trong m >> = f, (>> =) kéo kết quả ra khỏi m và liên kết nó với f, để kết quả nằm trong đơn nguyên. (Ngoài ra, chúng ta có thể nói rằng (>> =) kéo f vào m và áp dụng nó vào kết quả.) Kết quả là, nếu chúng ta có f :: a -> mb và g :: b -> mc, chúng ta có thể hành động "trình tự":

m >>= f >>= g

Hoặc, sử dụng "ký hiệu"

do x <- m
   y <- f x
   g y

Loại cho (>>) có thể được chiếu sáng. Nó là

(>>) :: m a -> m b -> m b

Nó tương ứng với toán tử (;) trong các ngôn ngữ thủ tục như C. Nó cho phép ký hiệu như:

m = do x <- someQuery
       someAction x
       theNextAction
       andSoOn

Trong logic toán học và triết học, chúng ta có các khung và mô hình, được mô hình hóa "tự nhiên" với chủ nghĩa đơn nguyên. Giải thích là một hàm nhìn vào miền của mô hình và tính giá trị thật (hoặc khái quát hóa) của một mệnh đề (hoặc công thức, theo các khái quát). Trong logic phương thức cho sự cần thiết, chúng tôi có thể nói rằng một đề xuất là cần thiết nếu nó đúng trong "mọi thế giới có thể" - nếu nó đúng với mọi miền được chấp nhận. Điều này có nghĩa là một mô hình trong ngôn ngữ cho một đề xuất có thể được thống nhất thành một mô hình có miền bao gồm tập hợp các mô hình riêng biệt (một mô hình tương ứng với mỗi thế giới có thể). Mỗi đơn nguyên có một phương thức có tên là "tham gia", làm phẳng các lớp, ngụ ý rằng mọi hành động đơn nguyên có kết quả là một hành động đơn nguyên có thể được nhúng vào đơn nguyên.

join :: m (m a) -> m a

Quan trọng hơn, điều đó có nghĩa là đơn nguyên được đóng lại dưới hoạt động "xếp chồng lớp". Đây là cách máy biến áp đơn nguyên hoạt động: họ kết hợp các đơn nguyên bằng cách cung cấp các phương thức "giống như tham gia" cho các loại như

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

để chúng ta có thể chuyển đổi một hành động trong (Có thể m) thành một hành động trong m, làm sụp đổ các lớp một cách hiệu quả. Trong trường hợp này, runMaybeT :: ShouldT ma -> m (Có thể a) là phương thức giống như tham gia của chúng tôi. (Có lẽ m) là một đơn nguyên, và Có lẽ ::

Một đơn vị miễn phí cho một functor là đơn vị được tạo ra bằng cách xếp chồng f, với ngụ ý rằng mọi chuỗi các hàm tạo cho f là một phần tử của đơn nguyên tự do (hay chính xác hơn là một thứ gì đó có hình dạng giống như cây trình tự của các hàm tạo f). Đơn nguyên miễn phí là một kỹ thuật hữu ích để xây dựng các đơn nguyên linh hoạt với số lượng tối thiểu của nồi hơi. Trong chương trình Haskell, tôi có thể sử dụng các đơn vị miễn phí để xác định các đơn vị đơn giản cho "lập trình hệ thống cấp cao" để giúp duy trì an toàn loại (Tôi chỉ sử dụng các loại và khai báo của chúng. Triển khai đơn giản với việc sử dụng các tổ hợp):

data RandomF r a = GetRandom (r -> a) deriving Functor
type Random r a = Free (RandomF r) a


type RandomT m a = Random (m a) (m a) -- model randomness in a monad by computing random monad elements.
getRandom     :: Random r r
runRandomIO   :: Random r a -> IO a (use some kind of IO-based backend to run)
runRandomIO'  :: Random r a -> IO a (use some other kind of IO-based backend)
runRandomList :: Random r a -> [a]  (some kind of list-based backend (for pseudo-randoms))

Monadism là kiến ​​trúc cơ bản cho cái mà bạn có thể gọi là mẫu "trình thông dịch" hoặc "lệnh", được trừu tượng hóa thành dạng rõ ràng nhất của nó, vì mọi tính toán đơn trị phải là "chạy", ít nhất là tầm thường. (Hệ thống thời gian chạy chạy đơn vị IO cho chúng tôi và là điểm vào của bất kỳ chương trình Haskell nào. IO "điều khiển" phần còn lại của các tính toán, bằng cách chạy các hành động IO theo thứ tự).

Kiểu tham gia cũng là nơi chúng ta nhận được tuyên bố rằng một đơn nguyên là một đơn chất trong danh mục endofunctor. Tham gia thường quan trọng hơn cho các mục đích lý thuyết, theo loại hình này. Nhưng hiểu loại có nghĩa là hiểu đơn nguyên. Các kiểu giống như tham gia của biến đổi tham gia và đơn nguyên là các thành phần có hiệu quả của endofunctor, theo nghĩa của thành phần chức năng. Để đặt nó trong một ngôn ngữ giả giống như Haskell,

Foo :: m (ma) <-> (m. M) a


3

Một đơn nguyên là một mảng các chức năng

(Pst: một mảng các hàm chỉ là một tính toán).

Trên thực tế, thay vì một mảng thực sự (một hàm trong một mảng ô), bạn có các hàm đó được xâu chuỗi bởi một hàm khác >> =. >> = cho phép điều chỉnh kết quả từ hàm i sang hàm i + 1, thực hiện các phép tính giữa chúng hoặc, thậm chí, không gọi hàm i + 1.

Các loại được sử dụng ở đây là "loại có ngữ cảnh". Đây là, một giá trị với một "thẻ". Các hàm bị xiềng xích phải lấy "giá trị trần" và trả về kết quả được gắn thẻ. Một trong những nhiệm vụ của >> = là trích xuất một giá trị trần trụi ra khỏi bối cảnh của nó. Ngoài ra còn có chức năng "return", lấy một giá trị trần và đặt nó với một thẻ.

Một ví dụ với Có thể . Hãy sử dụng nó để lưu trữ một số nguyên đơn giản để thực hiện tính toán.

-- a * b
multiply :: Int -> Int -> Maybe Int
multiply a b = return  (a*b)

-- divideBy 5 100 = 100 / 5
divideBy :: Int -> Int -> Maybe Int
divideBy 0 _ = Nothing -- dividing by 0 gives NOTHING
divideBy denom num = return (quot num denom) -- quotient of num / denom

-- tagged value
val1 = Just 160 

-- array of functions feeded with val1
array1 = val1 >>= divideBy 2  >>= multiply 3 >>= divideBy  4 >>= multiply 3

-- array of funcionts created with the do notation
-- equals array1 but for the feeded val1
array2 :: Int -> Maybe Int
array2 n = do
       v <- divideBy 2  n
       v <- multiply 3 v
       v <- divideBy 4 v
       v <- multiply 3 v
       return v

-- array of functions, 
-- the first >>= performs 160 / 0, returning Nothing
-- the second >>= has to perform Nothing >>= multiply 3 ....
-- and simply returns Nothing without calling multiply 3 ....
array3 = val1 >>= divideBy 0  >>= multiply 3 >>= divideBy  4 >>= multiply 3

main = do
     print array1
     print (array2 160)
     print array3

Chỉ cần chỉ ra rằng các đơn nguyên là mảng các hàm với các hoạt động của trình trợ giúp, hãy xem xét tương đương với ví dụ trên, chỉ cần sử dụng một mảng các hàm thực sự

type MyMonad = [Int -> Maybe Int] -- my monad as a real array of functions

myArray1 = [divideBy 2, multiply 3, divideBy 4, multiply 3]

-- function for the machinery of executing each function i with the result provided by function i-1
runMyMonad :: Maybe Int -> MyMonad -> Maybe Int
runMyMonad val [] = val
runMyMonad Nothing _ = Nothing
runMyMonad (Just val) (f:fs) = runMyMonad (f val) fs

Và nó sẽ được sử dụng như thế này:

print (runMyMonad (Just 160) myArray1)

1
Siêu gọn gàng! Vì vậy, liên kết chỉ là một cách để đánh giá một loạt các hàm với ngữ cảnh, theo trình tự, trên một đầu vào có ngữ cảnh :)
Musa Al-hassy

>>=là một nhà điều hành
user2418306

1
Tôi nghĩ rằng sự tương tự "mảng chức năng" không làm rõ nhiều. Nếu \x -> x >>= k >>= l >>= mlà một mảng các hàm, h . g . fthì cũng không liên quan đến các đơn nguyên.
song công

chúng ta có thể nói rằng functor , cho dù là đơn nguyên, ứng dụng hay đơn giản, là về "ứng dụng tôn tạo" . 'Ứng dụng' thêm chuỗi, và 'đơn nguyên' thêm sự phụ thuộc (nghĩa là tạo bước tính toán tiếp theo tùy thuộc vào kết quả từ bước tính toán trước đó).
Will Ness

3

Theo thuật ngữ OO, một đơn nguyên là một container lưu loát.

Yêu cầu tối thiểu là một định nghĩa class <A> Somethinghỗ trợ hàm tạo Something(A a)và ít nhất một phương thứcSomething<B> flatMap(Function<A, Something<B>>)

Có thể cho rằng, nó cũng được tính nếu lớp đơn nguyên của bạn có bất kỳ phương thức nào có chữ ký Something<B> work()giữ nguyên quy tắc của lớp - trình biên dịch sẽ tạo ra trong FlatMap tại thời điểm biên dịch.

Tại sao một đơn nguyên hữu ích? Bởi vì nó là một thùng chứa cho phép các hoạt động có khả năng chuỗi bảo tồn ngữ nghĩa. Ví dụ, Optional<?>bảo ngữ nghĩa của isPresent cho Optional<String>, Optional<Integer>, Optional<MyClass>vv

Một ví dụ điển hình,

Something<Integer> i = new Something("a")
  .flatMap(doOneThing)
  .flatMap(doAnother)
  .flatMap(toInt)

Lưu ý chúng tôi bắt đầu bằng một chuỗi và kết thúc bằng một số nguyên. Tuyệt đấy.

Trong OO, có thể cần vẫy tay một chút, nhưng bất kỳ phương thức nào trên Thứ gì đó trả về một lớp con khác của Thứ gì đó đều đáp ứng tiêu chí của hàm chứa trả về một thùng chứa loại gốc.

Đó là cách bạn bảo tồn ngữ nghĩa - nghĩa là các hoạt động và ý nghĩa của bộ chứa không thay đổi, chúng chỉ bao bọc và nâng cao đối tượng bên trong container.


2

Các đơn vị sử dụng điển hình là tương đương về chức năng của các cơ chế xử lý ngoại lệ của lập trình thủ tục.

Trong các ngôn ngữ thủ tục hiện đại, bạn đặt một trình xử lý ngoại lệ xung quanh một chuỗi các câu lệnh, bất kỳ câu lệnh nào cũng có thể đưa ra một ngoại lệ. Nếu bất kỳ câu lệnh nào đưa ra một ngoại lệ, việc thực thi bình thường chuỗi các câu lệnh sẽ dừng lại và chuyển sang một trình xử lý ngoại lệ.

Tuy nhiên, các ngôn ngữ lập trình chức năng về mặt triết học tránh các tính năng xử lý ngoại lệ do tính chất "goto" giống như chúng. Quan điểm lập trình chức năng là các chức năng không nên có "tác dụng phụ" như ngoại lệ làm gián đoạn dòng chảy chương trình.

Trong thực tế, tác dụng phụ không thể loại trừ trong thế giới thực do chủ yếu là I / O. Các đơn vị trong lập trình chức năng được sử dụng để xử lý việc này bằng cách thực hiện một tập hợp các lệnh gọi chuỗi (bất kỳ cuộc gọi nào có thể tạo ra kết quả không mong muốn) và biến bất kỳ kết quả không mong muốn nào thành dữ liệu được đóng gói vẫn có thể lưu chuyển an toàn thông qua các lệnh gọi chức năng còn lại.

Luồng kiểm soát được bảo tồn nhưng sự kiện bất ngờ được đóng gói và xử lý một cách an toàn.


2

Một lời giải thích đơn giản về Monads với nghiên cứu trường hợp của Marvel có ở đây .

Monads là trừu tượng được sử dụng để sắp xếp các hàm phụ thuộc có hiệu quả. Hiệu quả ở đây có nghĩa là họ trả về một loại ở dạng F [A] ví dụ Tùy chọn [A] trong đó Tùy chọn là F, được gọi là hàm tạo kiểu. Chúng ta hãy xem điều này trong 2 bước đơn giản

  1. Dưới đây Thành phần chức năng là bắc cầu. Vì vậy, để đi từ A đến CI có thể soạn A => B và B => C.
 A => C   =   A => B  andThen  B => C

nhập mô tả hình ảnh ở đây

  1. Tuy nhiên, nếu hàm trả về một loại hiệu ứng như Tùy chọn [A] tức là A => F [B] thì chế phẩm không hoạt động như để đi đến B, chúng ta cần A => B nhưng chúng ta có A => F [B].
    nhập mô tả hình ảnh ở đây

    Chúng ta cần một toán tử đặc biệt, "liên kết" biết cách hợp nhất các hàm này trả về F [A].

 A => F[C]   =   A => F[B]  bind  B => F[C]

Hàm "liên kết" được định nghĩa cho F cụ thể .

Ngoài ra còn có "return" , loại A => F [A] cho bất kỳ A nào , được xác định cho F cụ thể đó . Để trở thành một Monad, F phải có hai hàm này được xác định cho nó.

Do đó, chúng ta có thể xây dựng một hàm hiệu quả A => F [B] từ bất kỳ hàm thuần A => B ,

 A => F[B]   =   A => B  andThen  return

nhưng một F đã cho cũng có thể định nghĩa các hàm đặc biệt "tích hợp" mờ đục của chính nó theo kiểu mà người dùng không thể tự định nghĩa chúng (bằng ngôn ngữ thuần túy ), như

  • " Ngẫu nhiên" ( Phạm vi => Ngẫu nhiên [Int] )
  • "in" ( Chuỗi => IO [()] )
  • "thử ... bắt", v.v.

2

Tôi đang chia sẻ sự hiểu biết của tôi về Monads, có thể không hoàn hảo về mặt lý thuyết. Monads là về tuyên truyền bối cảnh . Monad là, bạn xác định một số bối cảnh cho một số dữ liệu (hoặc loại dữ liệu), sau đó xác định cách thức bối cảnh đó sẽ được thực hiện với dữ liệu trong suốt quá trình xử lý. Và việc xác định lan truyền ngữ cảnh chủ yếu là về việc xác định cách hợp nhất nhiều bối cảnh (cùng loại). Sử dụng Monads cũng có nghĩa là đảm bảo các bối cảnh này không vô tình bị tước khỏi dữ liệu. Mặt khác, dữ liệu không ngữ cảnh khác có thể được đưa vào một bối cảnh mới hoặc hiện có. Sau đó, khái niệm đơn giản này có thể được sử dụng để đảm bảo tính chính xác về thời gian của chương trình.



1

Xem câu trả lời của tôi cho "một đơn nguyên là gì?"

Nó bắt đầu với một ví dụ động lực, hoạt động thông qua ví dụ, lấy ra một ví dụ về một đơn nguyên và chính thức định nghĩa "đơn nguyên".

Nó giả định không có kiến ​​thức về lập trình chức năng và nó sử dụng mã giả với function(argument) := expressioncú pháp với các biểu thức đơn giản nhất có thể.

Chương trình C ++ này là một triển khai của đơn vị mã giả. (Để tham khảo: Mlà hàm tạo kiểu, feedlà thao tác "liên kết" và wraplà thao tác "trả về".)

#include <iostream>
#include <string>

template <class A> class M
{
public:
    A val;
    std::string messages;
};

template <class A, class B>
M<B> feed(M<B> (*f)(A), M<A> x)
{
    M<B> m = f(x.val);
    m.messages = x.messages + m.messages;
    return m;
}

template <class A>
M<A> wrap(A x)
{
    M<A> m;
    m.val = x;
    m.messages = "";
    return m;
}

class T {};
class U {};
class V {};

M<U> g(V x)
{
    M<U> m;
    m.messages = "called g.\n";
    return m;
}

M<T> f(U x)
{
    M<T> m;
    m.messages = "called f.\n";
    return m;
}

int main()
{
    V x;
    M<T> m = feed(f, feed(g, wrap(x)));
    std::cout << m.messages;
}

0

Từ quan điểm thực tế (tóm tắt những gì đã được nói trong nhiều câu trả lời trước đây và các bài viết liên quan), đối với tôi, một trong những "mục đích" cơ bản (hoặc hữu ích) của đơn nguyên là tận dụng sự phụ thuộc tiềm ẩn trong các yêu cầu phương pháp đệ quy aka thành phần chức năng (nghĩa là khi F1 gọi f2 gọi f3, f3 cần được đánh giá trước f2 trước F1) để biểu diễn thành phần tuần tự theo cách tự nhiên, đặc biệt là trong bối cảnh của mô hình đánh giá lười biếng (nghĩa là thành phần tuần tự như một chuỗi đơn giản , ví dụ: "f3 (); f2 (); f1 ();" trong C - mẹo đặc biệt rõ ràng nếu bạn nghĩ về trường hợp f3, f2 và f1 thực sự không trả lại gì [chuỗi của họ là F1 (f2 (f3)) là nhân tạo, hoàn toàn có ý định tạo chuỗi]).

Điều này đặc biệt có liên quan khi các tác dụng phụ có liên quan, nghĩa là khi một số trạng thái bị thay đổi (nếu F1, f2, f3 không có tác dụng phụ, thì chúng sẽ được đánh giá theo thứ tự nào; ngôn ngữ chức năng, để có thể song song hóa các tính toán đó chẳng hạn). Các chức năng càng tinh khiết, tốt hơn.

Tôi nghĩ theo quan điểm hạn hẹp đó, các đơn nguyên có thể được coi là đường cú pháp cho các ngôn ngữ thiên về đánh giá lười biếng (chỉ đánh giá mọi thứ khi thực sự cần thiết, tuân theo một trật tự không dựa vào việc trình bày mã) và không có phương tiện khác đại diện cho thành phần tuần tự. Kết quả cuối cùng là các phần của mã "không tinh khiết" (nghĩa là có tác dụng phụ) có thể được trình bày một cách tự nhiên, theo cách bắt buộc, nhưng được tách biệt rõ ràng khỏi các hàm thuần túy (không có tác dụng phụ), có thể được đánh giá lười biếng.

Đây chỉ là một khía cạnh, như được cảnh báo ở đây .


0

Giải thích đơn giản nhất mà tôi có thể nghĩ đến là các đơn nguyên là một cách kết hợp các hàm với kết quả được tô điểm (còn gọi là thành phần Kleisli). Hàm "embelished" có chữ ký a -> (b, smth)trong đó ablà các loại (nghĩ Int, Bool) có thể khác nhau, nhưng không nhất thiết - và smthlà "bối cảnh" hoặc "sự tô điểm".

Loại chức năng này cũng có thể được viết a -> m bở nơi mtương đương với "tôn tạo" smth. Vì vậy, đây là các hàm trả về giá trị theo ngữ cảnh (nghĩ rằng các hàm ghi nhật ký hành động của chúng, smththông điệp ghi nhật ký hoặc các hàm thực hiện đầu vào \ đầu ra và kết quả của chúng phụ thuộc vào kết quả của hành động IO).

Một đơn nguyên là một giao diện ("typeclass") làm cho người triển khai cho nó biết cách soạn các hàm như vậy. Người triển khai cần xác định hàm thành phần (a -> m b) -> (b -> m c) -> (a -> m c)cho bất kỳ loại mnào muốn thực hiện giao diện (đây là thành phần Kleisli).

Vì vậy, nếu chúng ta nói rằng chúng ta có một loại tuple (Int, String)đại diện cho kết quả tính toán trên Ints cũng ghi lại hành động của họ, với (_, String)"sự tô điểm" - nhật ký của hành động - và hai hàm increment :: Int -> (Int, String)twoTimes :: Int -> (Int, String)chúng ta muốn có được một hàm incrementThenDouble :: Int -> (Int, String)là thành phần của hai chức năng cũng tính đến các bản ghi.

Trong ví dụ đã cho, một triển khai đơn nguyên của hai hàm áp dụng cho giá trị nguyên 2 incrementThenDouble 2(bằng với twoTimes (increment 2)) sẽ trả về (6, " Adding 1. Doubling 3.")kết quả trung gian increment 2bằng (3, " Adding 1.")twoTimes 3bằng(6, " Doubling 3.")

Từ hàm thành phần Kleisli này, người ta có thể rút ra các hàm đơn âm thông thường.

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.