Fungsional vs murni katakan, jangan tanya?


14

"Jumlah argumen ideal untuk fungsi adalah nol" jelas salah. Jumlah argumen yang ideal adalah persis jumlah yang dibutuhkan untuk memungkinkan fungsi Anda bebas efek samping. Kurang dari itu dan Anda tidak perlu menyebabkan fungsi Anda menjadi tidak murni sehingga memaksa Anda untuk menjauh dari jurang kesuksesan dan menaiki kemiringan rasa sakit. Terkadang "Paman Bob" tepat dengan sarannya. Terkadang dia salah besar. Saran nol argumennya adalah contoh yang terakhir

( Sumber: komentar oleh @ David Arno di bawah pertanyaan lain di situs ini )

Komentar telah memperoleh 133 suara yang spektakuler, itulah sebabnya saya ingin lebih memperhatikan manfaatnya.

Sejauh yang saya ketahui, ada dua cara terpisah dalam pemrograman: pemrograman fungsional murni (apa komentar ini mendorong) dan katakan, jangan tanya (yang dari waktu ke waktu direkomendasikan di situs web ini juga). AFAIK kedua prinsip ini pada dasarnya tidak kompatibel, hampir saling bertentangan: fungsional murni dapat diringkas sebagai "hanya mengembalikan nilai, tidak memiliki efek samping" sementara mengatakan, jangan tanya dapat diringkas sebagai "jangan mengembalikan apa pun, hanya memiliki efek samping ". Juga, saya agak bingung karena saya pikir itu memberitahu, jangan tanya dianggap sebagai inti dari paradigma OO sementara fungsi murni dianggap sebagai inti dari paradigma fungsional - sekarang saya melihat fungsi murni yang direkomendasikan dalam OO!

Saya kira para pengembang seharusnya memilih salah satu dari paradigma ini dan berpegang teguh pada itu? Yah saya harus mengakui bahwa saya tidak akan pernah bisa mengikuti saya. Seringkali terasa nyaman bagi saya untuk mengembalikan nilai dan saya tidak dapat benar-benar melihat bagaimana saya dapat mencapai apa yang ingin saya capai hanya dengan efek samping. Seringkali terasa nyaman bagi saya untuk memiliki efek samping dan saya tidak dapat benar-benar melihat bagaimana saya dapat mencapai apa yang ingin saya capai hanya dengan mengembalikan nilai. Juga, seringkali (saya kira ini mengerikan) Saya punya metode yang melakukan keduanya.

Namun, dari 133 upvotes ini saya beralasan bahwa saat ini pemrograman fungsional murni "menang" karena menjadi konsensus yang lebih baik untuk diceritakan, jangan ditanya. Apakah ini benar?

Oleh karena itu, pada contoh dari game yang sarat antiputern ini, saya mencoba untuk membuat : Jika saya ingin membuatnya sesuai dengan paradigma fungsional murni - BAGAIMANA ?!

Tampaknya masuk akal bagi saya untuk memiliki keadaan pertempuran. Karena ini adalah permainan berbasis giliran, saya menyimpan status pertempuran dalam kamus (multipemain - mungkin ada banyak pertempuran yang dimainkan oleh banyak pemain secara bersamaan). Setiap kali seorang pemain melakukan giliran mereka, saya memanggil metode yang tepat pada keadaan pertempuran yang (a) memodifikasi negara sesuai dan (b) mengembalikan pembaruan kepada para pemain, yang mendapatkan serial ke dalam JSON dan pada dasarnya hanya memberi tahu mereka apa yang baru saja terjadi pada naik. Ini, saya kira, adalah pelanggaran mencolok prinsip KEDUA dan pada saat yang sama.

OK - saya bisa membuat metode KEMBALI keadaan pertempuran daripada mengubahnya di tempat jika saya benar-benar ingin. Tapi! Maka apakah saya harus menyalin segala sesuatu dalam keadaan pertempuran hanya untuk mengembalikan keadaan yang sama sekali baru alih-alih mengubahnya di tempat?

Sekarang mungkin jika langkah itu adalah serangan saya hanya bisa mengembalikan karakter yang diperbarui HP? Masalahnya, itu tidak sesederhana itu: aturan permainan, satu gerakan dapat dan seringkali akan memiliki banyak efek lebih dari sekadar melepas sebagian dari HP pemain. Misalnya, ini dapat meningkatkan jarak antar karakter, menerapkan efek khusus, dll.

