Performa penjadwalan berganda yang sulit dipahami

Di bawah cutscene, dekripsi laporan oleh Stefan Karpinsky, salah satu pengembang utama bahasa Julia, diusulkan. Dalam laporan itu, ia membahas hasil tak terduga dari pengiriman ganda yang nyaman dan efisien, diambil sebagai paradigma utama Julia.



Dari seorang penerjemah : judul laporan merujuk pada sebuah artikel oleh Eugene Wigner, "Efektivitas Matematika yang Tidak Terbandingkan dalam Ilmu Pengetahuan Alam . "


Penjadwalan ganda adalah paradigma kunci dari bahasa Julia, dan selama keberadaannya, kami, para pengembang bahasa, memperhatikan sesuatu yang diharapkan, tetapi pada saat yang sama membingungkan. Setidaknya kita tidak mengharapkan ini sejauh kita melihatnya. Ini adalah sesuatu - tingkat penggunaan kembali kode yang mengejutkan di ekosistem Julia, yang jauh lebih tinggi daripada bahasa lain yang saya tahu.


Kami terus-menerus melihat bahwa beberapa orang menulis kode umum, orang lain mendefinisikan tipe data baru, orang-orang ini tidak mengenal satu sama lain, dan kemudian seseorang menerapkan kode ini ke tipe data yang tidak biasa ini ... Dan semuanya hanya berfungsi. Dan ini sering terjadi secara mengejutkan .
Saya selalu berpikir bahwa perilaku tersebut harus diharapkan dari pemrograman berorientasi objek, tapi aku menikmati banyak bahasa berorientasi objek, dan ternyata mereka biasanya sangat sederhana tidak bekerja. Karena itu, pada titik tertentu saya berpikir: mengapa Julia menjadi bahasa yang begitu efektif dalam hal ini? Mengapa level penggunaan kembali kode begitu tinggi di sana? Dan juga - pelajaran apa yang bisa dipetik dari ini yang bisa dipinjam oleh bahasa lain dari Julia agar menjadi lebih baik?


Kadang-kadang, ketika saya mengatakan bahwa masyarakat tidak percaya, tapi Anda harus JuliaCon, sehingga Anda tahu apa yang terjadi, jadi saya akan fokus pada mengapa, dalam pandangan saya, hal ini terjadi.


Tetapi sebagai permulaan - salah satu contoh favorit saya.



Pada slide adalah hasil karya Chris Rakaukas. Dia menulis semua jenis paket yang sangat umum untuk menyelesaikan persamaan diferensial. Anda dapat memberi makan nomor ganda , atau BigFloat, apa pun yang Anda inginkan. Dan entah bagaimana dia memutuskan bahwa dia ingin melihat kesalahan hasil integrasi. Dan ada paket Pengukuran yang dapat melacak nilai kuantitas fisik dan propagasi kesalahan melalui urutan formula. Paket ini juga mendukung sintaks elegan untuk nilai ketidakpastian menggunakan karakter Unicode ± . Di sini pada slide ditunjukkan bahwa percepatan gravitasi, panjang pendulum, kecepatan awal, sudut penyimpangan semua diketahui dengan beberapa jenis kesalahan. Jadi, Anda mendefinisikan pendulum sederhana, lulus persamaan gerak melalui pemecah ODE dan - bam! - semuanya berfungsi . Dan Anda melihat grafik dengan ketidakakuratan kumis. Dan saya masih tidak menunjukkan bahwa kode untuk menggambar grafik juga digeneralisasi, dan Anda hanya memasukkan nilai dengan kesalahan dari Measurements.jl dan mendapatkan grafik dengan kesalahan.


Tingkat kompatibilitas paket-paket yang berbeda dan generalisasi dari kode itu hanya menyita pikiran. Bagaimana cara kerjanya ? Ternyata iya.


Yah, bukannya kita tidak mengharapkan ini sama sekali. Bagaimanapun, kami memasukkan konsep pengiriman ganda dalam bahasa justru karena itu memungkinkan kami untuk mengekspresikan algoritma umum. Jadi semua hal di atas tidak terlalu gila. Tetapi satu hal untuk mengetahui hal ini dalam teori, dan hal lain untuk dilihat dalam praktik bahwa pendekatan tersebut benar-benar berfungsi. Setelah semua, pengiriman tunggal dan kelebihan operator di C ++ juga harus memberikan hasil yang serupa - tetapi pada kenyataannya mereka sering tidak bekerja seperti yang mereka inginkan.


