Cepat Fungsional itu mudah

gambar


Artikel tentang pemrograman fungsional menulis banyak tentang bagaimana pendekatan FP meningkatkan pengembangan: menjadi mudah untuk menulis, membaca, streaming, kode, tes, membangun arsitektur yang buruk dan rambut menjadi lembut dan halus .


Salah satu kelemahannya adalah ambang masuk yang tinggi. Mencoba memahami FP, saya menemukan sejumlah besar teori, fungsi, monad, teori kategori dan tipe data aljabar. Dan bagaimana menerapkan AF dalam praktiknya tidak jelas. Selain itu, contoh diberikan dalam bahasa yang tidak saya kenal - Haskell dan rock.


Kemudian saya memutuskan untuk mencari tahu FP sejak awal. Saya menemukan dan mengatakan kepada codefest bahwa FP benar-benar hanya bahwa kita sudah menggunakannya di Swift dan dapat menggunakannya dengan lebih efisien.


Pemrograman fungsional: fungsi murni dan kurangnya status


Menentukan apa artinya menulis dalam paradigma tertentu bukanlah tugas yang mudah. Paradigma telah dibentuk selama beberapa dekade oleh orang-orang dengan visi berbeda, diwujudkan dalam bahasa dengan pendekatan yang berbeda, dan dikelilingi oleh alat. Alat dan pendekatan ini dianggap sebagai bagian integral dari paradigma, tetapi pada kenyataannya tidak.


Sebagai contoh, diyakini bahwa pemrograman berorientasi objek berdiri di atas tiga pilar - warisan, enkapsulasi, dan polimorfisme. Tetapi enkapsulasi dan polimorfisme diimplementasikan pada fungsi dengan kemudahan yang sama seperti pada objek. Atau penutupan - mereka dilahirkan dalam bahasa fungsional murni, tetapi begitu lama bermigrasi ke bahasa industri sehingga mereka tidak lagi dikaitkan dengan FP. Monads juga masuk ke dalam bahasa industri, tetapi belum kehilangan keanggotaan mereka dalam Haskell bersyarat dalam pikiran orang.


Akibatnya, ternyata mustahil untuk secara jelas menentukan apa paradigma tertentu itu. Sekali lagi, saya menemukan ini di codefest 2019, di mana semua pakar FP, berbicara tentang paradigma fungsional, menyebut berbagai hal.


Secara pribadi, saya menyukai definisi dari wiki:


"Pemrograman fungsional adalah bagian dari matematika diskrit dan paradigma pemrograman di mana proses perhitungan diperlakukan sebagai menghitung nilai-nilai fungsi dalam pemahaman matematika yang terakhir (yang bertentangan dengan fungsi sebagai subprogram dalam pemrograman prosedural)."


Apa itu fungsi matematika? Ini adalah fungsi yang hasilnya hanya bergantung pada data yang diterapkan.


Contoh fungsi matematika dalam empat baris kode terlihat seperti ini:


func summ(a: Int, b: Int) -> Int { return a + b } let x = summ(a: 2, b: 3) 

Memanggil fungsi sum dengan argumen input 2 dan 3, kita dapatkan 5. Hasil ini tidak berubah. Ubah program, utas, tempat eksekusi - hasilnya akan tetap sama.


Dan fungsi non-matematika adalah ketika variabel global dideklarasikan di suatu tempat.


 var z = 5 

Fungsi penjumlahan sekarang menambahkan argumen input dan nilai z.


 func summ(a: Int, b: Int) -> Int { return a + b + z } let x = summ(a: 2, b: 3) 

Menambah ketergantungan pada negara global. Sekarang tidak mungkin untuk secara gamblang memprediksi nilai x. Ini akan terus berubah tergantung pada saat fungsi dipanggil. Kami memanggil fungsi 10 kali berturut-turut, dan setiap kali kami bisa mendapatkan hasil yang berbeda.


Versi lain dari fungsi non-matematika:


 func summ(a: Int, b: Int) -> Int { z = b - a return a + b } 

Selain mengembalikan jumlah argumen input, fungsi mengubah variabel global z. Fitur ini memiliki efek samping.


Pemrograman fungsional memiliki istilah khusus untuk fungsi matematika - fungsi murni. Fungsi murni adalah fungsi yang mengembalikan hasil yang sama untuk set nilai input yang sama dan tidak memiliki efek samping.