Tampaknya jauh lebih sederhana bagi saya untuk hanya memodifikasi keadaan dan mengembalikan pembaruan ...

Tetapi bagaimana seorang insinyur yang berpengalaman menangani ini?


9
Mengikuti paradigma apa pun adalah cara pasti menuju kegagalan. Kebijakan seharusnya tidak pernah mengalahkan kecerdasan. Solusi untuk masalah harus bergantung pada masalah, bukan keyakinan agama Anda tentang penyelesaian masalah.
John Douma

1
Saya tidak pernah memiliki pertanyaan yang diajukan di sini tentang sesuatu yang saya katakan sebelumnya. Saya merasa terhormat. :)
David Arno

Jawaban:


14

Seperti kebanyakan aforisme pemrograman, "katakan, jangan tanya" mengorbankan kejelasan untuk mendapatkan singkatnya. Sama sekali tidak dimaksudkan untuk merekomendasikan agar tidak meminta hasil perhitungan, tetapi merekomendasikan untuk tidak meminta input dari perhitungan. "Jangan dapatkan, lalu hitung, lalu tetapkan, tapi tidak apa-apa mengembalikan nilai dari perhitungan," tidak bernas.

Dulu cukup umum bagi orang untuk memanggil pengambil, melakukan perhitungan, lalu memanggil setter dengan hasilnya. Ini adalah tanda yang jelas bahwa perhitungan Anda sebenarnya milik kelas yang Anda panggil pengambil. "Katakan, jangan tanya" diciptakan untuk mengingatkan orang agar waspada terhadap pola-anti itu, dan itu bekerja dengan sangat baik sehingga sekarang beberapa orang berpikir bagian itu jelas, dan mereka mencari jenis "permintaan" lain untuk menghapuskan. Namun, pepatah hanya berguna diterapkan untuk satu situasi itu.

Program fungsional murni tidak pernah menderita dari pola anti-persis itu, karena alasan sederhana bahwa tidak ada setter dalam gaya itu. Namun, masalah yang lebih umum (dan lebih sulit untuk dilihat) dari tidak mencampur tingkat abstraksi semantik yang berbeda dalam fungsi yang sama berlaku untuk setiap paradigma.


Terima kasih telah menjelaskan dengan benar "Katakan, jangan Tanya".
user949300

13

Baik Paman Bob maupun David Arno (penulis kutipan yang Anda miliki) memiliki pelajaran penting yang dapat kita peroleh dari apa yang mereka tulis. Saya pikir ada baiknya mempelajari pelajaran dan kemudian memperkirakan apa arti sebenarnya bagi Anda dan proyek Anda.

Pertama: Pelajaran Paman Bob

Paman Bob menyatakan bahwa semakin banyak argumen yang Anda miliki dalam fungsi / metode Anda, semakin banyak pengembang yang menggunakannya harus memahaminya. Beban kognitif itu tidak datang secara gratis, dan jika Anda tidak konsisten dengan urutan argumen, dll. Beban kognitif hanya meningkat.

Itu adalah fakta menjadi manusia. Saya pikir kesalahan utama dalam buku Kode Paman Bob adalah pernyataan "Jumlah ideal argumen untuk fungsi adalah nol" . Minimalisme bagus sampai tidak. Sama seperti Anda tidak pernah mencapai batas dalam Kalkulus, Anda tidak akan pernah mencapai kode "ideal" - begitu pula Anda.

Seperti yang dikatakan Albert Einstein, "Semuanya harus sesederhana mungkin, tetapi tidak sederhana".

Kedua: Pelajaran David Arno

Cara mengembangkan David Arno dijelaskan adalah pengembangan gaya lebih fungsional daripada berorientasi objek . Namun, skala kode fungsional jauh lebih baik daripada pemrograman berorientasi objek tradisional. Mengapa? Karena terkunci. Keadaan waktu mana pun bisa berubah dalam suatu objek, Anda menghadapi risiko kondisi balapan atau mengunci pertikaian.

Setelah menulis sistem yang sangat konkuren yang digunakan dalam simulasi dan aplikasi sisi server lainnya, model fungsional ini bekerja dengan sangat baik. Saya bisa membuktikan perbaikan yang telah dilakukan oleh pendekatan ini. Namun, itu adalah gaya pengembangan yang sangat berbeda, dengan persyaratan dan idiom yang berbeda.