Selain itu, kami menyaksikan sesuatu yang lebih dari yang kami perkirakan saat mengembangkan bahasa: bukan hanya kode umum yang sedang ditulis. Selanjutnya, saya akan mencoba mengatakan apa, menurut saya, ini lebih.


Jadi, ada dua jenis penggunaan kembali kode, dan mereka sangat berbeda. Salah satunya adalah algoritma umum, dan ini adalah hal pertama yang mereka ingat. Aspek kedua, kurang jelas, tetapi tampaknya lebih penting adalah kesederhanaan yang digunakan Julia untuk tipe data yang sama dalam berbagai paket. Untuk beberapa hal, ini terjadi karena metode tipe tidak menjadi penghambat untuk penggunaannya: Anda tidak perlu setuju dengan penulis tipe tentang antarmuka dan metode yang diwarisi; Anda bisa mengatakan: "Oh, saya suka jenis RGB ini. Saya akan membuat sendiri operasi di atasnya, tapi saya suka strukturnya."


Kata Pengantar Penjadwalan berganda versus fungsi yang berlebihan


Sekarang saya harus menyebutkan fungsi overloading di C ++ atau Java, karena saya terus bertanya tentang mereka. Sepintas, tidak ada bedanya dengan penjadwalan berganda. Apa perbedaannya dan mengapa kelebihan fungsi lebih buruk?


Saya akan mulai dengan contoh pada Julia:


 abstract type Pet end struct Dog <: Pet; name::String end struct Cat <: Pet; name::String end function encounter(a::Pet, b::Pet) verb = meets(a, b) println("$(a.name) meets $(b.name) and $verb") end meets(a::Dog, b::Dog) = "sniffs" meets(a::Dog, b::Cat) = "chases" meets(a::Cat, b::Dog) = "hisses" meets(a::Cat, b::Cat) = "slinks" 

Kami mendefinisikan tipe abstrak Pet , mengenalkan subtipe Dog dan Cat untuknya, mereka memiliki bidang nama (kode sedikit berulang, tetapi dapat ditoleransi) dan mendefinisikan fungsi umum "pertemuan" yang mengambil dua objek bertipe Pet argumen. Di dalamnya, pertama-tama kita menghitung "tindakan" yang ditentukan oleh hasil memanggil fungsi generalisasi meet() , dan kemudian mencetak kalimat yang menggambarkan pertemuan tersebut. Dalam fungsi meets() , kami menggunakan beberapa pengiriman untuk menentukan tindakan yang dilakukan satu hewan ketika bertemu lainnya.


Tambahkan beberapa anjing dan beberapa kucing dan lihat hasil pertemuan:


 fido = Dog("Fido") rex = Dog("Rex") whiskers = Cat("Whiskers") spots = Cat("Spots") encounter(fido, rex) encounter(rex, whiskers) encounter(spots, fido) encounter(whiskers, spots) 

