Bahasa Pemrograman (Programming Language)

Subjek bahasa pemrograman adalah suatu rimba raya yang hampir tak terpetakan, tercermin dari banyaknya usulan tentang klasifikasi bahasa pemrograman [APPL1991], dan terlalu banyaknya istilah yang bersifat different thing to different people [APPL1997].

Untuk bahasa yang strongly typed pun rimba raya masalah masih banyak. Ada istilah kelas, tipe, kelas abstrak, tipe data abstrak, parameterized type, dan sebagainya. Hal ini diperparah dengan tidak adanya hirarki tipe yang terpadu.









Terakhir, banyak bahasa pemrograman dirancang dengan tingkat kepaduan konsep (conceptual integrity) yang rendah. Frederick P. Brooks [BROO1975] menyatakan bahwa kepaduan konsep adalah pertimbangan terpenting dalam desain sistem. Lebih baik memiliki sistem yang menghilangkan fitur-fitur anomalis tertentu dan mencerminkan satu set pemikiran desain, ketimbang sistem yang memiliki banyak pemikiran yang bagus tapi tidak terkoordinasikan.”
Pentingnya Bahasa Pemrograman

Besarnya pengaruh bahasa pemrograman tak bisa dibantah. Bahasa pemrograman adalah satu-satunya alat komunikasi programmer dengan komputer, dan salah satu alat komunikasi terpenting antar para programmer. Menara Babel gagal dibangun bukan karena bencana alam, ketiadaan biaya, atau kekurangan jumlah pekerja. Kegagalan disebabkan oleh kekacauan bahasa—komunikasi. Demikian pula kegagalan, keberhasilan, dan produktivitas proyek pembuatan perangkat lunak banyak bergantung dari [keputusan akan] bahasa pemrograman yang dipakai.

Cara pandang untuk melihat peran bahasa pemrograman harus menghindarkan dua ekstrim. Ekstrim pertama adalah bahwa semua bahasa sama saja, cuma berbeda-beda sedikit. Dengan kata lain, bahasa dipandang cuma suatu alat. Ekstrim kedua adalah fanatisme buta terhadap suatu bahasa.
Pendapat Tentang Perancangan Bahasa Pemrograman

Pendapat seseorang tentang upaya perancangan bahasa sangat dipengaruhi oleh kemampuannya menghindari kedua ekstrim di atas. Orang yang berpikir terbuka tidak akan secara serta merta mematahkan semangat untuk merancang bahasa pemrograman baru, terutama bahasa yang ditujukan untuk keperluan khusus. Kelayakan dari sisi ekonomi—penerimaan pasar—perlu dipertimbangkan, namun merupakan masalah tersendiri.
Prinsip-Prinsip Perancangan Bahasa Pemrograman

Ada beberapa prinsip penting perancangan bahasa pemrograman. Yang pertama adalah kepaduan konseptual seperti yang telah dipaparkan sebelumnya.

Prinsip kedua adalah ortogonalitas ([APPL1991], [SEBE1996]). Ortogonal berarti bebas, sementara derajat ortogonalitas berarti tingkat kebebasan untuk membentuk sesuatu yang besar dari hal-hal yang lebih kecil. Bahasa yang tinggi derajat ortogonalitasnya memiliki sedikit pengecualian dalam hal sintaks dan semantik.

Prinsip ketiga diajukan oleh Charles A. R. Hoare—penemu algoritma quick sort—yang berbunyi “Konsolidasi, bukan inovasi.” Artinya, setiap perancang bahasa baru harus senantiasa mengupayakan lebih dulu konsolidasi keputusan-keputusan desain yang bagus dari bahasa-bahasa yang sudah ada, karena sangat kecil kemungkinan ada hal yang betul-betul baru. Tentu saja penerapannya harus diimbangi dengan upaya pencapaian kepaduan konseptual. Tanpa pengimbangan ini, suatu bahasa bisa menjadi amburadul karena asal comot sana comot sini.

Sejarah Pengembangan Bahasa Pemrograman Batak

Awalnya adalah “meditasi” panjang selama hampir 4 tahun; pengembaraan pemikiran tentang apa benang merah dari semua subjek dalam informatika, dalam hal apa semua topik tersebut memiliki kesamaan, dalam hal apa berbeda, dan apa yang benar-benar membedakan? Lebih jauh dari itu, apakah pengajaran dan pemaparan harus serumit yang ada saat ini, apakah tidak bisa dilakukan upaya penyederhanaan terhadap seluruh subjek agar terlihat peta dari seluruh subjek dalam informatika?

