Dalam posting ini saya ingin berbicara tentang beberapa bahasa dan teknologi populer yang mencakup elemen pemrograman deklaratif - PL / SQL , MS LINQ, dan GraphQL . Saya akan mencoba untuk mencari tahu tugas apa yang diselesaikan di dalamnya menggunakan pemrograman deklaratif, seberapa dekat pendekatan deklaratif dan imperatif terjalin, keuntungan apa yang diberikannya, dan ide apa yang dapat dipelajari darinya.
Ekstensi Prosedural SQL
Mari kita mulai dengan area di mana asosiasi ini telah lama menjadi standar industri - bahasa akses data. Yang paling terkenal adalah PL / SQL, perpanjangan prosedural dari bahasa SQL. Bahasa ini memungkinkan Anda memproses data dalam database relasional menggunakan imperatif (variabel, pernyataan kontrol, fungsi, objek) dan gaya pemrograman deklaratif (ekspresi SQL). Dengan menggunakan kueri SQL, kita dapat mendeskripsikan properti apa yang dimiliki data yang kita butuhkan - bidang apa yang dibutuhkan, dari tabel mana yang akan diambil, bagaimana kaitannya satu sama lain, batasan apa yang harus dipatuhi, bagaimana harus digabungkan, dll. Dan server basis data akan secara mandiri menyusun rencana eksekusi kueri dan menemukan semua kemungkinan set bidang yang memenuhi kondisi yang ditentukan. Bagian prosedural PL / SQL memungkinkan Anda untuk mengimplementasikan tugas-tugas tersebutyang sulit atau tidak mungkin untuk diungkapkan dalam bentuk deklaratif - untuk memproses hasil kueri dalam satu putaran, melakukan kalkulasi sewenang-wenang, menyusun kode dalam fungsi dan kelas.
Komponen prosedural dan deklaratif bahasa terintegrasi erat. PL / SQL memungkinkan Anda untuk mendeklarasikan fungsi, menjalankan kueri di dalamnya dan mengembalikan hasilnya, menggunakan fungsi di dalam kueri, meneruskan nilai bidang tabel sebagai argumen. Anda dapat mengakses hasil kueri menggunakan kursor dan kemudian secara imperatif melakukan perulangan melalui semua catatan yang diterima. Cursor memberi Anda lebih banyak kontrol atas konten tabel dan memungkinkan Anda menerapkan logika pemrosesan data yang jauh lebih kompleks daripada hanya menggunakan SQL. Kursor dapat ditempatkan ke variabel kursor dan diteruskan sebagai argumen ke fungsi, prosedur, atau bahkan aplikasi klien. Kode permintaan itu sendiri dapat dibuat secara dinamis dengan urutan perintah imperatif.Kombinasi prosedur dan kueri, menggunakan beberapa penyesuaian, memungkinkan Anda untuk mengimplementasikan kueri rekursif. Bahkan ada fitur berorientasi objek di PL / SQL yang memungkinkan Anda mendeklarasikan tipe data komposit untuk bidang tabel, menyertakan metode di dalamnya, dan membuat kelas melalui pewarisan.
PL / SQL memungkinkan Anda untuk mengimplementasikan logika bisnis di sisi server database. Selain itu, implementasi model domain akan mendekati deskripsinya. Konsep dasar model domain akan dipetakan ke model data relasional. Konsep tersebut akan sesuai dengan tabel, atribut - bidangnya. Batasan pada nilai bidang dapat disematkan dalam deskripsi tabel. Dan hubungan dengan tabel lain dapat diatur menggunakan kunci asing. Konsep abstrak yang dibangun atas dasar konsep dasar akan sesuai dengan pandangan. Mereka dapat digunakan dalam kueri bersama dengan tabel, termasuk untuk membangun tampilan lain. Tampilan dibangun atas dasar kueri, yang memungkinkan Anda menggunakan kekuatan penuh dan fleksibilitas SQL. Lewat sini,dari tabel dan tampilan, Anda dapat membangun model domain yang cukup kompleks dan multi-level sepenuhnya dengan gaya deklaratif. Dan apapun yang tidak cocok dengan gaya deklaratif dapat diimplementasikan dengan menggunakan prosedur dan fungsi.
Masalah utama adalah bahwa kode PL / SQL dijalankan secara eksklusif di sisi server DB. Ini membuatnya sulit untuk mengukur solusi seperti itu. Selain itu, model yang dihasilkan akan terikat secara kaku ke database relasional dan akan menjadi masalah untuk memasukkan data dari sumber lain ke dalamnya.
Kueri Terpadu Bahasa
Language Integrated Query (LINQ) adalah komponen populer dari platform .NET yang memungkinkan Anda untuk secara alami menyertakan ekspresi kueri SQL dalam kode bahasa berorientasi objek utama Anda. Berbeda dengan PL / SQL, yang menambahkan paradigma imperatif ke SQL di sisi server database, LINQ membawa SQL ke level aplikasi. Berkat ini, kueri di LINQ dapat digunakan untuk mendapatkan data tidak hanya dari database relasional, tetapi juga dari kumpulan objek, dokumen XML, dan kueri LINQ lainnya.
Arsitektur LINQ cukup fleksibel dan definisi kueri sangat terintegrasi dengan model OOP. LINQ memungkinkan Anda membuat penyedia Anda sendiri untuk mengakses sumber data baru. Anda juga dapat menyetel cara Anda sendiri untuk menjalankan kueri dan, misalnya, mengonversi pohon ekspresi LINQ dari kueri tersebut menjadi kueri ke sumber data yang diinginkan. Anda dapat menggunakan ekspresi lambda dan fungsi yang ditentukan dalam kode aplikasi Anda di badan permintaan Anda. Benar, dalam kasus LINQ ke SQL, kueri akan dieksekusi di sisi server database, di mana fungsi ini tidak akan tersedia, tetapi sebagai gantinya prosedur tersimpan dapat digunakan. Permintaan adalah inti dari bahasa tingkat pertama, Anda dapat bekerja dengannya seperti objek biasa. Kompilator dapat secara otomatis menyimpulkan jenis hasil kueri dan menghasilkan kelas yang sesuai, meskipun itu belum dideklarasikan secara eksplisit.
Mari coba menggunakan LINQ untuk membangun model domain sebagai sekumpulan kueri. Fakta awal dapat ditempatkan dalam daftar di sisi aplikasi atau dalam tabel di sisi database, dan konsep abstrak dapat diformat sebagai kueri LINQ. LINQ memungkinkan Anda membuat kueri berdasarkan kueri lain dengan menentukannya di klausa FROM. Ini memungkinkan Anda untuk membuat konsep baru berdasarkan yang sudah ada. Bidang di bagian PILIHAN akan sesuai dengan atribut konsep. Dan bagian WHERE akan berisi dependensi antar konsep. Contoh dengan faktur dari publikasi sebelumnya akan terlihat seperti ini.
Kami akan menempatkan objek dengan akun dan informasi pelanggan dalam daftar:
List<Bill> bills = new List<Bill>() { ... };
List<Client> clients = new List<Client>() { ... };
Dan kemudian kami akan membuat kueri agar mereka mendapatkan tagihan dan debitur yang belum dibayar:
IEnumerable<Bill> unpaidBillsQuery =
from bill in bills
where bill.AmountToPay > bill.AmountPaid
select bill;
IEnumerable<Client> debtorsQuery =
from bill in unpaidBillsQuery
join client in clients on bill.ClientId equals client.ClientId
select client;
Model domain yang diimplementasikan menggunakan LINQ telah mengambil bentuk yang agak aneh - beberapa gaya pemrograman terlibat sekaligus. Tingkat atas model memiliki semantik imperatif. Ini dapat direpresentasikan sebagai rantai transformasi objek, membangun koleksi objek di atas koleksi. Objek kueri adalah elemen dunia OOP. Mereka perlu dibuat, ditugaskan ke variabel, dan referensi ke sana harus diteruskan ke permintaan lain. Di tingkat menengah, objek kueri mengimplementasikan prosedur untuk menjalankan kueri, yang secara fungsional dikustomisasi dengan ekspresi lambda yang memungkinkan Anda untuk membentuk struktur hasil di bagian PILIH dan memfilter rekaman di klausa WHERE. Tingkat bagian dalam diwakili oleh prosedur eksekusi kueri, yang memiliki semantik logis dan didasarkan pada aljabar relasional.
Meskipun LINQ memungkinkan untuk menggambarkan model domain, sintaks SQL ditujukan terutama untuk mengambil dan memanipulasi data. Itu kekurangan beberapa konstruksi yang akan berguna dalam pemodelan. Jika pada PL / SQL struktur konsep dasar sangat jelas terwakili dalam bentuk tabel dan view, maka pada LINQ ternyata dirender menjadi kode OOP. Selain itu, sementara tabel dan tampilan dapat direferensikan menurut nama, kueri LINQ dapat dirujuk dalam gaya imperatif. Selain itu, SQL dibatasi oleh model relasional dan memiliki kemampuan yang terbatas ketika bekerja dengan struktur dalam bentuk grafik atau pohon.
Paralel antara model relasional dan pemrograman logika
Anda mungkin memperhatikan bahwa implementasi SQL dan Prolog model memiliki kesamaan. Di SQL, kami membangun tampilan berdasarkan tabel atau tampilan lain, dan di Prolog kami membuat aturan berdasarkan fakta dan aturan. Di SQL, tabel adalah kumpulan bidang, dan predikat di Prolog adalah kumpulan atribut. Di SQL, kami menetapkan dependensi antara kolom tabel sebagai ekspresi di klausa WHERE, dan di Prolog, menggunakan variabel predikat dan boolean yang menghubungkan atribut predikat satu sama lain. Dalam kedua kasus tersebut, kami menetapkan spesifikasi solusi secara deklaratif, dan mesin eksekusi kueri bawaan mengembalikan catatan yang ditemukan di SQL atau kemungkinan nilai variabel di Prolog kepada kami.
Kesamaan ini tidak disengaja. Meskipun landasan teoritis SQL - aljabar relasional dikembangkan secara paralel dengan pemrograman logika, tetapi kemudian koneksi teoritis terungkap di antara mereka. Mereka memiliki dasar matematika yang sama - logika orde pertama. Model data relasional menjelaskan aturan untuk membangun hubungan antara tabel data, pemrograman logis - antar pernyataan. Kedua teori ini menggunakan istilah yang berbeda, diterapkan di bidang yang berbeda, dikembangkan secara paralel, tetapi memiliki dasar matematika yang sama.
Sebenarnya, kalkulus relasional adalah adaptasi logika orde pertama untuk bekerja dengan data tabel. Pertanyaan ini dibahas lebih detail di sini.... Artinya, ekspresi aljabar relasional apa pun (kueri SQL apa pun) dapat diformulasi ulang menjadi ekspresi logika orde pertama, dan kemudian diimplementasikan di Prolog. Tapi tidak sebaliknya. Kalkulus relasional adalah bagian dari logika orde pertama. Ini berarti bahwa untuk beberapa jenis pernyataan yang dapat diterima dalam logika orde pertama, kita tidak dapat menemukan analogi dalam aljabar relasional. Misalnya, kemampuan kueri rekursif dalam SQL sangat terbatas, dan konstruksi relasi transitif juga tidak selalu tersedia. Operasi prolog seperti disjungsi target dan negasi seperti penolakan jauh lebih sulit untuk diterapkan di SQL. Sintaks fleksibel Prolog memberi Anda lebih banyak fleksibilitas untuk bekerja dengan struktur bersarang yang kompleks dan mendukung operasi pencocokan pola pada mereka.Ini membuatnya nyaman saat bekerja dengan struktur data kompleks seperti pohon dan grafik.
Tetapi Anda harus membayar semuanya. Algoritme eksekusi kueri bawaan dalam basis data relasional lebih sederhana dan kurang fleksibel dibandingkan algoritme inferensi di Prolog. Ini memungkinkan untuk mengoptimalkannya dan mencapai kinerja yang jauh lebih tinggi. Prolog juga tidak dapat memproses jutaan baris dengan cepat dalam database relasional. Selain itu, algoritma inferensi Prolog sama sekali tidak menjamin akhir dari eksekusi program - keluaran dari beberapa pernyataan dapat menyebabkan rekursi tak terbatas.
Ngomong-ngomong, di persimpangan antara database dan pemrograman logis, ada juga teknologi seperti database deduktif dan bahasa aturan dan kueri untuk Datalog. Alih-alih rekaman dalam tabel, database deduktif menyimpan sejumlah besar fakta dan aturan dalam gaya logis. Dan Datalog terlihat seperti Prolog, tetapi berfokus pada pengerjaan fakta yang digabungkan menjadi kumpulan, bukan fakta tunggal. Selain itu, beberapa fitur logika orde pertama di dalamnya dipotong untuk mengoptimalkan algoritme inferensi agar dapat bekerja cepat dengan data dalam jumlah besar. Jadi sintaks yang kurang ekspresif dari bahasa logis juga memiliki kelebihannya.
Pendekatan deklaratif untuk deskripsi lapisan API
SQL mengikat bangunan model ke lapisan akses data. Tetapi pemrograman deklaratif juga secara aktif berkembang di ujung aplikasi yang berlawanan - di lapisan API. Keunikannya adalah bahwa informasi tentang struktur permintaan harus tersedia bagi mereka yang menggunakan API ini. Memiliki deskripsi formal tentang struktur permintaan dan tanggapan adalah bentuk yang baik. Karenanya, ada keinginan untuk menyinkronkan deskripsi ini dengan kode aplikasi, misalnya, menghasilkan kelas permintaan dan respons berdasarkan padanya. Di mana Anda kemudian perlu menulis logika untuk memproses permintaan.
GraphQL adalah kerangka kerja untuk membangun API yang melampaui pendekatan tradisional ini, dan menawarkan tidak hanya bahasa kueri, tetapi juga lingkungan eksekusi kueri. Tidak perlu membuat kode, runtime memahami deskripsi permintaan. Untuk mengimplementasikan API menggunakan GraphQL, Anda membutuhkan:
- mendeskripsikan tipe data (objek) dari aplikasi yang merupakan bagian dari permintaan dan tanggapan;
- menjelaskan struktur permintaan dan tanggapan;
- mengimplementasikan fungsi yang mengimplementasikan logika membuat objek untuk mendapatkan nilai bidangnya.
Tipe data adalah deskripsi bidang objek. Tipe seperti tipe skalar, daftar, enumerasi, dan referensi ke tipe bersarang didukung. Karena bidang tipe dapat berisi referensi ke tipe lain, seluruh skema data dapat direpresentasikan sebagai grafik. Permintaan adalah deskripsi dari struktur data yang diminta dari API. Deskripsi permintaan tersebut mencakup daftar objek yang diperlukan, bidangnya, dan atribut masukan. Setiap tipe data dan setiap bidangnya harus dikaitkan dengan fungsi resolver. Penyelesai tipe (objek) menjelaskan algoritme untuk mendapatkan objeknya, resolver bidang menjelaskan nilai bidang objek. Mereka mewakili fungsi dalam salah satu bahasa fungsional atau bahasa berorientasi objek. Runtime GraphQL menerima permintaan, menentukan tipe data yang diperlukan, memanggil resolvernya, termasuk di sepanjang rantai objek bersarang,mengumpulkan objek respons.
GraphQL menggabungkan deskripsi skema data deklaratif dengan algoritma imperatif atau fungsional untuk mendapatkannya. Skema data dijelaskan secara eksplisit dan penting bagi aplikasi. Banyak orang menunjukkan bahwa merupakan praktik yang baik untuk membuat skema data yang tidak menduplikasi skema sumber data, tetapi sesuai dengan model domain. Ini membuat GraphQL menjadi solusi yang cukup populer untuk mengintegrasikan sumber data yang berbeda.
Dengan demikian, bahasa GraphQL memungkinkan Anda untuk mengekspresikan model domain dengan cara yang cukup jelas, untuk membedakannya dari kode lainnya, untuk mendekatkan model dan implementasinya. Sayangnya, komponen deklaratif bahasa hanya terbatas pada deskripsi komposisi tipe data; semua relasi lain antara elemen model harus diimplementasikan menggunakan resolver. Di satu sisi, resolver memungkinkan pengembang untuk secara independen menerapkan metode apa pun untuk mendapatkan data untuk suatu objek dan hubungan apa pun di antara mereka. Namun, di sisi lain, Anda harus mencoba menerapkan opsi kueri yang lebih kompleks daripada, misalnya, akses ke record dengan kunci. Di satu sisi, skema data di GraphQL dengan jelas menunjukkan hubungan antara lapisan API dan lapisan akses data. Namun, di sisi lain, lapisan utama yang terikat skema data adalah lapisan API.Isi dari skema data menyesuaikan dengan itu; itu tidak akan berisi entitas yang tidak terlibat dalam permintaan pemrosesan. Meskipun kekuatan ekspresif bahasa deskripsi data GraphQL lebih rendah daripada bahasa deklaratif lengkap seperti SQL dan Prolog, popularitas kerangka kerja ini menunjukkan bahwa alat untuk deskripsi model deklaratif dapat dan harus menjadi bagian dari bahasa pemrograman modern.
Saya akan meringkas
PL / SQL adalah bahasa yang nyaman baik untuk mendeskripsikan model domain dalam bentuk tabel dan tampilan, dan logika untuk bekerja dengannya. Komponen deklaratif dan prosedural terintegrasi erat dan saling melengkapi. Masalah utama adalah bahwa bahasa ini terkait erat dengan lokasi penyimpanan data, hanya dapat dijalankan di sisi server database, dan logika eksekusi kueri terbatas pada model data relasional.
Di sisi aplikasi, Anda dapat menggunakan teknologi seperti LINQ dan GraphQL untuk mendeskripsikan model dalam bentuk deklaratif. Dengan menggunakan skema data GraphQL, Anda dapat dengan jelas dan sangat jelas mendeskripsikan struktur model domain, kumpulan konsepnya. Dan runtime mampu mengumpulkan objek yang diperlukan secara otomatis. Sayangnya, semua hubungan dan koneksi antar konsep, kecuali untuk penumpukannya, harus diimplementasikan di lapisan fungsi resolver. LINQ memiliki kelebihan dan kekurangan yang berlawanan. Sintaks fleksibel SQL memberi Anda lebih banyak fleksibilitas untuk mendeskripsikan hubungan antar konsep. Tetapi di luar permintaan, deklaratif berakhir, objek permintaan adalah elemen dunia OOP. Mereka perlu dibuat, ditugaskan ke variabel, dan digunakan dalam gaya imperatif.
Saya ingin menggabungkan keunggulan LINQ dan GraphQL. Sehingga uraian struktur konsep jelas seperti pada GraphQL, dan hubungan antar konsep tersebut dapat diatur berdasarkan logika seperti pada SQL. Dan agar definisi konsep tersedia secara langsung dengan nama sebagai kelas, tanpa perlu secara eksplisit membuat objeknya, menugaskannya ke variabel, meneruskan referensi ke sana, dll.
Saya akan mulai merancang solusi semacam itu dengan mengembangkan bahasa deskripsi untuk model domain. Namun untuk itu perlu dibuat gambaran tentang bahasa representasi pengetahuan yang ada. Oleh karena itu, pada publikasi berikutnya, saya ingin berbicara tentang bahasa pemrograman logika, RDF, OWL dan logika bingkai, membandingkannya dan mencoba menemukan fitur-fitur yang akan menarik untuk bahasa yang dirancang untuk menggambarkan logika bisnis.
Bagi mereka yang tidak ingin menunggu rilis semua publikasi di HabrΓ©, ada teks lengkap dalam gaya ilmiah dalam bahasa Inggris, tersedia di tautan: Pemrograman Berorientasi Ontologi Hibrid untuk Pemrosesan Data Semi-Terstruktur .
Tautan ke publikasi sebelumnya:
Merancang bahasa pemrograman multi-paradigma. Bagian 1 - Untuk apa ini?