Sekarang kita akan "menerjemahkan" hal yang sama ke dalam C ++ sejelas mungkin. Kami mendefinisikan kelas Pet dengan bidang name - dalam C ++ kita dapat melakukan ini (omong-omong, salah satu keuntungan dari C ++ adalah bahwa bidang data bahkan dapat ditambahkan ke tipe abstrak. Kemudian kita mendefinisikan basis meets() fungsi, menentukan fungsi encounter() fungsi untuk dua objek dari tipe Pet dan, akhirnya, tentukan kelas turunan Dog and Cat dan lakukan overload meets() untuk mereka:


 class Pet { public: string name; }; string meets(Pet a, Pet b) { return "FALLBACK"; } void encounter(Pet a, Pet b) { string verb = meets(a, b); cout << a.name << " meets " << b. name << " and " << verb << endl; } class Cat : public Pet {}; class Dog : public Pet {}; string meets(Dog a, Dog b) { return "sniffs"; } string meets(Dog a, Cat b) { return "chases"; } string meets(Cat a, Dog b) { return "hisses"; } string meets(Cat a, Cat b) { return "slinks"; } 

Fungsi main() , seperti dalam kode Julia, membuat anjing dan kucing dan membuat mereka bertemu:


 int main() { Dog fido; fido.name = "Fido"; Dog rex; rex.name = "Rex"; Cat whiskers; whiskers.name = "Whiskers"; Cat spots; spots.name = "Spots"; encounter(fido, rex); encounter(rex, whiskers); encounter(spots, fido); encounter(whiskers, spots); return 0; } 

Jadi, banyak pengiriman terhadap fungsi kelebihan beban. Gong!



Menurut Anda apa yang akan mengembalikan kode dengan beberapa pengiriman?


$ julia pets.jl
 Fido meets Rex and sniffs Rex meets Whiskers and chases Spots meets Fido and hisses Whiskers meets Spots and slinks 

Hewan-hewan bertemu, mengendus, mendesis dan bermain mengejar - seperti yang dimaksudkan.


$ g ++ -o pet pets.cpp && ./pets
 Fido meets Rex and FALLBACK Rex meets Whiskers and FALLBACK Spots meets Fido and FALLBACK Whiskers meets Spots and FALLBACK 

Dalam semua kasus, opsi "fallback" dikembalikan.


Mengapa Karena ini adalah cara kerja overloading fungsi. Jika beberapa pengiriman berfungsi, maka meets(a, b) encounter() dalam encounter() akan dipanggil dengan tipe spesifik yang dimiliki a dan b pada saat panggilan berlangsung. Tapi kelebihan beban diterapkan, oleh karena itu meets() dipanggil untuk tipe statis a dan b , yang keduanya dalam hal ini adalah Pet .


Jadi, dalam pendekatan C ++, "terjemahan" langsung dari kode Julia yang digeneralisasi tidak memberikan perilaku yang diinginkan karena fakta bahwa kompiler menggunakan tipe yang diturunkan secara statis pada tahap kompilasi. Dan intinya adalah bahwa kita ingin memanggil fungsi berdasarkan tipe beton nyata yang variabel miliki dalam runtime. Fungsi templat, meskipun agak memperbaiki situasinya, masih membutuhkan pengetahuan tentang semua jenis yang termasuk dalam ekspresi secara statis pada waktu kompilasi, dan mudah untuk menghasilkan contoh di mana ini tidak mungkin.


Bagi saya, contoh-contoh seperti itu menunjukkan bahwa pengiriman ganda melakukan hal yang benar, dan semua pendekatan lain bukanlah pendekatan yang sangat baik untuk hasil yang benar.


Sekarang mari kita lihat tabel seperti itu. Saya harap Anda menganggapnya bermakna:


Jenis penjadwalanSintaksArgumen pengirimanTingkat ekspresiPeluang ekspresif
tidakf (x 1 , x 2 , ...){}O (1)konstan
kesendirianx 1 .f (x 2 , ...){x 1 }O (| X 1 |)linier
bergandaf (x 1 , x 2 , ...){x 1 , x 2 , ...}O (| X 1 | | X 2 | ...)eksponensial

Dalam bahasa tanpa pengiriman, Anda cukup menulis f(x, y, ...) , jenis semua argumen sudah diperbaiki, mis. panggilan ke f() adalah panggilan ke fungsi tunggal f() , yang mungkin ada dalam program. Tingkat ekspresifnya konstan: memanggil f() selalu melakukan satu dan hanya satu hal. Pengiriman tunggal adalah terobosan besar dalam transisi ke OOP pada 1990-an dan 2000-an. Sintaks dot biasanya digunakan, yang sangat disukai orang. Dan kesempatan ekspresif tambahan muncul: panggilan dikirim sesuai dengan jenis objek x 1 . Peluang ekspresif ditandai oleh kekuatan set | X 1 | tipe yang memiliki metode f() . Namun, dalam pengiriman ganda, jumlah opsi potensial untuk fungsi f() sama dengan kekuatan produk Cartesian dari set tipe yang menjadi tempat argumen. Pada kenyataannya, tentu saja, hampir tidak ada orang yang membutuhkan begitu banyak fungsi berbeda dalam satu program. Tetapi poin kunci di sini adalah bahwa programmer diberikan cara yang sederhana dan alami untuk menggunakan elemen apa pun dari varietas ini, dan ini mengarah pada pertumbuhan peluang secara eksponensial.


Bagian 1. Pemrograman umum


Mari kita bicara tentang kode umum - fitur utama pengiriman ganda.


Berikut adalah contoh kode generik (sepenuhnya tiruan):


 using LinearAlgebra function inner_sum(A, vs) t = zero(eltype(A)) for v in vs t += inner(v, A, v) #  ! end return t end inner(v, A, w) = dot(v, A * w) #    

Di sini A adalah sesuatu yang mirip matriks (walaupun saya tidak menunjukkan jenisnya, dan saya dapat menebak sesuatu berdasarkan nama), vs adalah vektor dari beberapa elemen yang mirip vektor, dan kemudian produk skalar dipertimbangkan melalui "matriks" ini, di mana definisi umum diberikan tanpa menentukan jenis apa pun. Pemrograman umum di sini terdiri dari panggilan fungsi inner() dalam satu lingkaran (saran profesional: jika Anda ingin menulis kode umum - cukup hapus semua batasan jenis).


Jadi, "lihat, bu, itu berhasil":


 julia> A = rand(3, 3) 3×3 Array{Float64,2}: 0.934255 0.712883 0.734033 0.145575 0.148775 0.131786 0.631839 0.688701 0.632088 julia> vs = [rand(3) for _ in 1:4] 4-element Array{Array{Float64,1},1}: [0.424535, 0.536761, 0.854301] [0.715483, 0.986452, 0.82681] [0.487955, 0.43354, 0.634452] [0.100029, 0.448316, 0.603441] julia> inner_sum(A, vs) 6.825340887556694 

Tidak ada yang istimewa, itu menghitung beberapa nilai. Tapi - kode ini ditulis dalam gaya umum dan akan bekerja untuk A dan vs , jika saja akan memungkinkan untuk melakukan operasi yang sesuai pada mereka.


Adapun efisiensi pada tipe data tertentu - betapa beruntungnya. Maksud saya untuk vektor dan matriks yang padat kode ini akan melakukannya "sebagaimana mestinya" - itu akan menghasilkan kode mesin dengan doa operasi BLAS, dll. dll. Jika Anda melewatkan array statis, maka kompiler akan mempertimbangkan ini, perluas siklusnya, terapkan vektorisasi - semuanya sudah sebagaimana mestinya.


Tetapi yang lebih penting, kode ini akan bekerja untuk tipe baru, dan Anda dapat membuatnya tidak hanya sangat efisien, tetapi juga sangat efisien! Mari kita tentukan tipe baru (ini adalah tipe data nyata yang digunakan dalam pembelajaran mesin), vektor kesatuan (vektor satu-panas). Ini adalah vektor di mana salah satu komponennya adalah 1, dan yang lainnya nol. Anda dapat membayangkannya dengan sangat ringkas: semua yang perlu disimpan adalah panjang vektor dan jumlah komponen bukan nol.


 import Base: size, getindex, * struct OneHotVector <: AbstractVector{Int} len :: Int ind :: Int end size(v::OneHotVector) = (v.len,) getindex(v::OneHotVector, i::Integer) = Int(i == v.ind) 

Bahkan, ini benar-benar definisi tipe keseluruhan dari paket yang menambahkannya. Dan dengan definisi ini, inner_sum() juga berfungsi:


 julia> vs = [OneHotVector(3, rand(1:3)) for _ in 1:4] 4-element Array{OneHotVector,1}: [0, 1, 0] [0, 0, 1] [1, 0, 0] [1, 0, 0] julia> inner_sum(A, vs) 2.6493739294755123 

Tetapi untuk produk skalar, definisi umum digunakan di sini - untuk jenis data ini lambat, tidak keren!


Jadi, definisi umum berfungsi, tetapi tidak selalu secara optimal, dan Anda kadang-kadang dapat menemukan ini ketika menggunakan Julia: "baiklah, definisi umum dipanggil, itu sebabnya kode GPU ini telah bekerja selama lima jam ..."


Di inner() secara default, definisi umum dari produk matriks oleh vektor disebut, yang ketika dikalikan dengan vektor kesatuan mengembalikan salinan salah satu kolom dari tipe Vector{Float64} . Kemudian definisi umum dari produk skalar dot() disebut dengan vektor kesatuan dan kolom ini, yang melakukan banyak pekerjaan yang tidak perlu. Bahkan, untuk setiap komponen dicentang "apakah Anda sama dengan satu? Dan Anda?" dll.


Kami dapat sangat mengoptimalkan prosedur ini. Misalnya, mengganti perkalian matriks dengan OneHotVector hanya dengan memilih kolom. Baik, tentukan metode ini, dan hanya itu.


 *(A::AbstractMatrix, v::OneHotVector) = A[:, v.ind] 

Dan ini dia, kekuatan : kita mengatakan "kita ingin membahas argumen kedua, " tidak peduli apa yang ada di argumen pertama. Definisi seperti itu hanya akan menarik baris keluar dari matriks dan akan jauh lebih cepat daripada metode umum - iterasi dan penjumlahan kolom dihapus.


Tetapi Anda dapat melangkah lebih jauh dan langsung mengoptimalkan inner() , karena mengalikan dua vektor kesatuan melalui sebuah matriks cukup mengeluarkan elemen dari matriks ini:


 inner(v::OneHotVector, A, w::OneHotVector) = A[v.ind, w.ind] 

Itulah efisiensi super-duper yang dijanjikan. Dan semua yang diperlukan adalah mendefinisikan metode inner() ini.


Contoh ini menunjukkan salah satu aplikasi penjadwalan berganda: ada definisi umum dari suatu fungsi, tetapi untuk beberapa tipe data tidak berfungsi secara optimal. Dan kemudian kami secara bijaksana menambahkan metode yang mempertahankan perilaku fungsi untuk tipe ini, tetapi bekerja jauh lebih efisien .


Tetapi ada area lain - ketika tidak ada definisi umum dari suatu fungsi, tapi saya ingin menambahkan fungsionalitas untuk beberapa jenis. Maka Anda dapat menambahkannya dengan sedikit usaha.


Dan opsi ketiga - Anda hanya ingin memiliki nama fungsi yang sama, tetapi dengan perilaku berbeda untuk tipe data yang berbeda - misalnya, agar fungsi berperilaku berbeda saat bekerja dengan kamus dan array.


Bagaimana cara mendapatkan perilaku serupa dalam bahasa pengiriman tunggal? Itu mungkin, tetapi sulit. Masalah: ketika membebani fungsi * perlu mengirim argumen kedua, dan bukan yang pertama. Anda dapat melakukan pengiriman ganda: pertama, kirim dengan argumen pertama dan panggil metode AbstractMatrix.*(v) . Dan metode ini, pada gilirannya, memanggil sesuatu seperti v.__rmul__(A) , mis. argumen kedua dalam panggilan asli kini telah menjadi objek yang metodenya sebenarnya dipanggil. __rmul__ sini diambil dari Python, di mana perilaku seperti itu adalah pola standar, tetapi tampaknya hanya berfungsi untuk penambahan dan perkalian. Yaitu masalah pengiriman ganda terpecahkan jika kita ingin memanggil fungsi yang disebut + atau * , jika tidak - sayangnya, bukan zaman kita. Dalam C ++ dan bahasa lainnya - Anda perlu membuat sepeda Anda.


OK, bagaimana dengan inner() ? Sekarang ada tiga argumen, dan pengiriman berlanjut pada yang pertama dan ketiga. Apa yang harus dilakukan dalam bahasa dengan pengiriman tunggal tidak jelas. "Pengiriman tiga" Aku tidak pernah bertemu langsung. Tidak ada solusi yang baik. Biasanya, ketika kebutuhan yang sama muncul (dan dalam kode numerik tampaknya sangat sering), orang akhirnya menerapkan sistem pengiriman ganda mereka. Jika Anda melihat proyek besar untuk perhitungan numerik dengan Python, Anda akan kagum berapa banyak dari mereka yang melakukannya dengan cara ini. Secara alami, implementasi seperti itu bekerja secara situasional, tidak dirancang dengan baik, penuh dengan bug dan lambat ( mengacu pada aturan kesepuluh Greenspan - kira - kira. Terjemahan ), Karena Jeff Besancon tidak bekerja pada proyek - proyek ini ( penulis dan kepala pengembang sistem pengiriman jenis di Julia - sekitar terjemahan. ).


Bagian 2. Tipe umum


Saya akan beralih ke sisi sebaliknya dari paradigma Julia - tipe umum. Ini, menurut pendapat saya, adalah "pekerja keras" utama bahasa, karena di area inilah saya mengamati penggunaan kembali kode tingkat tinggi.


Misalnya, Anda memiliki jenis RGB, seperti yang dimiliki ColorTypes.jl. Tidak ada yang rumit di dalamnya, hanya tiga nilai yang disatukan. Demi kesederhanaan, kami menganggap bahwa jenisnya tidak parametrik (tetapi bisa saja), dan penulis mendefinisikan beberapa operasi dasar baginya yang menurutnya berguna. Anda mengambil tipe ini dan berpikir: "Hmm, saya ingin menambahkan lebih banyak operasi pada tipe ini." Misalnya, bayangkan RGB sebagai ruang vektor (yang, sebenarnya, tidak benar, tetapi akan turun ke perkiraan pertama). Di Julia, Anda cukup mengambil dan menambahkan kode Anda semua operasi yang hilang.


Muncul pertanyaan - dan cho? Mengapa saya terlalu fokus pada hal ini? Ternyata dalam bahasa berorientasi objek yang didasarkan pada kelas, pendekatan seperti itu sangat sulit diimplementasikan. Karena definisi metode dalam bahasa ini ada di dalam definisi kelas, hanya ada dua cara untuk menambahkan metode: mengedit kode kelas untuk menambahkan perilaku yang diinginkan, atau membuat kelas pewaris dengan metode yang diperlukan.


Opsi pertama mengembang definisi kelas dasar, dan juga memaksa pengembang kelas dasar untuk menjaga dukungan dari semua metode yang ditambahkan saat mengubah kode. Apa yang suatu hari bisa membuat kelas seperti itu tidak didukung.


Warisan adalah pilihan "direkomendasikan" klasik, tetapi juga bukan tanpa cacat. Pertama, Anda perlu mengubah nama kelas - biarkan sekarang bukan RGB , tetapi MyRGB . Selain itu, metode baru tidak lagi berfungsi untuk kelas RGB asli; jika saya ingin menerapkan metode baru saya ke objek RGB dibuat dalam kode orang lain, saya perlu mengonversi atau membungkusnya dalam MyRGB . Tapi ini bukan yang terburuk. Jika saya membuat kelas MyRGB dengan beberapa fungsi tambahan, orang lain OurRGB , dll. - maka jika seseorang menginginkan kelas yang memiliki semua fungsi baru, Anda perlu menggunakan banyak pewarisan (dan ini hanya jika bahasa pemrograman memungkinkannya sama sekali!).


Jadi, kedua opsi ini biasa saja. Namun, ada solusi lain:


  • Letakkan fungsional dalam fungsi eksternal daripada metode kelas - pergi ke f(x, y) bukan xf(y) . Tapi kemudian perilaku umum hilang.
  • Meludahi menggunakan kembali kode (dan, menurut saya, dalam banyak kasus ini terjadi). Cukup salin diri Anda sendiri ke kelas RGB asing dan tambahkan yang hilang.

Fitur utama Julia dalam hal menggunakan kembali kode hampir sepenuhnya dikurangi menjadi fakta bahwa metode ini didefinisikan di luar tipe . Itu saja. Lakukan hal yang sama dalam bahasa pengiriman tunggal - dan jenis dapat digunakan kembali dengan mudah. Seluruh cerita dengan "mari kita membuat metode menjadi bagian dari kelas" sebenarnya adalah ide yang begitu-begitu saja. Benar, ada poin bagus - penggunaan kelas sebagai ruang nama. Jika saya menulis xf(y) - f() tidak harus berada di namespace saat ini, itu harus dicari di namespace x . Ya, ini adalah hal yang baik - tetapi apakah itu layak untuk semua masalah lainnya? Saya tidak tahu. Menurut pendapat saya, tidak (meskipun pendapat saya, seperti yang Anda duga, sedikit bias).


Epilog. Masalah ekspresi


Ada masalah pemrograman yang terlihat pada tahun 70-an. Ini sebagian besar terkait dengan pemeriksaan tipe statis, karena muncul dalam bahasa tersebut. Benar, saya pikir itu tidak ada hubungannya dengan pengecekan tipe statis. Inti dari masalah adalah sebagai berikut: apakah mungkin untuk mengubah model data dan serangkaian operasi pada data pada saat yang sama, tanpa menggunakan teknik yang meragukan.


Masalahnya dapat lebih atau kurang dikurangi menjadi sebagai berikut:


  1. apakah mungkin untuk dengan mudah dan bebas kesalahan menambahkan tipe data baru yang mana metode yang berlaku dan
  2. Apakah mungkin untuk menambahkan operasi baru pada tipe yang ada .

(1) mudah dilakukan dalam bahasa berorientasi objek dan sulit secara fungsional, (2) - sebaliknya. Dalam hal ini, kita bisa berbicara tentang dualisme pendekatan OOP dan FP.


Dalam bahasa multi-pengiriman, kedua operasi mudah. (1) , (2) — . , . ( https://en.wikipedia.org/wiki/Expression_problem ), . ? , , . , " , " — " " . " , " , , .


, . , , — .


, Julia ( ), . .

Source: https://habr.com/ru/post/id468609/


All Articles