Pembangunan adalah serangkaian pertukaran

Anda tahu aplikasi Anda lebih baik daripada kita semua. Anda mungkin tidak perlu skalabilitas yang datang dengan pemrograman gaya fungsional. Ada dunia antara dua cita-cita yang tercantum di atas. Kita yang berurusan dengan sistem yang perlu menangani throughput tinggi dan paralelisme konyol akan cenderung ke arah pemrograman fungsional yang ideal.

Yang mengatakan, Anda bisa menggunakan objek data untuk menyimpan kumpulan informasi yang Anda perlukan untuk diteruskan ke suatu metode. Itu membantu masalah beban kognitif yang ditangani Paman Bob, sambil tetap mendukung cita-cita fungsional yang ditangani David Arno.

Saya telah bekerja pada kedua sistem desktop dengan paralelisme terbatas yang diperlukan, dan perangkat lunak simulasi throughput tinggi. Mereka memiliki kebutuhan yang sangat berbeda. Saya bisa menghargai kode berorientasi objek yang ditulis dengan baik yang dirancang di sekitar konsep menyembunyikan data yang Anda kenal. Ini berfungsi untuk beberapa aplikasi. Namun, itu tidak bekerja untuk mereka semua.

Siapa yang benar Nah, David lebih benar daripada Paman Bob dalam hal ini. Namun, poin mendasar yang ingin saya garis bawahi di sini adalah bahwa metode harus memiliki argumen sebanyak yang masuk akal.


Ada paralelisme. Pertempuran yang berbeda dapat diproses secara paralel. Namun, ya: satu pertempuran, saat sedang diproses, perlu dikunci.
gaazkam

Ya, saya maksudkan bahwa pembaca (penuai dalam analogi Anda) akan mendapatkan dari tulisan mereka (penabur). Yang mengatakan, saya telah kembali untuk melihat beberapa hal yang saya tulis di masa lalu dan entah mempelajari kembali sesuatu atau tidak setuju dengan diri saya sebelumnya. Kita semua belajar dan berkembang, dan itulah alasan nomor satu mengapa Anda harus selalu bernalar melalui bagaimana dan jika Anda menerapkan sesuatu yang Anda pelajari.
Berin Loritsch

8

OK - saya bisa membuat metode KEMBALI keadaan pertempuran daripada mengubahnya di tempat jika saya benar-benar ingin.

Ya, itulah idenya.

Maka apakah saya harus menyalin semua yang ada di status pertempuran untuk mengembalikan keadaan yang sama sekali baru alih-alih mengubahnya di tempat?

Tidak. "Status perang" Anda dapat dimodelkan sebagai struktur data yang tidak dapat diubah, yang berisi struktur data yang tidak dapat diubah sebagai blok bangunan, mungkin bersarang di beberapa hierarki struktur data yang tidak dapat diubah.

Jadi mungkin ada bagian dari kondisi pertempuran yang tidak harus diubah selama satu putaran, dan yang lain harus diubah. Bagian-bagian yang tidak berubah tidak harus disalin, karena tidak dapat diubah, orang hanya perlu menyalin referensi ke bagian-bagian itu, tanpa risiko memperkenalkan efek samping. Itu bekerja paling baik di lingkungan bahasa sampah yang dikumpulkan.

Google untuk "Struktur Data yang Tidak Berubah Efisien", dan Anda pasti akan menemukan beberapa referensi bagaimana ini bekerja secara umum.

Tampaknya jauh lebih sederhana bagi saya untuk hanya memodifikasi keadaan dan mengembalikan pembaruan.

Untuk masalah tertentu, ini memang bisa lebih sederhana. Game dan simulasi berbasis-bulat mungkin termasuk dalam kategori ini, mengingat sebagian besar kondisi permainan berubah dari satu putaran ke putaran lainnya. Namun, persepsi tentang apa yang benar-benar "sederhana" pada tingkat tertentu subjektif, dan itu tergantung juga pada apa yang digunakan orang.


8

Sebagai penulis komentar, saya kira saya harus mengklarifikasi di sini, karena tentu saja ada lebih dari itu daripada versi sederhana yang ditawarkan komentar saya.