Fungsi murni adalah landasan FP, yang lainnya adalah sekunder. Diasumsikan bahwa, mengikuti paradigma ini, kita hanya menggunakannya. Dan jika Anda tidak bekerja dengan negara global atau yang bisa berubah, maka mereka tidak akan ada dalam aplikasi.


Kelas dan Struktur dalam Paradigma Fungsional


Awalnya, saya berpikir bahwa FP hanya tentang fungsi, dan kelas dan struktur hanya digunakan dalam OOP. Tetapi ternyata kelas juga cocok dengan konsep FP. Hanya mereka yang seharusnya, katakanlah, "bersih".


Kelas "murni" adalah kelas, semua metode yang merupakan fungsi murni, dan properti tidak dapat diubah. (Ini adalah istilah tidak resmi, diciptakan sebagai persiapan untuk laporan).


Lihatlah kelas ini:


 class User { let name: String let surname: String let email: String func getFullname() -> String { return name + " " + surname } } 

Itu dapat dianggap sebagai enkapsulasi data ...


 class User { let name: String let surname: String let email: String } 

dan fungsi untuk bekerja dengannya.


 func getFullname() -> String { return name + " " + surname } 

Dari sudut pandang FP, menggunakan kelas Pengguna tidak berbeda dengan bekerja dengan primitif dan fungsi.


Deklarasikan nilai - pengguna Vanya.


 let ivan = User( name: "", surname: "", email: "ivanov@example.com" ) 

Terapkan fungsi getFullname ke sana.


 let fullName = ivan.getFullname() 

Hasilnya, kami mendapatkan nilai baru - nama pengguna lengkap. Karena Anda tidak dapat mengubah parameter properti ivan, hasil pemanggilan getFullname tidak berubah.


Tentu saja, pembaca yang penuh perhatian dapat mengatakan: "Tunggu sebentar, metode getFullname mengembalikan hasilnya berdasarkan nilai global untuknya - properti kelas, bukan argumen." Tetapi sebenarnya suatu metode hanyalah fungsi di mana objek dilewatkan sebagai argumen.


Swift bahkan mendukung entri ini secara eksplisit:


 let fullName = User.getFullname(ivan)() 

Jika kita perlu mengubah beberapa nilai objek, misalnya email, kita harus membuat objek baru. Ini dapat dilakukan dengan metode yang sesuai.


 class User { let name: String let surname: String let email: String func change(email: String) -> User { return User(name: name, surname: surname, email: email) } } let newIvan = ivan.change(email: "god@example.com") 

Atribut Fungsional dalam Swift


Saya sudah menulis bahwa banyak alat, implementasi, dan pendekatan yang dianggap bagian dari paradigma sebenarnya dapat digunakan dalam paradigma lain. Misalnya, monad, tipe data aljabar, inferensi tipe otomatis, pengetikan ketat, tipe dependen, pengecekan kebenaran program selama kompilasi dianggap sebagai bagian dari FP. Tetapi banyak dari alat-alat ini dapat kita temukan di Swift.


Pengetikan dan inferensi tipe yang kuat adalah bagian dari Swift. Mereka tidak perlu dipahami atau diperkenalkan ke dalam proyek, kita hanya memilikinya.


Tidak ada tipe dependen, meskipun saya tidak akan menolak untuk memeriksa string oleh kompiler bahwa itu adalah email, array, bahwa itu tidak kosong, kamus, bahwa itu berisi kunci apel. Omong-omong, tidak ada tipe dependen di Haskell.


Jenis data aljabar tersedia, dan ini keren, tetapi sulit untuk dipahami secara matematis. Keindahannya adalah tidak perlu dipahami secara matematis untuk menggunakannya. Misalnya, Int, enum, Opsional, Hashable adalah tipe aljabar. Dan jika Int ada dalam banyak bahasa, dan Protokol ada di Objective-C, maka enum dengan nilai terkait, protokol dengan implementasi default dan tipe asosiatif jauh dari mana-mana.