Hasilnya telah dimuat dalam karya ilmiah yang dipaparkan di King Mongkut Institute of Technology, Bangkok, 1996. Karya tersebut memuat konsep dasar komputasi: VOTO—Value, Object, Type, Operation—dan bagaimana VOTO akan menyederhanakan pemaparan semua subjek dalam informatika. Lebih jauh, penerapan VOTO dapat memperjelas kesamaan dan perbedaan suatu topik dengan topik lain, dan merangsang siapapun untuk dapat mengajukan pertanyaan yang esensial ke suatu topik.
C++ Sebagai Pemicu

Dalam skala yang kecil, penulis telah menerapkan VOTO untuk mengajar tentang C++ dan pemrograman berorientasi objek di Tokyo, Jepang, 1995. Usai even ini dan presentasi paper di Bangkok, ada keinginan untuk membuat buku C++ seraya mengikuti program S2 di Inggris.

Proyek pembuatan buku ini dibatalkan karena C++ ternyata berkembang makin kompleks dan makin tak karuan, diganti dengan proyek pembuatan buku software engineering. Buku software engineering ini akhirnya usai juga pada awal tahun 1998 [HUTA1999] dan sempat diajukan ke Addison Wesley. Dengan 5 peninjau dan perbandingan 1 setuju lawan 4 menolak, Addison Wesley akhirnya tak menerbitkan buku ini.
Efek Buku Software Engineering

Proses penulisan buku software engineering melibatkan bacaan tentang bahasa pemrograman yang sangat beragam. Nyata bahwa belum ada satu bahasapun yang sangat bagus mengimplementasikan prinsip-prinsip software engineering. Dengan tidak diterimanya buku software engineering, usaha kemudian dialihkan ke upaya perancangan bahasa baru.
Upaya Awal Di STT Telkom

Desain sintaks Batak dimulai dengan mengamati deklarasi tipe, deklarasi objek, dan deklarasi operasi. Mengacu pada prinsip ortogonalitas, diperoleh sintaks yang sangat ortogonal bagi ketiganya. Berikutnya adalah pembuatan hirarki tipe yang terpadu, yang dirasa hilang dari semua bahasa pemrograman yang ada saat ini. Karena sesuatu hal, sintaks yang ortogonal belum dapat dipadukan ke rancangan bahasa pada saat itu. Rancangan pertama Batak usai pada awal tahun 1999 dan masuk ke perpustakaan STT Telkom sebagai karya ilmiah, bekerjasama dengan mahasiswa saat itu bernama Rudy Harinugroho—yang sekarang bekerja di RISTI Telkom.

Keputusan Desain

Bagian berikut memaparkan beberapa keputusan desain dalam Batak disertai alasannya. Keputusan perancangan yang masih belum disertakan dalam parser diberi tanda #.
Pemakaian Kata Module

Berikut contoh sebuah modul.

module m;

interface

type int_array := integer[10];
integer j;

Ekivalensi di C/C++:

typedef int int_array[10];

Ekivalensi di Pascal:

Unit m;

interface

Type int_array = array[1..10] of integer;

implementation

Alasan pemakaian module adalah untuk memudahkan siapa pun memahami makna pemrograman modular. Dalam pengalaman saya selama bertahun-tahun mengajar dan bekerja di industri, terlalu sedikit orang yang mampu menjelaskan apa itu pemrograman modular dan apa itu modul. Banyak yang menyamakan modul dengan function.

Kurang dipahaminya makna pemrograman modular tidak mengherankan karena:

C/C++ tak memakai kata ini secara eksplisit;

C/C++ memakai 2 file untuk module: file header (.h) dan file implementasi (.c, .cpp) yang menyulitkan seseorang untuk melihat bahwa sebuah modul merupakan paduan keduanya;

Java memakai kata yang berbeda: package. Tak ada istilah package programming; yang ada modular programming;

Pascal memakai kata unit. Tak ada istilah unit programming.

Memang adalah mungkin untuk memberi penjelasan bahwa package adalah seperti modul, atau unit pada intinya adalah modul, atau bahwa .h dan .c membentuk sebuah modul, namun:

Tidak cukup banyak buku yang menulis tentang hal ini (kalaupun ada);

Mengapa harus memakai kata lain jika bisa memakai kata yang lebih tepat?
Bagian Interface dan Implementation

Keputusan perancangan ini mengacu kepada hasil rancangan bahasa pemrograman Modula 2—yang pada gilirannya diimplementasikan di Turbo Pascal dan Microsoft Pascal. Kedua bagian ini secara bersama-sama mewujudkan penyembunyian informasi. Perbedaan besar antara Batak dan Pascal dalam menangani bagian interface dan implementation akan dijelaskan pada bagian tentang deklarasi tipe.
Pemakaian Kata Program

Program adalah unit logika translasi, sama seperti sebuah modul. Berbeda dengan modul-modul yang lain, sebuah program adalah module yang khusus:

Tidak bisa dipakai atau diacu oleh module atau program lain;

Tidak memiliki bagian interface dan implementation (implikasi dari butir di atas).

Adalah mungkin untuk membuat keputusan sehingga kata module tetap dipakai. Konsekuensinya, nama modul yang khusus ini juga harus khusus, misalnya: main. Ini berakibat pada pemrosesan semantik yang lebih sulit.

Program yang memakai module m bisa ditulis seperti ini

Program p;

Use (m);

Integer main()
{ return (0); }

Alasan-Alasan Lain

Pemakaian kata program memiliki alasan lain, namun sebelumnya mari kita lihat beberapa bahasa lain yang tidak memakai kata ini.

    C/C++ tak memakai kata program. Ini membuat penulisan program (main module) sedikit lebih sederhana.
    Java memakai cara yang lebih sulit. Nama program (sekalipun kata-kata program tidak eksplisit) harus identik dengan nama satu-satu tipe (class) yang publik.

Seperti terlihat pada contoh pertama ada deklarasi objek—dalam hal ini variabel—bernama j. Pada deklarasi tersebut terlihat bahwa deklarasi objek bertipe integer dimulai dengan nama tipe.

Hal yang sama juga berlaku pada deklarasi objek yang bertipe program. Dalam contoh terakhir, p adalah objek bertipe program.

Tipe integer dan program berbeda terutama atas operasi yang berlaku. Pada objek-objek bertipe integer berlaku operasi-operasi numerik—aritmetika, algoritma, dan lain-lain—sedangkan pada objek bertipe program—dan module—berlaku operasi compile (translate), sesuai dengan sifatnya sebagai unit logika translasi.

Singkatnya, alasan pencantuman program sebagai nama tipe (di deklarasi program) :

    menyamakan persepsi tentang deklarasi objek;
    membantu memperjelas konsep VOTO, untuk memperjelas bahwa programmer sedang berhadapan dengan sebuah program, dan objek-objek bertipe program terkait dengan sekumpulan operasi tertentu;
    membantu pembuatan hirarki tipe yang terpadu, seperti akan terlihat nanti.

Sampai titik ini, kita secara ringkas telah melihat penerapan 3 dari 4 konsep pada VOTO: Object, Type, Operation. Ada 3 objek: m, j, dan p. Ada 4 tipe yang terlibat: module, integer, int_array, dan program. Ada 3 operasi yang terlibat: assignment (:=), array-typing ([]), use, dan main.

Mengacu kepada prinsip kepaduan konseptual, tersirat bahwa prinsip ini cukup dipenuhi. Semua dapat dijelaskan dengan mengacu kepada konsep tentang objek, tipe, dan operasi.

Catatan:

    Selain compile, untuk tipe module berlaku juga operasi use (lihat contoh code terakhir).
    Compile adalah operasi yang berada di luar source code Batak. Pemanggilan aktual operasi ini ekuivalen dengan pemanggilan executable code compiler (contoh pada C: pemanggilan cc atau cl).

Deklarasi Tipe

Mari kita buat modul m2 sebagai berikut:

module m2;

interface

type int_array := integer[10];
integer j := -2;
int_array the_array;
integer k := the_array[1];

Dalam kode di atas terlihat ada deklarasi tipe int_array dan object j (selain object ia dan k). Mari kita lihat persamaan keduanya:

    Keduanya diberi nilai ekspresi (ekspresi tipe pada int_array, ekspresi numerik pada j).
    Operasi assignment pada keduanya memakai lambang yang sama.