AFAIK kedua prinsip ini pada dasarnya tidak kompatibel, hampir saling bertentangan: fungsional murni dapat diringkas sebagai "hanya mengembalikan nilai, tidak memiliki efek samping" sementara mengatakan, jangan tanya dapat diringkas sebagai "jangan mengembalikan apa pun, hanya memiliki efek samping ".

Sejujurnya, saya menemukan ini benar-benar aneh menggunakan istilah "katakan, jangan tanya". Jadi saya membaca apa yang dikatakan Martin Fowler tentang masalah ini beberapa tahun yang lalu, yang mencerahkan . Alasan saya menemukan itu aneh adalah karena "katakan jangan tanya" identik dengan injeksi ketergantungan di kepala saya dan bentuk paling murni injeksi ketergantungan melewati segala yang dibutuhkan fungsi melalui parameternya.

Tetapi nampaknya makna yang saya terapkan untuk "katakan jangan tanya" berasal dari mengambil definisi yang berfokus pada OO Fowler dan menjadikannya lebih paradigma agnostik. Dalam prosesnya, saya percaya dibutuhkan konsep untuk kesimpulan logisnya.

Mari kita kembali ke awal yang sederhana. Kami memiliki "gumpalan logika" (prosedur) dan kami memiliki data global. Prosedur membaca data itu secara langsung untuk mengaksesnya. Kami memiliki skenario "tanyakan" sederhana.

Maju sedikit. Kami sekarang memiliki objek dan metode. Data itu tidak lagi perlu bersifat global, dapat dikirimkan melalui konstruktor dan terkandung di dalam objek. Dan kemudian kita memiliki metode yang bertindak berdasarkan data itu. Jadi sekarang kita telah "katakan, jangan tanya" seperti yang dijelaskan Fowler. Objek diberi tahu datanya. Metode-metode itu tidak lagi harus meminta ruang lingkup global untuk data mereka. Tapi ini intinya: ini masih tidak benar "katakan, jangan tanya" dalam pandangan saya karena metode-metode itu masih harus menanyakan cakupan objek. Ini lebih merupakan skenario "katakan, lalu tanyakan" yang saya rasakan.

Jadi angin maju ke hari modern, buang pendekatan "itu OO sepanjang jalan" dan meminjam beberapa prinsip dari pemrograman fungsional. Sekarang ketika suatu metode dipanggil, semua data diberikan kepadanya melalui parameter-parameternya. Itu dapat (dan) telah diperdebatkan, "apa gunanya, itu hanya menyulitkan kodenya?" Dan ya, melewati parameter, data yang dapat diakses melalui ruang lingkup objek, menambah kompleksitas kode. Tetapi menyimpan data itu dalam suatu objek, daripada membuatnya dapat diakses secara global, menambah kompleksitas juga. Namun sedikit yang berpendapat bahwa variabel global selalu lebih baik karena mereka lebih sederhana. Intinya adalah, manfaat yang "katakan, jangan tanya" membawa, melebihi kerumitan mengurangi ruang lingkup. Ini berlaku lebih untuk melewati parameter daripada membatasi ruang lingkup objek.private staticdan lulus semua yang dibutuhkan melalui parameter dan sekarang metode itu dapat dipercaya untuk tidak secara diam-diam mengakses hal-hal yang seharusnya tidak. Lebih lanjut, ini mendorong menjaga metode ini tetap kecil, jika tidak, daftar parameter menjadi tidak terkendali. Dan itu mendorong metode penulisan yang sesuai dengan kriteria "fungsi murni".

Jadi saya tidak melihat "fungsional murni" dan "katakan, jangan tanya" sebagai berlawanan satu sama lain. Yang pertama adalah satu-satunya implementasi penuh yang terakhir sejauh yang saya ketahui. Pendekatan Fowler tidak lengkap "katakan, jangan tanya".

Tetapi penting untuk diingat bahwa "implementasi penuh permintaan jangan tanya" ini benar-benar adalah ideal, yaitu pragmatisme harus ikut berperan kurang kita menjadi idealis dan karenanya memperlakukannya secara salah sebagai satu-satunya pendekatan yang mungkin benar. Sangat sedikit aplikasi yang bisa mendekati efek samping 100% gratis karena alasan sederhana bahwa mereka tidak akan melakukan apa pun yang berguna jika mereka benar-benar bebas efek samping. Kita perlu mengubah keadaan, kita membutuhkan IO dll agar aplikasi berguna. Dan dalam kasus seperti itu, metode harus menyebabkan efek samping sehingga tidak boleh murni. Tetapi aturan praktis di sini adalah untuk menjaga metode "tidak murni" ini seminimal mungkin; hanya mereka memiliki efek samping karena mereka perlu, bukan sebagai norma.