Validasi kompilasi sering disebut ketika berbicara tentang bahasa seperti karat atau haskell. Dipahami bahwa bahasa ini sangat ekspresif sehingga memungkinkan Anda untuk menggambarkan semua kasus tepi sehingga diperiksa oleh kompiler. Jadi, jika program itu dikompilasi, maka itu pasti akan berhasil. Tidak ada yang membantah bahwa itu mungkin mengandung kesalahan dalam logika, karena Anda salah memfilter data untuk ditampilkan kepada pengguna. Tapi itu tidak akan jatuh, karena Anda tidak menerima data dari database, server mengembalikan jawaban yang salah yang Anda harapkan, atau pengguna memasukkan tanggal lahirnya sebagai string, bukan angka.


Saya tidak bisa mengatakan bahwa kompilasi kode cepat dapat menangkap semua bug: misalnya, mudah untuk mencegah kebocoran memori. Tapi pengetikan yang kuat dan Opsional melindungi terhadap banyak kesalahan bodoh. Hal utama adalah membatasi ekstraksi paksa.


Monads: bukan bagian dari paradigma FP, tetapi alat (opsional)


Cukup sering, FP dan monad digunakan dalam aplikasi yang sama. Pada suatu waktu, saya bahkan berpikir bahwa monad adalah pemrograman fungsional. Ketika saya memahaminya (tapi ini tidak akurat), saya membuat beberapa kesimpulan:


  • mereka sederhana;
  • mereka nyaman;
  • memahaminya secara opsional, itu sudah cukup untuk dapat diterapkan;
  • Anda dapat dengan mudah melakukannya tanpa mereka.

Swift sudah memiliki dua monad standar - Opsional dan Hasil. Keduanya dibutuhkan untuk menghadapi efek samping. Opsional melindungi dari kemungkinan nihil. Hasil - dari berbagai situasi luar biasa.


Pertimbangkan contoh yang dibawa ke titik absurditas. Misalkan kita memiliki fungsi yang mengembalikan integer dari database dan dari server. Yang kedua mungkin mengembalikan nol, tetapi kami menggunakan ekstraksi implisit untuk mendapatkan perilaku Objective-C-time.


 func getIntFromDB() -> Int func getIntFromServer() -> Int! 

Kami terus mengabaikan Opsional dan menerapkan fungsi untuk menjumlahkan angka-angka ini.


 func summInts() -> Int! { let intFromDB = getIntFromDB() let intFromServer = getIntFromServer()! let summ = intFromDB + intFromServer return summ } 

Kami memanggil fungsi final dan menggunakan hasilnya.


 let result = summInts() print(result) 

Akankah contoh ini berhasil? Yah, itu pasti mengkompilasi, tetapi apakah kita mendapatkan crash saat runtime atau tidak tidak diketahui siapa pun. Kode ini baik, dengan sempurna menunjukkan niat kami (kami membutuhkan jumlah dari dua angka) dan tidak mengandung sesuatu yang berlebihan. Tapi dia berbahaya. Karena itu, hanya junior dan orang yang percaya diri yang menulis seperti ini.


Ubah contoh untuk membuatnya aman.


 func getIntFromDB() -> Int func getIntFromServer() -> Int? func summInts() -> Int? { let intFromDB = getIntFromDB() let intFromServer = getIntFromServer() if let intFromServer = intFromServer { let summ = intFromDB + intFromServer return summ } else { return nil } } if let result = summInts() { print(result) } 

Kode ini bagus, aman. Dengan menggunakan ekstraksi eksplisit, kami bertahan melawan kemungkinan nol. Tapi itu menjadi rumit, dan di antara pemeriksaan yang aman sudah sulit untuk membedakan niat kita. Kami masih membutuhkan penjumlahan dari dua angka, bukan pemeriksaan keamanan.


Dalam hal ini, Opsional memiliki metode peta, yang diwarisi dari tipe Maybe dari Haskell. Kami menerapkannya, dan contohnya akan berubah.


 func getIntFromDB() -> Int func getIntFromServer() -> Int? func summInts() -> Int? { let intFromDB = getIntFromDB() let intFromServer = getIntFromServer() return intFromServer.map { x in x + intFromDB } } if let result = summInts() { print(result) } 

Atau bahkan lebih ringkas.


 func getIntFromDB() -> Int func getintFromServer() -> Int? func summInts() -> Int? { return getintFromServer().map { $0 + getIntFromDB() } } if let result = summInts() { print(result) } 

Kami menggunakan peta untuk mengonversi intFromServer ke hasil yang kami butuhkan tanpa ekstraksi.


