"Isomorfisme" adalah salah satu konsep dasar matematika modern. Menggunakan contoh-contoh konkret dalam Haskell dan C #, saya tidak hanya akan menjelaskan teori untuk non-matematikawan (tanpa menggunakan simbol dan istilah matematika yang tidak jelas), tetapi saya juga akan menunjukkan bagaimana ini dapat digunakan dalam praktik sehari-hari.
Masalahnya adalah bahwa kesetaraan yang ketat (misalnya, 2 + 2 = 4) seringkali terlalu ketat. Berikut ini sebuah contoh:
Haskelladd :: (a, a) -> a add (x, y) = x + y
C # int Add(Tuple<int, int> pair) { return pair.Item1 + pair.Item2; }
Namun, ada satu lagi - lebih rumit dan dalam banyak situasi jauh lebih praktis - cara untuk mendefinisikan fungsi yang sama dalam arti :
Haskell add' :: a -> a -> a add' x = \y -> x + y
C # Func<int, int> Add_(int x) { return y => x + y; }
Bertentangan dengan fakta yang jelas bahwa untuk dua x, y kedua fungsi akan selalu mengembalikan hasil yang sama, mereka tidak memenuhi kesetaraan yang ketat:
- fungsi pertama segera mengembalikan jumlah (mis., melakukan perhitungan pada saat ekspor),
- sementara fungsi kedua mengembalikan fungsi lain (yang pada akhirnya akan mengembalikan jumlah - jika seseorang memanggilnya, tentu saja, jika tidak, perhitungan tidak akan dilakukan: ini adalah contoh dari perhitungan yang tertunda dan ada juga isomorfisma di sini, yang akan saya kembalikan ke sedikit kemudian).
Dan ini "terlalu ketat."
Isomorfisme "cukup ketat"; ia tidak membutuhkan kesetaraan yang lengkap dan mencakup semua, tetapi terbatas pada kesetaraan "dalam arti", yang selalu ditentukan oleh konteks tertentu.
Seperti yang Anda duga, kedua definisi di atas adalah isomorfik. Ini berarti persis sebagai berikut: jika hanya salah satu dari mereka diberikan kepada saya, maka keduanya diberikan kepada saya secara implisit : semua berkat isomorfisme - konverter dua arah dari satu ke yang lain . Ringkas jenis-jenisnya:
Haskell curry :: ((a, b) → c) → a → b → c curry fxy = f (x, y), uncurry :: (a → b → c) → (a, b) → c uncurry f (x, y) = fxy
C # Func<TArg1, Func<TArg2, TRes>> Curry(Func<Tuple<TArg1, TArg2>, TRes> uncurried) { return arg1 => arg2 => uncurried(Tuple.Create(arg1, arg2)); } Func<Tuple<TArg1, TArg2>, TRes> Uncurry(Func<TArg1, Func<TArg2, TRes>> curried) { return pair => curried(pair.Item1)(pair.Item2); }
... dan sekarang untuk setiap x, y :
Haskell curry add $ x, y = uncurry add' $ (x, y)
C # Curry(Add)(x)(y) = Uncurry(Add_)(Tuple.Create(x, y))
Sedikit lebih banyak matematika untuk terutama yang penasaranBahkan, seharusnya terlihat seperti ini:
Haskell curry . uncurry = id uncurry . curry = id id x = x
C # Compose(Curry, Uncurry) = Id Compose(Uncurry, Curry) = Id, : T Id<T>(T arg) => arg; Func<TArg, TFinalRes> Compose<TArg, TRes, TFinalRes>( Func<TArg, TRes> first, Func<TRes, TFinalRes> second) { return arg => second(first(arg)); } ... extension- ( Id ): Curry.Compose(Uncurry) = Id Uncurry.Compose(Curry) = Id, : public static Func<TArg, TFinalRes> Compose<TArg, TRes, TFinalRes>( this Func<TArg, TRes> first, Func<TRes, TFinalRes> second) { return arg => second(first(arg)); }
Id harus dipahami sebagai "tidak ada yang terjadi." Karena isomorfisma adalah transformator dua arah, Anda selalu dapat 1) mengambil satu hal, 2) mengubahnya menjadi yang lain, dan 3) mengubahnya kembali menjadi yang pertama. Hanya ada dua operasi seperti itu: karena pada tahap pertama (No. 1), pilihannya hanya dari dua opsi. Dan dalam kedua kasus, operasi harus mengarah pada hasil yang persis sama, seolah-olah tidak ada yang terjadi sama sekali (karena alasan inilah kesetaraan yang ketat terlibat - karena tidak ada yang berubah sama sekali, dan bukan "sesuatu" tidak berubah).
Selain itu, ada teorema bahwa elemen id selalu unik. Perhatikan bahwa fungsi Id adalah generik, polimorfik dan karenanya benar-benar unik sehubungan dengan setiap jenis tertentu.
Isomorfisme sangat, sangat berguna justru karena ketat, tetapi tidak terlalu banyak. Ini mempertahankan properti penting tertentu (dalam contoh di atas - hasil yang sama dengan argumen yang sama), sementara memungkinkan Anda untuk secara bebas mengubah struktur data itu sendiri (pembawa perilaku dan properti isomorfik). Dan ini benar-benar aman - karena isomorfisme selalu bekerja di kedua arah, yang berarti Anda selalu dapat kembali tanpa kehilangan "properti penting" tersebut. Saya akan memberikan contoh lain yang sangat berguna dalam praktiknya sehingga bahkan mendukung banyak bahasa pemrograman "maju" seperti Haskell:
Haskell toLazy :: a -> () -> a toLazy x = \_ -> a fromLazy :: (() -> a) -> a fromLazy f = f ()
C # Func<TRes> Lazy(TRes res) { return () => res; } TRes Lazy(Func<TRes> lazy) { return lazy(); }
Isomorfisma ini mempertahankan hasil perhitungan yang ditangguhkan itu sendiri - ini adalah "properti penting", sementara struktur datanya berbeda.
Kesimpulannya? OOP, khususnya sangat diketik, (dipaksa) bekerja pada tingkat "kesetaraan ketat". Dan oleh karena itu - setelah contoh-contoh di atas - seringkali terlalu ketat. Ketika Anda terbiasa berpikir "terlalu ketat" (dan ini terjadi tanpa terasa - itu bocor ke programmer, terutama jika dia tidak mencari inspirasi dalam matematika), keputusan Anda tanpa disadari kehilangan fleksibilitas yang mereka inginkan (atau setidaknya secara objektif dimungkinkan). Memahami isomorfisme - dalam komunitas dengan upaya sadar untuk lebih memperhatikan komunitasnya sendiri
dan kode asing - ini membantu untuk lebih jelas mendefinisikan lingkaran "properti penting", abstrak dari detail yang tidak perlu: yaitu, dari struktur data spesifik di mana "properti penting" ini dicetak (mereka juga "detail implementasi", dalam hal ini). Pertama-tama, ini adalah cara berpikir, dan hanya kemudian - solusi arsitektur (mikro) yang lebih sukses dan, sebagai konsekuensi alami, pendekatan revisi untuk pengujian.
NB Jika saya melihat bahwa artikel tersebut telah menguntungkan, maka saya akan kembali ke topik "solusi arsitektur (mikro) yang lebih sukses" dan "pendekatan revisi untuk pengujian".