Tampaknya masuk akal bagi saya untuk memiliki keadaan pertempuran. Karena ini adalah permainan berbasis giliran, saya menyimpan status pertempuran dalam kamus (multipemain - mungkin ada banyak pertempuran yang dimainkan oleh banyak pemain secara bersamaan). Setiap kali seorang pemain melakukan giliran mereka, saya memanggil metode yang tepat pada keadaan pertempuran yang (a) memodifikasi negara sesuai dan (b) mengembalikan pembaruan kepada para pemain, yang mendapatkan serial ke dalam JSON dan pada dasarnya hanya memberi tahu mereka apa yang baru saja terjadi pada naik.

Tampaknya lebih masuk akal untuk memiliki keadaan pertempuran bagiku; sepertinya penting. Seluruh tujuan kode tersebut adalah untuk menangani permintaan untuk mengubah status, mengelola perubahan status tersebut dan melaporkannya kembali. Anda bisa menangani keadaan itu secara global, Anda bisa memegangnya di dalam objek pemain individu atau Anda bisa mengedarkannya di sekitar serangkaian fungsi murni. Yang mana yang Anda pilih akan menjadi yang terbaik untuk skenario khusus Anda. Status global menyederhanakan desain kode, dan cepat, yang merupakan persyaratan utama dari sebagian besar game. Tetapi itu membuat kode lebih sulit untuk dipertahankan, diuji dan di-debug. Seperangkat fungsi murni akan membuat kode lebih kompleks untuk diterapkan dan berisiko terlalu lambat karena penyalinan data yang berlebihan. Tapi itu akan menjadi yang paling sederhana untuk diuji dan dirawat. "Pendekatan OO" berada di tengah jalan.

Kuncinya adalah: tidak ada satu solusi sempurna yang berfungsi sepanjang waktu. Tujuan dari fungsi murni adalah untuk membantu Anda "jatuh ke dalam lubang kesuksesan". Tetapi jika lubang itu sangat dangkal, karena kerumitannya dapat membawa ke kode, bahwa Anda tidak terlalu jatuh ke dalamnya sebagai tersandung, maka itu bukan pendekatan yang tepat untuk Anda. Bertujuan untuk cita-cita, tetapi bersikaplah pragmatis dan berhentilah ketika cita-cita itu bukanlah tempat yang baik untuk pergi kali ini.

Dan sebagai poin terakhir, hanya mengulangi: fungsi murni dan "katakan, jangan tanya" sama sekali tidak bertentangan.


5

Untuk apa pun, pernah dikatakan, ada konteks, di mana Anda dapat menempatkan pernyataan itu, yang akan membuatnya absurd.

masukkan deskripsi gambar di sini

Paman Bob benar-benar salah jika Anda mengambil saran argumen nol sebagai persyaratan. Dia sepenuhnya benar jika Anda menganggap itu berarti setiap argumen tambahan membuat kode lebih sulit dibaca. Itu datang dengan biaya. Anda tidak menambahkan argumen ke fungsi karena itu membuatnya lebih mudah dibaca. Anda menambahkan argumen ke fungsi karena Anda tidak bisa memikirkan nama baik yang membuat ketergantungan pada argumen itu jelas.

Sebagai contoh pi()adalah fungsi yang sangat baik. Mengapa? Karena saya tidak peduli bagaimana, atau bahkan jika, itu dihitung. Atau jika itu digunakan e, atau sin (), untuk sampai pada nomor yang dikembalikan. Saya setuju dengan itu karena namanya memberi tahu saya semua yang perlu saya ketahui.

Namun, tidak setiap nama memberi tahu saya semua yang perlu saya ketahui. Beberapa nama tidak mengungkapkan penting untuk memahami informasi yang mengontrol perilaku fungsi serta argumen terbuka. Itulah yang membuat gaya fungsional pemrograman lebih mudah untuk dipikirkan.