Kami menyingkirkan cek di dalam summInts, tetapi meninggalkannya di tingkat atas. Ini dilakukan dengan sengaja, karena pada akhir rantai perhitungan kita harus memilih metode untuk memproses kurangnya hasil.


Keluarkan


 if let result = summInts() { print(result) } 

Gunakan nilai default


 print(result ?? 0) 

Atau tampilkan peringatan jika data tidak diterima.


 if let result = summInts() { print(result) } else { print("") } 

Sekarang kode dalam contoh tidak mengandung terlalu banyak, seperti pada contoh pertama, dan aman, seperti pada yang kedua.


Namun peta tidak selalu berfungsi sebagaimana mestinya


 let a: String? = "7" let b = a.map { Int($0) } type(of: b)//Optional<Optional<Int>> 

Jika kita melewatkan fungsi untuk memetakan, yang hasilnya opsional, kita mendapatkan dobel Opsional. Tapi kita tidak perlu perlindungan ganda terhadap nol. Satu sudah cukup. Metode flatMap memungkinkan untuk menyelesaikan masalah, itu adalah analog peta dengan satu perbedaan, menyebarkan boneka bersarang.


 let a: String? = "7" let b = a.flatMap { Int($0) } type(of: b)//Optional<Int>. 

Contoh lain di mana peta dan flatMap sangat tidak nyaman untuk digunakan.


 let a: Int? = 3 let b: Int? = 7 let c = a.map { $0 + b! } 

Bagaimana jika suatu fungsi mengambil dua argumen dan keduanya opsional? Tentu saja, FP punya solusinya - ini adalah fungsi aplikator dan currying. Tetapi alat ini terlihat agak canggung tanpa menggunakan operator khusus yang tidak dalam bahasa kita, dan menulis operator kustom dianggap sebagai bentuk yang buruk. Karena itu, kami mempertimbangkan cara yang lebih intuitif: kami menulis fungsi khusus.


 @discardableResult func perform<Result, U, Z>( _ transform: (U, Z) throws -> Result, _ optional1: U?, _ optional2: Z?) rethrows -> Result? { guard let optional1 = optional1, let optional2 = optional2 else { return nil } return try transform(optional1, optional2) } 

Dibutuhkan dua nilai opsional sebagai argumen dan fungsi dengan dua argumen. Jika kedua opsi memiliki nilai, fungsi diterapkan padanya.
Sekarang kita dapat bekerja dengan beberapa opsi tanpa menggunakan mereka.


 let a: Int? = 3 let b: Int? = 7 let result = perform(+, a, b) 

Monad kedua, Hasil, juga memiliki metode peta dan flatMap. Jadi, Anda dapat bekerja dengannya dengan cara yang persis sama.


 func getIntFromDB() -> Int func getIntFromServer() -> Result<Int, ServerError> func summInts() -> Result<Int, ServerError> { let intFromDB = getIntFromDB() let intFromServer = getIntFromServer() return intFromServer.map { x in x + intFromDB } } if case .success(let result) = summInts() { print(result) } 

Sebenarnya, inilah yang menyatukan para monad - kemampuan untuk bekerja dengan nilai di dalam wadah tanpa melepasnya. Menurut pendapat saya, ini membuat kode tersebut ringkas. Tetapi jika Anda tidak menyukainya, cukup gunakan ekstrak eksplisit, ini tidak bertentangan dengan paradigma FP.


Contoh: mengurangi jumlah fungsi kotor


Sayangnya, dalam program nyata, keadaan global dan efek samping ada di mana-mana - permintaan jaringan, sumber data, UI. Dan hanya fungsi murni yang tidak dapat diabaikan. Tetapi ini tidak berarti bahwa FP sama sekali tidak dapat diakses untuk kita: kita dapat mencoba mengurangi jumlah fungsi kotor, yang biasanya sangat banyak.


Mari kita lihat contoh kecil yang dekat dengan pengembangan produksi. Bangun UI, khususnya formulir entri. Bentuknya memiliki beberapa keterbatasan:


1) Masuk tidak kurang dari 3 karakter
2) Kata sandi setidaknya 6 karakter
3) Tombol "Login" aktif jika kedua bidang valid.
4) Warna bingkai bidang mencerminkan kondisinya, hitam - valid, merah - tidak valid