Dua keputusan perancangan yang penting pada Batak adalah:

    Pemakaian operasi assignment untuk memberi ekspresi tipe.
    Pemakaian lambang yang sama untuk operasi assignment bagi ekspresi tipe dan ekspresi non-tipe.

Tidak diambilnya kedua keputusan ini di bahasa-bahasa lain sangat berpengaruh bagi seseorang untuk memahami ekspresi tipe. Selanjutnya pemahaman ekspresi tipe sangat berpengaruh bagi kemampuan seseorang untuk memahami pemrograman (minimal dalam hal penanganan tipe) serta topik-topik pemrograman kompiler.

Berikut adalah gambaran mengapa ketiadaan kedua keputusan di atas berpengaruh bagi susahnya pemahaman ekspresi tipe. Contoh pertama diambil dari C.

typedef int int_array[10];

Sangat sulit melihat ekspresi tipe dalam kasus ini karena operasi pembentukan tipe ( [] ) dan operan lainnya (10) diletakkan pada objek (int_array), bukan pada tipe (int). Andai C membuat deklarasinya seperti di bawah ini, akan lebih mudah memahami ekspresi tipe.

typedef int[10] int_array;    /* int[10] adalah ekspresi tipe */

typedef int_array = int[10];  /* lebih baik lagi */

Contoh kedua diambil dari Pascal.

type int_array = array[1..10] of integer;
(* assignment memakai lambang = *)
var j : integer;

begin
  J := 5; (* assignment memakai lambang := *)
end;

Keputusan perancangan pada Pascal:

    Pemakaian operasi assignment untuk mengassign ekspresi tipe.
    Pemakaian lambang yang berbeda untuk operasi assignment bagi ekspresi tipe dan espresi non-tipe.

Sebagai hasilnya, tetap sangat sulit untuk melihat bahwa kita memberikan ekspresi tipe. Konsekuensi lanjutan, programmer cenderung menjadi penghafal sintaks: sesudah ini lalu itu, dan seterusnya.

Dari contoh yang sama kita bisa melihat kesamaan ekspresi non-tipe dan ekspresi tipe: keduanya adalah ekspresi, dan tiap ekspresi dapat melibatkan operasi dan operan.

Ekspresi numerik (non-tipe) yang diberikan ke j melibatkan operasi – dan operan 2.

Ekspresi tipe yang diberikan ke int_array melibatkan operasi [], operan 2, dan operan integer.

Operasi [] bersifat unik. Bila diterapkan pada tipe, ia menjadi operasi pembentuk ekspresi tipe. Sedangkan bila diterapkan pada non-tipe (pada objek, the_array pada contoh) ia menjadi operasi subskrip, untuk mengekstrak suatu elemen dari array.
Operasi-Operasi Deklaratif

Yang dimaksud operasi-operasi deklaratif adalah operasi-operasi yang tak mensyaratkan tanda kurung untuk melingkupi operan-operan. Contoh adalah operasi +. Pemanggilannya 5 + 3, bukan + (5, 3).

Operasi-operasi pembentuk ekspresi tipe ada yang bersifat deklaratif dan ada yang imperatif. Pada bagian berikut ini kita akan melihat operasi-operasi yang deklaratif.
Orientasi Yang Sama Bagi Operasi-Operasi Pembentuk Ekspresi Tipe

Keputusan perancangan lain yang penting pada Batak adalah orientasi yang sama bagi operasi-operasi (deklaratif) pembentuk ekspresi tipe. Keputusan ini sangat berpengaruh bagi kemudahan pembacaan ekspresi tipe yang besar. Module m3 memberi ilustrasi. Nama-nama pada contoh sengaja dibuat kurang user-friendly untuk menunjukkan kemudahan pembacaan ekspresi tipe.

module m3;

interface

type int_array := integer[],
  h := integer[]^,

  i := integer^[];

Orientasi pembacaan operasi deklaratif pembentuk ekspresi tipe pada Batak adalah dari kanan ke kiri. Operasi ^ berperan sebagai pointer-typing, sehingga ekspresi

integer[]^ dibaca sebagai pointer ke array integer

integer^[] dibaca sebagai array dari pointer ke integer.

Kode di atas bisa ditulis ulang menjadi lebih deskriptif seperti berikut:

type int_array := integer[],
  ptr2array_int := integer[]^,
  array2int_ptr := integer^[];

Perbandingan tentang cara C/C++ dapat dilihat pada Lampiran.
Pemisahan Item Pada Deklarasi Tipe Dan Deklarasi Objek

Mengacu kepada prinsip ortogonalitas, ada keputusan perancangan pada Batak untuk memiripkan deklarasi tipe dan deklarasi objek. Kemiripan yang dimaksud di sini adalah kesamaan bahwa untuk suatu ekspresi tipe yang sama bisa dideklarasikan lebih dari satu item. Berikut adalah contohnya.

module m4;

interface

type int_array := integer[],
  h := integer[]^,

  i := integer^[];

char[] a, b, c;

integer j := 1, k, l;

Pada baris 5 sampai 7 item-item yang dideklarasikan adalah item-tipe. Pada baris 9 dideklarasikan 3 objek dengan ekspresi tipe char[]. Pada baris 10 dideklarasikan 3 objek dengan ekspresi tipe integer.

Ada 2 keuntungan dari keputusan perancangan ini:

Programmer tak perlu berulangkali menuliskan metatipe (type) untuk deklarasi banyak tipe. Ini sangat membantu dalam pemrograman skala besar yang melibatkan banyak deklarasi tipe.

Mengurangi beban menghafal. Sintaks deklarasi tipe sangat mirip dengan sintaks deklarasi objek. Tanda baca koma berperan untuk memisahkan item, dan setiap item bisa diberikan ekspresi.

Sekali lagi, ini adalah contoh penerapan prinsip ortogonalitas. Seandainya tidak ada pengecualian yang perlu dibuat, maka janganlah dibuat.

Hasil keputusan perancangan pada C/C++ bisa dilihat pada Lampiran.
Operasi Subtype

Keputusan perancangan berikutnya adalah tentang operasi subtype. Operasi subtype adalah cara Batak untuk mewujudkan pewarisan (inheritance). Berikut adalah sebuah contoh.

module m4;

interface

type shape := { boolean is_visible; },
  ellipse := subtype (shape)  + { integer a, b; },
  e := { boolean is_visible;} + { integer a, b; },
  f := { boolean is_visible; integer a, b; } ;

Baris ke-5 menyatakan pemberian nilai (Value, satu dari empat konsep dasar VOTO) tipe ke shape. Untuk melihat bahwa { boolean is_visible; } adalah sebuah nilai, bisa dibandingkan dengan

Program p2;

integer next (integer i) { return (i + 1); }

integer main()
{
  integer k := 5,
    n := next(4);
}

Dari contoh m4 dan p2, kedua baris berikut memiliki kesamaan: yang diberikan adalah nilai (literal).

type shape := { boolean is_visible; };
integer k := 5;

Seperti halnya hasil evaluasi next(4) memberi nilai numerik 5, demikian pula hasil evaluasi subtype(shape) memberi nilai tipe { boolean is_visible; }. Dengan pemahaman ini jelas bahwa struktur ketiga tipe (ellipse, e, dan f) adalah sama, yakni { boolean is_visible; integer a, b; }. Operasi + adalah operasi yang berlaku bagi ekspresi tipe, yang memiliki makna penggabungan ekspresi tipe, persisnya penggabungan 2 ekspresi tipe komposit (record).

Kesamaan tipe ellipse, e, dan f berlaku hanya pada struktur, tidak pada semantik. ellipse sebagai tipe turunan dari shape memiliki hak untuk mewarisi operasi yang berlaku pada shape. Sebagai contoh:

void display (shape s);

shape s1;

ellipse e1;

e e2;

f f1;

display (s1);
display (e1);
display (e2);
display (f1);

Pemanggilan operasi display dengan operan e2 dan f1 akan membangkitkan kesalahan.

Keputusan perancangan ini memudahkan penjelasan dan menjawab berbagai pertanyaan tentang pewarisan. Contoh: apa itu pewarisan? Pewarisan adalah fasilitas untuk:

memberikan nilai–tipe ke tipe lain;

mengizinkan berlakunya operasi-operasi di supertipe ke subtipe;

mengizinkan berlakunya operasi–operasi pada objek dari supertipe ke objek dari subtipe;

diimplementasikan lewat operasi subtype.