Saya dapat menjaga hal-hal yang tidak berubah dan efek samping gratis dalam gaya OOP sepenuhnya. Return hanyalah sebuah mekanik yang digunakan untuk meninggalkan nilai pada stack untuk prosedur selanjutnya. Anda dapat tetap sama abadi menggunakan port output untuk mengkomunikasikan nilai ke hal-hal tidak berubah lainnya sampai Anda menekan port output terakhir yang akhirnya harus mengubah sesuatu jika Anda ingin orang dapat membacanya. Itu berlaku untuk setiap bahasa, fungsional atau tidak.

Jadi tolong jangan mengklaim bahwa Pemrograman Fungsional dan Pemrograman Berorientasi Objek "secara fundamental tidak kompatibel". Saya dapat menggunakan objek dalam program fungsional saya dan saya dapat menggunakan fungsi murni dalam program OO saya.

Namun, ada biaya untuk mencampurnya: Harapan. Anda dapat dengan setia mengikuti mekanisme kedua paradigma dan masih menyebabkan kebingungan. Salah satu manfaat menggunakan bahasa fungsional adalah bahwa efek samping, walaupun harus ada untuk mendapatkan hasil, diletakkan di tempat yang dapat diprediksi. Kecuali tentu saja objek yang dapat diubah diakses dengan cara yang tidak disiplin. Lalu apa yang Anda ambil sebagai diberikan dalam bahasa itu berantakan.

Demikian pula, Anda dapat mendukung objek dengan fungsi murni, Anda dapat merancang objek yang tidak berubah. Masalahnya adalah, jika Anda tidak memberi sinyal bahwa fungsinya murni atau objek tidak berubah maka orang tidak mendapatkan manfaat alasan dari fitur-fitur itu sampai mereka menghabiskan banyak waktu membaca kode.

Ini bukan masalah baru. Selama bertahun-tahun orang telah membuat kode secara prosedural dalam "bahasa OO" dengan berpikir bahwa mereka melakukan OO karena mereka menggunakan "bahasa OO". Beberapa bahasa begitu bagus untuk mencegah Anda menembak diri sendiri. Agar gagasan ini berhasil, mereka harus hidup di dalam Anda.

Keduanya menyediakan fitur yang bagus. Anda bisa melakukan keduanya. Jika Anda cukup berani untuk mencampurkannya, silakan beri label dengan jelas.


0

Kadang-kadang saya berjuang untuk memahami semua aturan berbagai paradigma. Mereka kadang-kadang berselisih satu sama lain karena mereka berada dalam situasi ini.

OOP adalah paradigma imperatif yaitu menjalankan dengan gunting di dunia di mana hal-hal berbahaya terjadi.

FP adalah paradigma fungsional di mana seseorang menemukan keamanan absolut dalam komputasi murni. Tidak ada yang terjadi di sini.

Namun, semua program harus menjembatani ke dunia imperatif agar bermanfaat. Dengan demikian, inti fungsional, shell imperatif .

Hal-hal menjadi membingungkan ketika Anda mulai mendefinisikan objek yang tidak dapat diubah (yang perintahnya mengembalikan salinan yang dimodifikasi daripada benar-benar bermutasi). Anda berkata pada diri sendiri, "Ini OOP," dan "Saya mendefinisikan perilaku objek." Anda memikirkan kembali prinsip Tell, Don't Ask yang sudah dicoba dan diuji. Masalahnya adalah, Anda menerapkannya ke dunia yang salah.

Ranahnya sama sekali berbeda dan mematuhi aturan yang berbeda. Ranah fungsional membangun hingga ke titik di mana ia ingin melepaskan efek samping ke dunia. Agar efek-efek tersebut dapat dirilis, semua data yang akan dienkapsulasi dalam objek imperatif (seandainya ini ditulis seperti itu!) Harus tersedia pada shell imperatif. Tanpa akses ke data ini bahwa di dunia yang berbeda akan disembunyikan melalui enkapsulasi, itu tidak akan berfungsi. Tidak mungkin secara komputasi.

Jadi, ketika Anda sedang menulis objek yang tidak dapat diubah (yang oleh Clojure disebut sebagai struktur data persisten), ingatlah Anda berada di domain fungsional. Throw Tell, Don't Ask out the window, dan biarkan kembali di rumah hanya ketika Anda memasuki kembali ranah imperatif.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.