Kode yang menjelaskan batasan ini mungkin terlihat seperti ini:


Menangani input pengguna apa pun


 @IBAction func textFieldTextDidChange() { // 1.     // 2.   guard let login = loginView.text, let password = passwordView.text else { // 3. - loginButton.isEnabled = false return } let loginIsValid = login.count > constants.loginMinLenght if loginIsValid { // 4. - loginView.layer.borderColor = constants.normalColor } let passwordIsValid = password.count > constants.passwordMinLenght if passwordIsValid { // 5. - passwordView.layer.borderColor = constants.normalColor } // 6. - loginButton.isEnabled = loginIsValid && passwordIsValid } 

Pemrosesan penyelesaian login:


 @IBAction func loginDidEndEdit() { let color: CGColor // 1.     // 2.   if let login = loginView.text, login.count > 3 { color = constants.normalColor } else { color = constants.errorColor } // 3.   loginView.layer.borderColor = color } 

Proses penyelesaian kata sandi:


 @IBAction func passwordDidEndEdit() { let color: CGColor // 1.     // 2.   if let password = passwordView.text, password.count > 6 { color = constants.normalColor } else { color = constants.errorColor } // 3. - passwordView.layer.borderColor = color } 

Menekan tombol enter:


 @IBAction private func loginPressed() { // 1.     // 2.   guard let login = loginView.text, let password = passwordView.text else { return } auth(login: login, password: password) { [weak self] user, error in if let user = user { /*  */ } else if error is AuthError { guard let `self` = self else { return } // 3. - self.passwordView.layer.borderColor = self.constants.errorColor // 4. - self.loginView.layer.borderColor = self.constants.errorColor } else { /*   */ } } } 

Kode ini mungkin bukan yang terbaik, tetapi secara keseluruhan itu baik dan berfungsi. Benar, ia memiliki sejumlah masalah:


  • 4 ekstrak eksplisit;
  • 4 ketergantungan pada negara global;
  • 8 efek samping;
  • keadaan akhir yang tidak jelas;
  • aliran nonlinier.

Masalah utama adalah Anda tidak bisa hanya mengambil dan mengatakan apa yang terjadi dengan layar kami. Melihat satu metode, kami melihat apa yang dilakukannya dengan negara global, tetapi kami tidak tahu siapa, di mana dan kapan itu menyentuh negara. Akibatnya, untuk memahami apa yang terjadi, Anda perlu menemukan semua poin pekerjaan dengan pandangan dan memahami dalam urutan apa pengaruh apa yang terjadi. Menjaga semua ini dalam pikiran sangat sulit.


Jika proses mengubah keadaan adalah linier, Anda dapat mempelajarinya langkah demi langkah, yang akan mengurangi beban kognitif pada programmer.


Mari kita coba untuk mengubah contoh, menjadikannya lebih fungsional.


Pertama, kami mendefinisikan model yang menggambarkan kondisi layar saat ini. Ini akan memungkinkan Anda untuk mengetahui informasi apa yang diperlukan untuk pekerjaan itu.


 struct LoginOutputModel { let login: String let password: String var loginIsValid: Bool { return login.count > 3 } var passwordIsValid: Bool { return password.count > 6 } var isValid: Bool { return loginIsValid && passwordIsValid } } 

Model yang menjelaskan perubahan yang diterapkan pada layar. Dia perlu tahu persis apa yang akan kita ubah.


 struct LoginInputModel { let loginBorderColor: CGColor? let passwordBorderColor: CGColor? let loginButtonEnable: Bool? let popupErrorMessage: String? } 

Acara yang dapat menyebabkan kondisi layar baru. Jadi kita akan tahu persis tindakan apa yang mengubah layar.


 enum Event { case textFieldTextDidChange case loginDidEndEdit case passwordDidEndEdit case loginPressed case authFailure(Error) } 

Sekarang kami menjelaskan metode utama perubahan. Fungsi murni ini, berdasarkan peristiwa keadaan saat ini, mengumpulkan keadaan layar yang baru.


 func makeInputModel( event: Event, outputModel: LoginOutputModel?) -> LoginInputModel { switch event { case .textFieldTextDidChange: let mapValidToColor: (Bool) -> CGColor? = { $0 ? normalColor : nil } return LoginInputModel( loginBorderColor: outputModel .map { $0.loginIsValid } .flatMap(mapValidToColor), passwordBorderColor: outputModel .map { $0.passwordIsValid } .flatMap(mapValidToColor), loginButtonEnable: outputModel?.passwordIsValid ) case .loginDidEndEdit: return LoginInputModel(/**/) case .passwordDidEndEdit: return LoginInputModel(/**/) case .loginPressed: return LoginInputModel(/**/) case .authFailure(let error) where error is AuthError: return LoginInputModel(/**/) case .authFailure: return LoginInputModel(/**/) } } 

Yang paling penting adalah bahwa metode ini adalah satu-satunya yang diizinkan untuk terlibat dalam pembangunan negara baru - dan itu bersih. Itu bisa dipelajari selangkah demi selangkah. Lihat bagaimana peristiwa mengubah layar dari titik A ke titik B. Jika ada yang rusak, maka masalahnya ada di sini. Dan mudah untuk diuji.


Tambahkan properti tambahan untuk mendapatkan status saat ini, ini adalah satu-satunya metode yang tergantung pada keadaan global.


 var outputModel: LoginOutputModel? { return perform(LoginOutputModel.init, loginView.text, passwordView.text) } 

Tambahkan metode "kotor" lain untuk membuat efek samping mengubah layar.


 func updateView(_ event: Event) { let inputModel = makeInputModel(event: event, outputModel: outputModel) if let color = inputModel.loginBorderColor { loginView.layer.borderColor = color } if let color = inputModel.passwordBorderColor { passwordView.layer.borderColor = color } if let isEnable = inputModel.loginButtonEnable { loginButton.isEnabled = isEnable } if let error = inputModel.popupErrorMessage { showPopup(error) } } 

Meskipun metode updateView tidak bersih, itu adalah satu-satunya tempat di mana properti layar berubah. Item pertama dan terakhir dalam rantai perhitungan. Dan jika terjadi kesalahan, disinilah breakpoint akan berada.


Tetap hanya memulai konversi di tempat yang tepat.


 @IBAction func textFieldTextDidChange() { updateView(.textFieldTextDidChange) } @IBAction func loginDidEndEdit() { updateView(.loginDidEndEdit) } @IBAction func passwordDidEndEdit() { updateView(.passwordDidEndEdit) } 

Metode loginPressed keluar sedikit unik.


 @IBAction private func loginPressed() { updateView(.loginPressed) let completion: (Result<User, Error>) -> Void = { [weak self] result in switch result { case .success(let user): /*  */ case .failure(let error): self?.updateView(.authFailure(error)) } } outputModel.map { auth(login: $0.login, password: $0.password, completion: completion) } } 

Faktanya adalah bahwa mengklik tombol "Login" meluncurkan dua rantai perhitungan, yang tidak dilarang.


Kesimpulan


Sebelum belajar FP, saya membuat penekanan kuat pada paradigma pemrograman. Penting bagi saya bahwa kode mengikuti OOP, saya tidak suka fungsi statis atau objek stateless, saya tidak menulis fungsi global.


Sekarang bagi saya tampaknya semua hal yang saya anggap sebagai bagian dari paradigma agak sewenang-wenang. Yang utama adalah kode yang bersih dan mudah dimengerti. Untuk mencapai tujuan ini, Anda dapat menggunakan segala sesuatu yang mungkin: fungsi murni, kelas, monad, warisan, komposisi, inferensi jenis. Mereka semua rukun dan membuat kode lebih baik - hanya menerapkannya ke tempat itu.


Apa lagi yang harus dibaca pada topik


Definisi pemrograman fungsional dari Wikipedia
Haskell Starter Book
Penjelasan fungsi, jari, dan fungsi aplikatif di jari
Buku Haskell tentang praktik untuk Menggunakan Maybe (Opsional)
Buku tentang sifat fungsional Swift
Menentukan tipe data aljabar dari wiki
Artikel tentang tipe data aljabar
Artikel lain tentang tipe data aljabar
Laporan Yandex pada pemrograman fungsional di Swift
Menerapkan Perpustakaan Standar Pendahuluan (Haskell) di Swift
Perpustakaan dengan alat fungsional di Swift
Perpustakaan lain
Dan satu lagi

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


All Articles