Rumah > Artikel > pembangunan bahagian belakang > Corak Fungsian: Antara Muka dan Fungsi
Ini adalah bahagian 3 daripada siri artikel bertajuk Corak Fungsian.
Pastikan anda menyemak artikel yang lain!
- Monoid
- Komposisi dan Tersirat
Untuk menjadi betul, fungsi mesti menaip-semak, dan oleh itu boleh dibuktikan. Tetapi dalam kes fungsi generalisasi, bertujuan untuk menangani pelbagai jenis, ini serta-merta menunjukkan sebagai titik kesakitan. Untuk menjadikan fungsi dwi berfungsi merentas jenis, kami perlu mentakrifkannya secara berasingan!
doubleInt :: Int -> Int doubleChar :: Char -> Char doubleFloat :: Float -> Float -- ...
Dan bagi mana-mana pengaturcara yang menghargai diri sendiri, anda sepatutnya mendapati diri anda benar-benar terkejut dengan perkara ini. Kami baru sahaja belajar tentang corak untuk membina pengendalian kes menggunakan aplikasi separa tetapi kami tidak boleh menggunakannya di sini kerana tandatangan jenis kami tidak membenarkannya, dan fungsi kami telah untuk menaip-semak.
Syukurlah, ini sudah menjadi ciri dalam kebanyakan bahasa pengaturcaraan moden. Kami dibenarkan untuk menentukan jenis generik. Jenis hipotesis yang hanya perlu mengesahkan kedudukan yang sepadan dalam tandatangan fungsi atau pengisytiharan berubah.
// c++ template <typename T> T double(T x) { return x*2; }
// rust fn double<T>(x: T) -> T { return x*2; }
-- haskell double :: a -> a double = (*2) -- partially applied multiplication
Dan itu sepatutnya menyelesaikan masalah kita! Selagi pengkompil diberikan generik ini, ia boleh mengetahui jenis yang perlu digunakan pada masa larian (Rust sebenarnya masih melakukan inferens ini pada masa kompilasi!).
Walau bagaimanapun, walaupun terdapat merit dalam pelaksanaan ini— masih terdapat kelemahan yang ketara, yang sebenarnya ditunjukkan oleh pengkompil Haskell, kerana kod Haskell di atas sebenarnya menimbulkan ralat.
Tiada contoh untuk 'Num a' yang timbul daripada penggunaan '*'...
Kami telah menentukan jenis, tetapi kami tidak selalu memastikan jenis ini mempunyai kapasiti untuk digandakan. Sudah tentu, ini berfungsi dengan serta-merta pada nombor, tetapi apakah yang menghalang pengguna daripada memanggil dua kali pada String? Satu senarai? Tanpa kaedah yang dipratakrifkan untuk menggandakan jenis ini, mereka tidak seharusnya dibenarkan sebagai hujah, pada mulanya.
Jadi bertentangan dengan nama generik, kita perlu mendapatkan sedikit lagi khusus, tetapi masih umum.
Di sinilah kelas jenis masuk, atau juga lebih dikenali dalam dunia imperatif sebagai antara muka. Sekali lagi, jika anda menggunakan mana-mana bahasa yang telah dibuat lewat daripada C++, anda sepatutnya mempunyai akses kepada beberapa pelaksanaan antara muka.
Antara muka, berbanding generik, menyatakan sejenis keupayaan jenis yang boleh dikategorikan di bawahnya.
Berikut ialah versi tetap kod kami yang terdahulu.
double :: (Num a) => a -> a -- a has to be of typeclass Num double = (*2)
atau dalam Go:
// We first create an interface that is the union of floats and integers. type Num interface { ~int | ~float64 // ... plus all other num types } func double[T Num](a T) T { return a * 2 }
Demi ringkasnya, kami akan mengatakan bahawa Haskell tidak benar-benar menangani keadaan terbenam dalam antara muka mereka, seperti antara muka Typescript dan Go (kekangan yang dibawa oleh peraturan fungsi tulen). Jadi, walaupun anda mungkin boleh mentakrifkan atribut yang diperlukan bagi jenis yang berada di bawah antara muka, ketahui bahawa antara muka tulen hanya perlu menentukan fungsi atau keupayaan jenis.
Dan mengikut keupayaan dalam konteks ini, kita bercakap tentang jika jenis itu mempunyai kebergantungan dalam bentuk fungsi penggandaan— adakah pengkompil ajar bagaimana untuk menggandakannya?
import Control.Monad (join) class CanDouble a where double :: a -> a instance CanDouble Int where double = (* 2) instance CanDouble Float where double = (* 2) -- we tell the compiler that doubling a string is concatenating it to itself. instance CanDouble String where double = join (++) -- W-combinator, f x = f(x)(x)
Dan kini kita hampir kembali ke tempat kita pada mulanya apabila melibatkan pengulangan kod, bukankah itu lucu?
Tetapi kawalan pelaksanaan yang terperinci ini sebenarnya di mana kuasa ini datang. Jika anda pernah mendengar tentang corak Strategi sebelum ini, ini hampir sama, dari segi fungsi.
quadruple :: (CanDouble a) => a -> a quadruple = double . double leftShift :: (CanDouble a) => Int -> a -> a leftShift n e | e <= 0 = n | otherwise = leftShift (double n) $ e - 1
Fungsi ini taip-semak sekarang, semuanya kerana kami mengajar pengkompil cara menaip berganda di bawah kelas taip CanDouble.
Kami boleh mencapai sesuatu yang serupa dalam Go, satu kaveat besar ialah kami hanya boleh menentukan kaedah antara muka pada jenis bukan primitif. Maksudnya, kita perlu menentukan struktur pembalut kepada jenis primitif.
type CanDouble interface { double() CanDouble } type String string type Number interface { ~int | ~float64 // ... plus all other num types } type Num[T Number] struct { v T } func (s String) double() String { return s + s } func (n Num[T]) double() Num[T] { return Num[T]{n.v * 2} } func quadruple(n CanDouble) CanDouble { return n.double().double() } func leftShift(n CanDouble, e uint) CanDouble { for i := uint(0); i < e; i++ { n = n.double() } return n }
Sejujurnya ini agak mengecewakan, tetapi jangan risau, kerana kebanyakan masa anda akan berurusan dengan antara muka adalah dengan jenis dan struktur tersuai.
Teori kategori ialah teori umum struktur matematik dan hubungannya.
Kami telah meneliti secara ringkas teori kategori kembali dalam The Monoid, dan kami ingin mengekalkannya seperti itu, hanya pertemuan rapat. Saya akan merujuknya di sana sini, tetapi yakinlah: anda tidak perlu mempunyai latar belakang untuk memahami apa sahaja yang berikut.
Walau bagaimanapun, tidak dinafikan bahawa kami pernah menemui set sebelum ini.
Sebagai rekap ringkas, Set boleh dianggap sebagai koleksi elemen. Elemen ini boleh menjadi apa sahaja.
{ 0, 1, 2, 3, ... } -- the set of natural numbers { a, b, c, ..., z} -- the set of lowercase letters { abs, min, max, ... } -- the set of `Math` functions in Javascript { {0, 1}, {a, b}, {abs, min} } -- the set of sets containing the first 2 elements of the above sets
Adding on to that, we have these things called morphisms, which we can think of a mapping between elements.
Very big omission here on the definitions of morphisms, in that they are relations between elements, and not strictly functions/mappings,
you can look it up if you are curious.
We can say a function like toUpper() is a morphism between lowercase letters to uppercase letters, just like how we can say double = (*2) is a morphism from numbers to numbers (specifically even numbers).
And if we group these together, the set of elements and their morphisms, we end up with a category.
Again, omission, categories have more constraints such as a Composition partial morphism and identities. But these properties are not that relevant here.
If you have a keen eye for patterns you'd see that there is a parallel to be drawn between categories and our interfaces! The objects (formal name for a category's set of elements) of our category are our instances, and our implementations are our morphisms!
class CanDouble a where double :: a -> a -- `Int` is our set of elements { ... -1, 0, 1, ... } -- `(* 2)` is a morphism we defined -- ... (other omissions) -- ... -- Therefore, `CanDouble Int` is a Category. instance CanDouble Int where double = (* 2)
Man, that was a lot to take in. Here's a little bit more extra:
A Functor is a type of a function (also known as a mapping) from category to another category (which can include itself, these are called endofunctors).
What this essentially means, is that it is a transformation on some category that maps every element to a corresponding element, and every morphism to a corresponding morphism. An output category based on the input category.
In Haskell, categories that can be transformed by a functor is described by the following typeclass (which also makes it a category in of itself, that's for you to ponder):
class Functor f where fmap :: (a -> b) -> f a -> f b -- ...
f here is what we call a type constructor. By itself it isn't a concrete type, until it is accompanied by a concrete type. An example of this would be how an array isn't a type, but an array of Int is. The most common form of a type constructor is as a data type (a struct).
From this definition we can surmise that all we need to give to this function fmap is a function (a -> b) (which is our actual functor, don't think about the naming too much), and this would transform a type f a to type f b, a different type in the same category.
Yes, this means Haskell's Functor typeclass is actually a definition for endofunctors, woops!
If all of that word vomit was scary, a very oversimplified version for the requirement of the Functor typeclass is that you are able to map values to other values in the same category.
Arguably the most common Functor we use are arrays:
instance Functor [] where -- fmap f [] = [] -- fmap f (a:as) = f a : fmap as -- simplified fmap :: (a -> b) -> [a] -> [b] fmap f arr = map f arr
We are able to map an array of [a] to [b] using our function (or functor) f. The typeconstructor of [] serves as our category, and so our functor is a transformation from one type of an array to another.
So, formally: the map function, though commonly encountered nowadays in other languages and declarative frameworks such as React, is simply the application of an endofunctor on the category of arrays.
Wow. That is certainly a description.
Here are more examples of functors in action:
// Go type Functor[T any] interface { fmap(func(T) T) Functor[T] } type Pair[T any] struct { a T b T } type List[T any] struct { get []T } // Applying a functor to a Pair is applying the function // to both elements func (p *Pair[T]) fmap(f func(T) T) Pair[T] { return Pair[T]{ // apply f to both a and b f(p.a), f(p.b), } } func (a *List[T]) fmap(f func(T) T) List[T] { res := make([]T, len(a.get)) // create an array of size len(a.get) for i, v := range a.get { res[i] = f(v) } return List[T]{res} }
-- haskell data Pair t = P (t, t) instance Functor Pair where fmap f (P (x, y)) = P (f x, f y)
So all that it takes to fall under the Functor (again, endofunctor), interface is to have a definition on how to map the contents of the struct to any other type (including its own).
This is another simplifcation, functors also need to have property of identity and composition.
To put simply, whenever you do a map, you're not only transforming the elements of your array (or struct), you're also transforming the functions you are able to apply on this array (or struct). This is what we mean by mapping both objects and morphisms to different matching objects and morphisms in the same category.
This is important to note as even though we end up in the same category (in this context, we map an array, which results in another array), these might have differing functions or implementations available to them (though most of them will be mapped to their relatively equivalent functions, such as a reverse on an array of Int to reverse on an array of Float).
Di sinilah penyederhanaan yang berlebihan agak mengelirukan kita, kerana jika kita mengikut definisi kita sahaja, kita boleh mengatakan bahawa fungsi pengurangan seperti jumlah dan concat adalah fungsi daripada kategori tatasusunan kepada atom, tetapi ini tidak semestinya benar. Oleh kerana functors juga memerlukan anda memelihara struktur kategori, yang tidak akan dibincangkan dalam siri artikel ini kerana itu terlalu berakar umbi dalam teori kategori.
Maaf jika artikel ini mengandungi cara lebih banyak matematik daripada aplikasi, tetapi memahami definisi ini akan membantu kami memahami corak yang lebih sukar kemudian dalam siri ini, iaitu Aplikatif dan akhirnya Monad.
Monad ialah monoid dalam kategori endofunctor.
Kami sampai ke sana! :>
Atas ialah kandungan terperinci Corak Fungsian: Antara Muka dan Fungsi. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!