Khi nào các loại kinded cao hơn hữu ích?


88

Tôi đã làm dev ở F # một thời gian và tôi thích nó. Tuy nhiên, một từ thông dụng mà tôi biết không tồn tại trong F # là các loại từ khóa cao hơn. Tôi đã đọc tài liệu về các loại tốt hơn và tôi nghĩ tôi hiểu định nghĩa của chúng. Tôi chỉ không chắc tại sao chúng lại hữu ích. Ai đó có thể cung cấp một số ví dụ về những loại có mối quan hệ cao hơn giúp dễ dàng trong Scala hoặc Haskell, yêu cầu giải pháp thay thế trong F # không? Ngoài ra đối với những ví dụ này, cách giải quyết sẽ như thế nào nếu không có các loại định hướng cao hơn (hoặc ngược lại trong F #)? Có lẽ tôi đã quá quen với việc làm việc xung quanh nó nên tôi không nhận thấy sự vắng mặt của tính năng đó.

(Tôi nghĩ) Tôi hiểu rằng thay vì myList |> List.map fhoặc myList |> Seq.map f |> Seq.toListcác loại kinded cao hơn cho phép bạn chỉ cần viết myList |> map fvà nó sẽ trả về a List. Điều đó thật tuyệt (giả sử nó chính xác), nhưng có vẻ hơi nhỏ? (Và nó không thể được thực hiện đơn giản bằng cách cho phép quá tải hàm?) Tôi thường chuyển đổi thành Seqanyway và sau đó tôi có thể chuyển đổi thành bất kỳ thứ gì tôi muốn sau đó. Một lần nữa, có lẽ tôi đã quá quen với việc làm việc xung quanh nó. Nhưng có ví dụ nào trong đó các kiểu loại cao hơn thực sự giúp bạn tiết kiệm trong các lần gõ phím hoặc an toàn khi gõ không?


2
Nhiều chức năng trong Control.Monad sử dụng các loại cao hơn, vì vậy bạn có thể muốn xem ở đó để biết một số ví dụ. Trong F #, việc triển khai sẽ phải được lặp lại cho từng loại đơn nguyên cụ thể.
Lee

1
@Lee nhưng bạn không thể chỉ tạo một giao diện IMonad<T>và sau đó truyền nó trở lại ví dụ IEnumerable<int>hoặc IObservable<int>khi bạn hoàn thành? Tất cả chỉ là để tránh casting thôi sao?
lobsterism,

4
Truyền tốt là không an toàn, do đó, câu trả lời cho câu hỏi của bạn về an toàn kiểu. Một vấn đề khác là làm thế nào returnsẽ hoạt động vì nó thực sự thuộc về loại đơn nguyên, không phải là một trường hợp cụ thể, vì vậy bạn sẽ không muốn đưa nó vào IMonadgiao diện.
Lee

4
@Lee yeah Tôi chỉ nghĩ rằng bạn phải chọn kết quả cuối cùng sau biểu thức, không có vấn đề gì vì bạn chỉ tạo biểu thức để bạn biết loại. Nhưng có vẻ như bạn cũng phải truyền vào bên trong mỗi mô hình bindhay còn gọi là SelectManyvv. Điều đó có nghĩa là ai đó có thể sử dụng API cho bindmột IObservableđến một IEnumerablevà cho rằng nó sẽ hoạt động, thật tuyệt nếu đúng như vậy và không có cách nào giải quyết được điều đó. Chỉ không chắc chắn 100% không có cách nào xung quanh nó.
lobsterism,

5
Câu hỏi tuyệt vời. Tôi vẫn chưa thấy một ví dụ thực tế hấp dẫn nào về tính năng ngôn ngữ này là IRL hữu ích.
JD

Câu trả lời:


78