Contoh pertanyaan lain: bagaimana melepas fasilitas pewarisan dari Batak? Jawabannya: hilangkan operasi subtype.

Mengacu kepada prinsip ortogonalitas, keputusan perancangan ini membuat Batak ortogonal:

Pemberian nilai untuk ekspresi tipe pada pewarisan sama dengan pemberian nilai untuk ekspresi tipe yang tak melibatkan pewarisan.

Pemberian nilai untuk ekspresi tipe pada pewarisan sama dengan pemberian nilai ekspresi non-tipe.

Mengacu kepada VOTO dan prinsip kepaduan konseptual:

    Konsep pewarisan bisa diturunkan dari keempat konsep utama : Value, Object, Type, Operation (lihat cara menjawab tentang apa itu pewarisan).

Artinya: Batak memenuhi prinsip kepaduan konseptual dalam keputusan perancangan ini.
Information Hiding: Bagian Interface Dan Implementation

Ilustrasi tentang penyembunyian informasi memakai module m5 yang merupakan sedikit modifikasi dari m4. Mari kita lihat dulu program p3 yang memakai module m4.

Program p3;

Use (m4);

Integer main()
{
  shape s;
  s.is_visible := true;
}

Pengaksesan is_visible absah karena aksesibilitas s bersifat publik, sejalan dengan penempatannya di bagian interface. Sebagai perbandingan, berikut adalah module m5 dengan program p4.

module m5;

interface

type shape, ellipse := subtype (shape);

implementation

type shape := { boolean is_visible; },
  ellipse := subtype (shape) + { integer a, b; };

program p4;

use (m5);

shape s;

integer main()
{
  s.is_visible := true; /* error */
}

Kedua bagian adalah sarana penyembunyian informasi. Yang tercantum di interface tidak disembunyikan sama sekali, sehingga semua tipe, operasi, dan objek yang ada bisa diakses oleh semua modul atau program yang mengacu modul yang bersangkutan. Atribut yang dicantumkan di bagian ini juga dapat diakses dengan operasi dot (.).

Yang tercantum di implementation suatu modul tidak terlihat bagi modul atau program yang mengacunya. Inilah alasan mengapa pengacuan dalam program p4 gagal.

Dengan kata lain, interface dan implementation adalah pembeda akses (access qualifier) seperti halnya public, private, dan protected pada C++ dan Java.

Sebagai ilustrasi lebih lanjut mengenai penyembunyian informasi : katakanlah ada kasus di mana satu atau lebih programmer implementor module m5 ingin memberi hanya informasi yang dibutuhkan (sebagai interface), dan ingin menyembunyikan informasi yang rahasia. Pembuat module m5 bisa menyalin source code m5 sampai bagian interface saja (menjadi sebuah file interface), mengkompilasi m5, dan memberi file interface serta intermediate codenya. Dengan asumsi bahwa file interface adalah m5.itf (lihat di bawah) dan intermediate codenya file m5.obj, maka sedemikian mudahlah caranya untuk mengimplementasikan penyembunyian informasi di Batak. Berikut adalah file m5.itf.

Module m5;

Interface

Type shape, ellipse := subtype (shape);

Keputusan perancangan ini adalah lebih baik daripada C, C++, Java dan Turbo Pascal. Keempatnya meletakkan pembeda akses pada tipe sehingga pembeda akses cenderung harus terus-menerus diketik ulang untuk tiap tipe. Dengan membuat pembeda akses berlingkup modul, di Batak hal di atas tak perlu terjadi.

Sebagai contoh, andai dibuat module m6 yang mendeklarasikan tipe shape, ellipse, dan complex; pembeda akses tak perlu diulangi untuk 3 (atau lebih) tipe seperti ini.

Module m6;

interface

type shape, ellipse := subtype (shape), complex;

Kepaduan konseptual dan ortogonalitas sekaligus dicapai dalam keputusan perancangan ini. Pembeda akses seharusnya merupakan urusan modul, bukan tipe. Ini sejalan dengan konsep pemrograman modular yang menyatakan modul sebagai sarana penyembunyian informasi (bukan tipe).

Komentar

Postingan populer dari blog ini

Contoh Program Percabangan IF, IF-ELSE dan NESTED IF pada C++

Nominal dan Verbal Sentence

Perbedaan Website E-Learning dan E-Commerce