Vì vậy, loại của một loại là loại đơn giản của nó. Ví dụ: Intloại *có nghĩa là loại cơ sở và có thể được khởi tạo bằng các giá trị. Theo một định nghĩa lỏng lẻo nào đó về kiểu có kiểu dáng cao hơn (và tôi không chắc F # vẽ đường thẳng ở đâu, vì vậy hãy chỉ bao gồm nó) các thùng chứa đa hình là một ví dụ tuyệt vời về kiểu kiểu cao hơn.

data List a = Cons a (List a) | Nil

Kiểu xây dựng Listcó loại * -> *có nghĩa là nó phải được thông qua một kiểu cụ thể để dẫn đến một kiểu cụ thể: List Intcó thể có người dân thích [1,2,3]nhưng Listbản thân nó thì không.

Tôi sẽ giả định rằng lợi ích của các thùng chứa đa hình là rõ ràng, nhưng * -> *tồn tại nhiều loại hữu ích hơn là chỉ các thùng chứa. Ví dụ, các mối quan hệ

data Rel a = Rel (a -> a -> Bool)

hoặc phân tích cú pháp

data Parser a = Parser (String -> [(a, String)])

cả hai cũng có loại * -> *.


Tuy nhiên, chúng ta có thể làm điều này xa hơn trong Haskell, bằng cách có các loại có thứ tự cao hơn. Ví dụ, chúng ta có thể tìm kiếm một loại có tử tế (* -> *) -> *. Một ví dụ đơn giản về điều này có thể là Shapecố gắng lấp đầy một loại vật chứa * -> *.

data Shape f = Shape (f ())

[(), (), ()] :: Shape List

TraversableVí dụ, điều này rất hữu ích cho việc mô tả đặc điểm của các s trong Haskell, vì chúng luôn có thể được chia thành hình dạng và nội dung của chúng.

split :: Traversable t => t a -> (Shape t, [a])

Ví dụ khác, hãy xem xét một cây được tham số hóa trên loại nhánh mà nó có. Ví dụ, một cây bình thường có thể

data Tree a = Branch (Tree a) a (Tree a) | Leaf

Nhưng chúng ta có thể thấy rằng các loại chi nhánh chứa một Paircủa Tree as và vì vậy chúng tôi có thể trích xuất mảnh rằng trong số các loại parametrically

data TreeG f a = Branch a (f (TreeG f a)) | Leaf

data Pair a = Pair a a
type Tree a = TreeG Pair a

Hàm tạo TreeGkiểu này có loại (* -> *) -> * -> *. Chúng tôi có thể sử dụng nó để tạo ra các biến thể thú vị khác nhưRoseTree

type RoseTree a = TreeG [] a

rose :: RoseTree Int
rose = Branch 3 [Branch 2 [Leaf, Leaf], Leaf, Branch 4 [Branch 4 []]]

Hoặc những bệnh lý như một MaybeTree

data Empty a = Empty
type MaybeTree a = TreeG Empty a

nothing :: MaybeTree a
nothing = Leaf

just :: a -> MaybeTree a
just a = Branch a Empty

Hoặc một TreeTree

type TreeTree a = TreeG Tree a

treetree :: TreeTree Int
treetree = Branch 3 (Branch Leaf (Pair Leaf Leaf))

Một nơi khác mà điều này hiển thị là trong "đại số của functors". Nếu chúng ta giảm bớt một vài lớp trừu tượng, điều này có thể được coi là một nếp gấp, chẳng hạn như sum :: [Int] -> Int. Đại số được tham số hóa trong functorhãng . Các functor có loại * -> *và các tàu sân bay loại *nên hoàn toàn

data Alg f a = Alg (f a -> a)

có lòng tốt (* -> *) -> * -> *. Alghữu ích vì mối quan hệ của nó với các kiểu dữ liệu và lược đồ đệ quy được xây dựng trên chúng.

-- | The "single-layer of an expression" functor has kind `(* -> *)`
data ExpF x = Lit Int
            | Add x x
            | Sub x x
            | Mult x x

-- | The fixed point of a functor has kind `(* -> *) -> *`
data Fix f = Fix (f (Fix f))

type Exp = Fix ExpF

exp :: Exp
exp = Fix (Add (Fix (Lit 3)) (Fix (Lit 4))) -- 3 + 4

fold :: Functor f => Alg f a -> Fix f -> a
fold (Alg phi) (Fix f) = phi (fmap (fold (Alg phi)) f)

Cuối cùng, mặc dù họ đang ở trên lý thuyết có thể, tôi chưa bao giờ thấy một thậm chí constructor loại cao kinded. Đôi khi chúng ta thấy các chức năng của loại đó chẳng hạn như mask :: ((forall a. IO a -> IO a) -> IO b) -> IO b, nhưng tôi nghĩ bạn sẽ phải tìm hiểu sâu về loại prolog hoặc tài liệu được đánh máy phụ thuộc để xem mức độ phức tạp đó trong các loại.


3
Tôi sẽ nhập kiểm tra và chỉnh sửa mã sau vài phút nữa, tôi đang sử dụng điện thoại của mình.
J. Abrahamson

12
@ J.Abrahamson 1 cho một câu trả lời tốt và có đủ kiên nhẫn để gõ đó trên điện thoại của bạn O_o
Daniel Gratzer

3
@lobsterism A TreeTreechỉ là bệnh lý, nhưng thực tế hơn, điều đó có nghĩa là bạn có hai loại cây khác nhau đan xen vào nhau — đẩy ý tưởng đó ra xa hơn một chút có thể giúp bạn có một số khái niệm an toàn về loại rất mạnh mẽ chẳng hạn như màu đỏ an toàn tĩnh / cây đen và loại FingerTree cân bằng tĩnh gọn gàng.
J. Abrahamson

3
@JonHarrop Một ví dụ tiêu chuẩn trong thế giới thực đang trừu tượng hóa các đơn nguyên, ví dụ với ngăn xếp hiệu ứng kiểu mtl. Bạn có thể không đồng ý rằng điều này có giá trị trong thế giới thực. Tôi nghĩ rõ ràng là các ngôn ngữ có thể tồn tại thành công mà không cần HKT, vì vậy bất kỳ ví dụ nào cũng sẽ cung cấp một số loại trừu tượng phức tạp hơn các ngôn ngữ khác.
J. Abrahamson

2
Bạn có thể có, ví dụ: tập hợp con của các hiệu ứng được ủy quyền trong các monads khác nhau và tóm tắt trên bất kỳ monads nào đáp ứng thông số kỹ thuật đó. Ví dụ: monads khởi tạo "teletype" cho phép đọc và ghi cấp độ ký tự có thể bao gồm cả IO và trừu tượng ống. Bạn có thể tóm tắt các triển khai không đồng bộ khác nhau như một ví dụ khác. Không có HKT, bạn giới hạn bất kỳ loại nào được sáng tác từ tác phẩm chung đó.
J. Abrahamson

64

Hãy xem xét Functorlớp kiểu trong Haskell, đâu flà biến kiểu kiểu cao hơn:

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

Chữ ký kiểu này nói gì là fmap thay đổi tham số kiểu của một ftừ athành b, nhưng vẫn fgiữ nguyên như cũ. Vì vậy, nếu bạn sử dụng fmaptrên một danh sách, bạn sẽ nhận được một danh sách, nếu bạn sử dụng nó trên một trình phân tích cú pháp, bạn sẽ nhận được một trình phân tích cú pháp, v.v. Và đây là những đảm bảo tĩnh , thời gian biên dịch.

Tôi không biết F #, nhưng chúng ta hãy xem xét điều gì sẽ xảy ra nếu chúng ta cố gắng diễn đạt sự Functortrừu tượng bằng một ngôn ngữ như Java hoặc C #, với sự kế thừa và generics, nhưng không có generic cao hơn. Lần thử đầu tiên:

interface Functor<A> {
    Functor<B> map(Function<A, B> f);
}

Vấn đề với lần thử đầu tiên này là việc triển khai giao diện được phép trả về bất kỳ lớp nào thực hiện Functor. Ai đó có thể viết một phương thức FunnyList<A> implements Functor<A>mapphương thức trả về một loại tập hợp khác hoặc thậm chí một cái gì đó khác không phải là một tập hợp nhưng vẫn là một Functor. Ngoài ra, khi bạn sử dụng mapphương thức này, bạn không thể gọi bất kỳ phương thức cụ thể kiểu phụ nào trên kết quả trừ khi bạn chuyển nó xuống kiểu mà bạn thực sự mong đợi. Vì vậy, chúng tôi có hai vấn đề:

  1. Hệ thống kiểu không cho phép chúng ta thể hiện bất biến rằng mapphương thức luôn trả về cùng một Functorlớp con với người nhận.
  2. Do đó, không có cách an toàn kiểu tĩnh nào để gọi một Functorphương thức không phải là kết quả của map.

Bạn có thể thử những cách khác phức tạp hơn, nhưng không có cách nào thực sự hiệu quả. Ví dụ: bạn có thể thử tăng cường lần thử đầu tiên bằng cách xác định các kiểu phụ của kiểu Functorhạn chế kết quả:

interface Collection<A> extends Functor<A> {
    Collection<B> map(Function<A, B> f);
}

interface List<A> extends Collection<A> {
    List<B> map(Function<A, B> f);
}

interface Set<A> extends Collection<A> {
    Set<B> map(Function<A, B> f);
}

interface Parser<A> extends Functor<A> {
    Parser<B> map(Function<A, B> f);
}

// …

Điều này giúp cấm những người triển khai các giao diện hẹp hơn đó trả về loại sai Functortừ mapphương thức, nhưng vì không có giới hạn về số lượng Functortriển khai bạn có thể có, nên không có giới hạn về số lượng giao diện hẹp hơn bạn sẽ cần.

( CHỈNH SỬA: Và lưu ý rằng điều này chỉ hoạt động vì nó Functor<B>xuất hiện dưới dạng loại kết quả và do đó, giao diện con có thể thu hẹp nó. Vì vậy AFAIK chúng tôi không thể thu hẹp cả hai cách sử dụng Monad<B>trong giao diện sau:

interface Monad<A> {
    <B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}

Trong Haskell, với các biến kiểu cấp cao hơn, đây là (>>=) :: Monad m => m a -> (a -> m b) -> m b.)

Tuy nhiên, một thử nghiệm khác là sử dụng các generic đệ quy để thử và có giao diện hạn chế kiểu kết quả của kiểu con đối với chính kiểu con đó. Ví dụ đồ chơi:

/**
 * A semigroup is a type with a binary associative operation.  Law:
 *
 * > x.append(y).append(z) = x.append(y.append(z))
 */
interface Semigroup<T extends Semigroup<T>> {
    T append(T arg);
}

class Foo implements Semigroup<Foo> {
    // Since this implements Semigroup<Foo>, now this method must accept 
    // a Foo argument and return a Foo result. 
    Foo append(Foo arg);
}

class Bar implements Semigroup<Bar> {
    // Any of these is a compilation error:

    Semigroup<Bar> append(Semigroup<Bar> arg);

    Semigroup<Foo> append(Bar arg);

    Semigroup append(Bar arg);

    Foo append(Bar arg);

}

Nhưng loại kỹ thuật này (khá phức tạp đối với nhà phát triển OOP hàng đầu của bạn, còn đối với nhà phát triển chức năng hàng loạt của bạn) vẫn không thể thể hiện Functorràng buộc mong muốn :

interface Functor<FA extends Functor<FA, A>, A> {
    <FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}

Vấn đề ở đây là điều này không hạn chế FBcó giống Fnhư FA— vì vậy khi bạn khai báo một kiểu List<A> implements Functor<List<A>, A>, mapphương thức vẫn có thể trả về a NotAList<B> implements Functor<NotAList<B>, B>.

Lần thử cuối cùng, trong Java, sử dụng các kiểu thô (vùng chứa chưa được phân loại):

interface FunctorStrategy<F> {
    F map(Function f, F arg);
} 

Ở đây Fsẽ được khởi tạo cho các loại chưa được phân loại như just Listhoặc Map. Điều này đảm bảo rằng a FunctorStrategy<List>chỉ có thể trả về a List—nhưng bạn đã từ bỏ việc sử dụng các biến kiểu để theo dõi các loại phần tử của danh sách.

Trọng tâm của vấn đề ở đây là các ngôn ngữ như Java và C # không cho phép các tham số kiểu có tham số. Trong Java, nếu Tlà một biến kiểu, bạn có thể viết TList<T>nhưng không T<String>. Các loại thân thiện hơn loại bỏ hạn chế này, do đó bạn có thể có một cái gì đó như thế này (không được suy nghĩ đầy đủ):

interface Functor<F, A> {
    <B> F<B> map(Function<A, B> f);
}

class List<A> implements Functor<List, A> {

    // Since F := List, F<B> := List<B>
    <B> List<B> map(Function<A, B> f) {
        // ...
    }

}

Và giải quyết vấn đề này cụ thể:

(Tôi nghĩ) Tôi hiểu rằng thay vì myList |> List.map fhoặc myList |> Seq.map f |> Seq.toListcác loại kinded cao hơn cho phép bạn chỉ cần viết myList |> map fvà nó sẽ trả về a List. Điều đó thật tuyệt (giả sử nó chính xác), nhưng có vẻ hơi nhỏ? (Và nó không thể được thực hiện đơn giản bằng cách cho phép quá tải hàm?) Tôi thường chuyển đổi thành Seqanyway và sau đó tôi có thể chuyển đổi thành bất kỳ thứ gì tôi muốn sau đó.

Có nhiều ngôn ngữ khái quát hóa ý tưởng của maphàm theo cách này, bằng cách mô hình hóa nó như thể, về cơ bản, ánh xạ là về các chuỗi. Nhận xét này của bạn là trên tinh thần đó: nếu bạn có một loại hỗ trợ chuyển đổi sang và đi Seq, bạn sẽ nhận được hoạt động bản đồ "miễn phí" bằng cách sử dụng lại Seq.map.

Tuy nhiên, trong Haskell, Functorlớp này chung chung hơn thế; nó không bị ràng buộc với khái niệm về trình tự. Bạn có thể triển khai fmapcho các loại không có ánh xạ tốt tới các chuỗi, như IOhành động, tổ hợp phân tích cú pháp, hàm, v.v.:

instance Functor IO where
    fmap f action =
        do x <- action
           return (f x)

 -- This declaration is just to make things easier to read for non-Haskellers 
newtype Function a b = Function (a -> b)

instance Functor (Function a) where
    fmap f (Function g) = Function (f . g)  -- `.` is function composition

Khái niệm "ánh xạ" thực sự không gắn liền với trình tự. Tốt nhất bạn nên hiểu luật của functor:

(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs

Rất chính thức:

  1. Luật đầu tiên nói rằng ánh xạ với một hàm nhận dạng / noop giống như không làm gì cả.
  2. Định luật thứ hai nói rằng bất kỳ kết quả nào bạn có thể tạo ra bằng cách ánh xạ hai lần, bạn cũng có thể tạo ra bằng cách ánh xạ một lần.

Đây là lý do tại sao bạn muốn fmapduy trì kiểu — bởi vì ngay sau khi bạn nhận được các mapphép toán tạo ra một kiểu kết quả khác, việc đảm bảo như thế này sẽ trở nên khó khăn hơn rất nhiều.


Vì vậy, tôi quan tâm đến phần cuối cùng của bạn, tại sao nó lại hữu ích khi fmapbật Function akhi nó đã .hoạt động? Tôi hiểu tại sao .định nghĩa của fmapop lại có ý nghĩa , nhưng tôi không hiểu bạn cần dùng đến đâu để fmapthay thế .. Có lẽ nếu bạn có thể đưa ra một ví dụ hữu ích, nó sẽ giúp tôi hiểu.
lobsterism

1
À, hiểu rồi: bạn có thể tạo ra một fn doublecủa một functor, nơi double [1, 2, 3]cho [2, 4, 6]double sincho một fn là gấp đôi tội lỗi. Tôi có thể thấy nếu bạn bắt đầu suy nghĩ theo tư duy đó, khi bạn chạy bản đồ trên một mảng, bạn mong đợi một mảng trở lại, không chỉ là seq, bởi vì, chúng tôi đang làm việc trên các mảng ở đây.
lobsterism

@lobsterism: Có các thuật toán / kỹ thuật dựa vào việc có thể tóm tắt a Functorvà cho phép khách hàng của thư viện chọn ra. Câu trả lời của J. Abrahamson đưa ra một ví dụ: các nếp gấp đệ quy có thể được tổng quát hóa bằng cách sử dụng các bộ hàm. Một ví dụ khác là monads miễn phí; bạn có thể coi đây là một loại thư viện triển khai trình thông dịch chung, nơi máy khách cung cấp "tập lệnh" như một tùy ý Functor.
Luis Casillas

3
Một câu trả lời hợp lý về mặt kỹ thuật nhưng nó khiến tôi tự hỏi tại sao mọi người lại muốn điều này trong thực tế. Tôi không thấy mình đạt đến Haskell's Functorhay a SemiGroup. Các chương trình thực sử dụng đặc điểm ngôn ngữ này ở đâu?
JD

28

Tôi không muốn lặp lại thông tin trong một số câu trả lời xuất sắc đã có ở đây, nhưng có một điểm chính tôi muốn bổ sung.

Bạn thường không cần các loại có cấu trúc cao hơn để triển khai bất kỳ một đơn nguyên hoặc trình điều khiển cụ thể nào (hoặc trình điều khiển ứng dụng, hoặc mũi tên, hoặc ...). Nhưng làm như vậy là thiếu điểm.

Nói chung, tôi nhận thấy rằng khi mọi người không thấy sự hữu ích của functors / monads / whatevers, thường là vì họ đang nghĩ đến những thứ này tại một thời điểm . Các phép toán Functor / monad / etc thực sự không thêm gì vào bất kỳ trường hợp nào (thay vì gọi bind, fmap, v.v., tôi chỉ có thể gọi bất kỳ hoạt động nào tôi đã sử dụng để triển khai bind, fmap, v.v.). Những gì bạn thực sự muốn những sự trừu tượng này là để bạn có thể có mã hoạt động chung với bất kỳ chức năng / đơn nguyên / v.v. nào.

Trong bối cảnh mà mã chung như vậy được sử dụng rộng rãi, điều này có nghĩa là bất cứ khi nào bạn viết một phiên bản đơn nguyên mới, kiểu của bạn ngay lập tức có được quyền truy cập vào một số lượng lớn các thao tác hữu ích đã được viết cho bạn . Đó là điểm nhìn thấy các monads (và functors, và ...) ở khắp mọi nơi; không phải để tôi có thể sử dụng bindthay vì concatmapđể triển khai myFunkyListOperation(bản thân tôi chẳng thu được gì), mà là để khi tôi cần myFunkyParserOperationmyFunkyIOOperationtôi có thể sử dụng lại mã mà tôi đã thấy ban đầu trong danh sách vì nó thực sự là đơn nguyên .

Nhưng để trừu tượng hóa trên một kiểu được tham số hóa như một đơn nguyên với độ an toàn của kiểu , bạn cần các kiểu có định hướng cao hơn (cũng được giải thích trong các câu trả lời khác ở đây).


9
Đây gần như là một câu trả lời hữu ích hơn bất kỳ câu trả lời nào khác mà tôi đã đọc cho đến nay nhưng tôi vẫn muốn thấy một ứng dụng thực tế duy nhất trong đó các loại cao hơn hữu ích.
JD

"Những gì bạn thực sự muốn những sự trừu tượng này là để bạn có thể có mã hoạt động chung với bất kỳ chức năng / đơn nguyên nào". F # có monads dưới dạng biểu thức tính toán 13 năm trước, ban đầu là monads thể thao seq và async. Hôm nay F # tận hưởng đơn nguyên thứ 3, truy vấn. Với rất ít monads có quá ít điểm chung tại sao bạn lại muốn tóm tắt chúng?
JD

@JonHarrop Bạn nhận thức rõ ràng rằng những người khác đã viết mã bằng cách sử dụng số lượng lớn các monads (và các hàm, mũi tên, v.v.; HKT không chỉ là về monads) bằng các ngôn ngữ hỗ trợ HKT và tìm cách sử dụng để trừu tượng hóa chúng. Và rõ ràng là bạn không nghĩ rằng bất kỳ đoạn mã nào trong số đó có bất kỳ công dụng thực tế nào, và tò mò tại sao người khác lại muốn viết nó. Bạn hy vọng sẽ có được cái nhìn sâu sắc nào bằng cách quay lại bắt đầu cuộc tranh luận về một bài đăng cũ 6 năm mà bạn đã bình luận 5 năm trước?
Ben

"hy vọng sẽ đạt được bằng cách trở lại để bắt đầu cuộc tranh luận về một bài đăng 6 năm tuổi". Hồi tưởng. Với lợi ích của nhận thức muộn, giờ đây chúng ta biết rằng các bản tóm tắt của F # so với các đơn nguyên phần lớn vẫn chưa được sử dụng. Do đó, khả năng trừu tượng hóa 3 thứ phần lớn khác nhau là không thể thuyết phục.
JD

@JonHarrop Vấn đề của câu trả lời của tôi là các monads riêng lẻ (hoặc functors, hoặc v.v.) không thực sự hữu ích hơn bất kỳ chức năng tương tự nào được thể hiện mà không có giao diện du mục, nhưng việc hợp nhất nhiều thứ khác nhau là có. Tôi sẽ dựa vào kiến ​​thức chuyên môn của bạn về F #, nhưng nếu bạn đang nói rằng nó chỉ có 3 đơn nguyên riêng lẻ (thay vì triển khai giao diện đơn nguyên cho tất cả các khái niệm có thể có một, như thất bại, trạng thái, phân tích cú pháp, v.v.), thì vâng, không có gì ngạc nhiên khi bạn sẽ không nhận được nhiều lợi ích từ việc thống nhất 3 điều đó.
Ben

15

Để có góc nhìn cụ thể hơn về .NET, tôi đã viết một bài đăng trên blog về điều này một thời gian trước. Điểm mấu chốt của nó là, với các loại có định hướng cao hơn, bạn có thể sử dụng lại các khối LINQ giống nhau giữa IEnumerablesIObservables, nhưng nếu không có các loại định hướng cao hơn thì điều này là không thể.

Gần nhất bạn có thể nhận được (tôi đã tìm ra sau khi công bố các blog) là để làm của riêng bạn IEnumerable<T>IObservable<T>và mở rộng cả hai từ một IMonad<T>. Điều này sẽ cho phép bạn sử dụng lại khối LINQ của bạn nếu họ đang ký hiệu IMonad<T>, nhưng sau đó nó không còn typesafe vì nó cho phép bạn kết hợp và trận đấu IObservablesIEnumerablestrong cùng một khối, mà trong khi nó nghe có vẻ hấp dẫn để kích hoạt tính năng này, bạn muốn về cơ bản chỉ nhận được một số hành vi không xác định.

Tôi đã viết một bài đăng sau đó về cách Haskell thực hiện điều này dễ dàng. (Thực sự là không - giới hạn một khối đối với một loại đơn nguyên nhất định yêu cầu mã; cho phép sử dụng lại là mặc định).


2
Tôi sẽ cho bạn +1 vì là câu trả lời duy nhất đề cập đến điều gì đó thực tế nhưng tôi không nghĩ rằng mình đã từng sử dụng IObservablestrong mã sản xuất.
JD

5
@JonHarrop Điều này có vẻ không đúng sự thật. Trong F #, tất cả các sự kiện đều có IObservablevà bạn sử dụng các sự kiện trong chương WinForms của cuốn sách của riêng bạn.
Dax Fohl

1
Microsoft đã trả tiền cho tôi để viết cuốn sách đó và yêu cầu tôi trang bị tính năng đó. Tôi không thể nhớ việc sử dụng các sự kiện trong mã sản xuất nhưng tôi sẽ xem xét.
JD

Tôi cho rằng việc sử dụng lại giữa IQueryable và IEnumerable cũng có thể xảy ra
KolA

Bốn năm sau và tôi đã xem xét xong: chúng tôi đã loại bỏ Rx khỏi sản xuất.
JD

13

Ví dụ được sử dụng nhiều nhất về đa hình kiểu cao hơn trong Haskell là Monadgiao diện. FunctorApplicativeđược đánh giá cao hơn theo cách tương tự, vì vậy tôi sẽ trình bày Functorđể thể hiện điều gì đó súc tích.

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

Bây giờ, hãy kiểm tra định nghĩa đó, xem cách fsử dụng biến kiểu . Bạn sẽ thấy điều đó fkhông có nghĩa là một loại có giá trị. Bạn có thể xác định các giá trị trong chữ ký kiểu đó vì chúng là đối số và kết quả của một hàm. Vì vậy, các biến kiểu ablà các kiểu có thể có giá trị. Các biểu thức kiểu f af b. Nhưng không phải fchính nó. flà một ví dụ về một biến kiểu cao hơn. Cho rằng đó *là loại kiểu có thể có giá trị, fphải có kiểu * -> *. Đó là, nó cần một kiểu có thể có giá trị, bởi vì chúng ta biết từ lần kiểm tra trước rằng abphải có giá trị. Và chúng tôi cũng biết rằng f af b phải có giá trị, vì vậy nó trả về một kiểu phải có giá trị.

Điều này làm cho biến fđược sử dụng trong định nghĩa của Functormột loại biến kiểu cao hơn.

Các giao diện ApplicativeMonadthêm nhiều hơn nữa, nhưng chúng tương thích. Điều này có nghĩa là chúng cũng hoạt động trên các biến kiểu với loại * -> *.

Làm việc trên các kiểu cao hơn giới thiệu một mức độ trừu tượng bổ sung - bạn không bị hạn chế chỉ tạo ra các kiểu trừu tượng hơn các kiểu cơ bản. Bạn cũng có thể tạo trừu tượng trên các kiểu sửa đổi các kiểu khác.


4
Một lời giải thích kỹ thuật tuyệt vời khác về những loại cao hơn là gì khiến tôi tự hỏi chúng hữu ích cho việc gì. Bạn đã tận dụng điều này ở đâu trong mã thực?
JD
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.