Sedikit latihan pemrograman fungsional di Swift untuk pemula



Saya ingin memperkenalkan konsep Pemrograman Fungsional kepada pemula dengan cara paling sederhana, menyoroti beberapa kelebihannya dari banyak yang lain yang benar-benar akan membuat kode lebih mudah dibaca dan ekspresif. Saya mengambil beberapa demo menarik untuk Anda yang berada di Playground on Github .

Pemrograman Fungsional: Definisi


Pertama-tama, Pemrograman Fungsional bukan bahasa atau sintaksis, tetapi kemungkinan besar cara untuk memecahkan masalah dengan memecah proses kompleks menjadi yang lebih sederhana dan komposisi mereka selanjutnya. Seperti namanya, " Pemrograman Fungsional, " unit komposisi untuk pendekatan ini adalah fungsi ; dan tujuan dari fungsi tersebut adalah untuk menghindari perubahan status atau nilai di luar scope) .

Di Swift World, ada semua kondisi untuk ini, karena fungsi di sini adalah sebagai peserta penuh dalam proses pemrograman sebagai objek, dan masalah mutation diselesaikan pada tingkat konsep JENIS value (struktur struktur dan enum enumerasi) yang membantu mengelola mutabilitas ( mutation ) dan jelas mengomunikasikan bagaimana dan kapan ini bisa terjadi.

Namun, Swift tidak sepenuhnya memahami bahasa pemrograman Fungsional , ia tidak memaksa Anda untuk pemrograman Fungsional , meskipun Swift mengakui keunggulan pendekatan Fungsional dan menemukan cara untuk menanamkannya.

Dalam artikel ini, kami akan fokus pada penggunaan elemen bawaan dari Pemrograman Fungsional di Swift (yaitu, "di luar kotak") dan memahami bagaimana Anda dapat menggunakannya dengan nyaman dalam aplikasi Anda.

Pendekatan Imperatif dan Fungsional: Perbandingan


Untuk mengevaluasi Pendekatan Fungsional , mari kita bandingkan solusi untuk beberapa masalah sederhana dengan dua cara berbeda. Solusi pertama adalah " Imperatif, " di mana kode mengubah keadaan di dalam program.

 //Imperative Approach var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] for i in 0..<numbers.count { let timesTen = numbers[i] * 10 numbers[i] = timesTen } print(numbers) //[10, 20, 30, 40, 50, 60, 70, 80, 90, 100] 

Perhatikan bahwa kita memanipulasi nilai-nilai di dalam array yang dapat diubah yang bernama numbers , dan kemudian mencetaknya ke konsol. Melihat kode ini, cobalah untuk menjawab pertanyaan-pertanyaan berikut yang akan kita bahas dalam waktu dekat:

  1. Apa yang ingin Anda capai dengan kode Anda?
  2. Apa yang terjadi jika thread lain mencoba mengakses array numbers saat kode Anda berjalan?
  3. Apa yang terjadi jika Anda ingin memiliki akses ke nilai asli dalam array numbers ?
  4. Seberapa andal kode ini dapat diuji?

Sekarang mari kita lihat pendekatan " Fungsional " alternatif:

 //Functional Approach let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] extension Array where Element == Int { func timesTen() -> [Int] { var output = [Int]() for num in self { output.append(num * 10) } return output } } let result = numbers.timesTen() print(numbers) //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] print(result) //[10, 20, 30, 40, 50, 60, 70, 80, 90, 100] 

Dalam potongan kode ini, kita mendapatkan hasil yang sama pada konsol, mendekati solusi untuk masalah dengan cara yang sangat berbeda. Perhatikan bahwa kali ini array numbers kita tidak dapat diubah berkat kata kunci let . Kami telah memindahkan proses mengalikan angka dari array numbers ke metode timesTen() , yang terletak di ekstensi extension Array . Kami masih menggunakan for dan memodifikasi variabel yang disebut output , tetapi scope variabel ini hanya dibatasi oleh metode ini. Demikian pula, argumen input kami diteruskan ke metode timesTen() berdasarkan nilai ( by value ), yang memiliki cakupan yang sama dengan output variabel. Metode timesTen() dipanggil, dan kita dapat mencetak pada konsol array numbers asli dan hasil array result .
Mari kita kembali ke 4 pertanyaan kita.

1. Apa yang ingin Anda capai dengan kode Anda?

Dalam contoh kami, kami melakukan tugas yang sangat sederhana dengan mengalikan angka dalam array numbers dengan 10 .

Dengan pendekatan imperatif , untuk mendapatkan output, Anda harus berpikir seperti komputer, mengikuti instruksi dalam for loop. Dalam hal ini, kode menunjukkan Anda mencapai hasil. Dengan Pendekatan Fungsional , " " "dibungkus" dalam metode timesTen() . Asalkan metode ini diterapkan di tempat lain, Anda benar-benar hanya dapat melihat ekspresi numbers.timesTen() . Kode semacam itu dengan jelas menunjukkan dicapai oleh kode ini, dan bukan tugas tersebut diselesaikan. Ini disebut Pemrograman Deklaratif , dan mudah ditebak mengapa pendekatan semacam itu menarik. Pendekatan imperatif membuat pengembang memahami kode bekerja untuk menentukan harus ia lakukan. Pendekatan fungsional dibandingkan dengan pendekatan Imperatif jauh lebih "ekspresif" dan memberikan pengembang kesempatan mewah untuk hanya berasumsi bahwa metode melakukan apa yang diklaimnya harus dilakukan! (Jelas, asumsi ini hanya berlaku untuk kode pra-diverifikasi).

2. Apa yang terjadi jika thread lain mencoba mengakses array numbers saat kode Anda berjalan?

Contoh-contoh yang disajikan di atas ada dalam ruang yang sepenuhnya terisolasi, meskipun dalam lingkungan multi-utas yang kompleks, sangat mungkin bahwa dua threads mencoba mengakses sumber daya yang sama secara bersamaan. Dalam kasus pendekatan Imperatif , mudah untuk melihat bahwa ketika thread lain memiliki akses ke array numbers dalam proses menggunakannya, hasilnya akan ditentukan oleh urutan di mana threads mengakses array numbers . Situasi ini disebut race condition dan dapat menyebabkan perilaku yang tidak terduga dan bahkan ketidakstabilan dan crash aplikasi.

Sebagai perbandingan, Pendekatan Fungsional tidak memiliki "efek samping". Dengan kata lain, output dari metode output tidak mengubah nilai yang tersimpan di sistem kami dan hanya ditentukan oleh input. Dalam hal ini, setiap utas ( threads ) yang memiliki akses ke array numbers akan SELALU menerima nilai yang sama dan perilakunya akan stabil dan dapat diprediksi.

3. Apa yang terjadi jika Anda ingin memiliki akses ke nilai asli yang disimpan dalam array numbers ?

Ini adalah kelanjutan dari diskusi kami tentang "efek samping". Jelas, perubahan status tidak dilacak. Oleh karena itu, dengan pendekatan Imperatif , kami kehilangan status awal array numbers kami selama proses konversi. Solusi kami, berdasarkan pada Pendekatan Fungsional , menyimpan array numbers asli dan menghasilkan array result baru dengan properti yang diinginkan pada output. Itu meninggalkan array numbers asli utuh dan cocok untuk diproses di masa depan.

4. Seberapa andal kode ini dapat diuji?

Karena pendekatan Fungsional menghancurkan semua "efek samping", fungsi yang diuji sepenuhnya berada di dalam metode. Input dari metode ini TIDAK PERNAH akan mengalami perubahan, sehingga Anda dapat menguji beberapa kali menggunakan siklus sebanyak yang Anda suka, dan Anda akan SELALU mendapatkan hasil yang sama. Dalam hal ini, pengujian sangat mudah. Sebagai perbandingan, menguji solusi Imperatif dalam satu lingkaran akan mengubah awal entri dan Anda akan mendapatkan hasil yang sangat berbeda setelah setiap iterasi.

Ringkasan Manfaat


Seperti yang kita lihat dari contoh yang sangat sederhana, Pendekatan Fungsional adalah hal yang keren jika Anda berurusan dengan Model Data karena:

  • Itu deklaratif
  • Ini memperbaiki masalah terkait utas seperti race condition dan kebuntuan
  • Ini membuat keadaan tidak berubah, yang dapat digunakan untuk transformasi selanjutnya.
  • Mudah untuk diuji.

Mari kita melangkah lebih jauh dalam mempelajari Pemrograman Fungsional di Swift . Ini mengasumsikan bahwa "aktor" utama adalah fungsi, dan mereka harus menjadi objek utama dari kelas pertama .

Fungsi Kelas Pertama dan Fungsi Orde Tinggi


Agar suatu fungsi menjadi kelas satu, ia harus memiliki kemampuan untuk dinyatakan sebagai variabel. Ini memungkinkan Anda untuk mengelola fungsi sebagai JENIS data normal dan sekaligus menjalankannya. Untungnya, di Swift fungsi adalah objek dari kelas pertama, yaitu, mereka didukung dengan meneruskannya sebagai argumen ke fungsi lain, mengembalikannya sebagai hasil dari fungsi lain, menugaskannya ke variabel, atau menyimpannya dalam struktur data.

Karena itu, kami memiliki fungsi lain di Swift - fungsi tingkat tinggi yang didefinisikan sebagai fungsi yang menggunakan fungsi lain sebagai argumen atau mengembalikan fungsi. Ada banyak dari mereka: map , filter , reduce , forEach , flatMap , compactMap , sorted , dll. Contoh paling umum dari fungsi tingkat tinggi adalah map , filter dan reduce . Mereka tidak global, mereka semua "melekat" pada JENIS-JENIS tertentu. Mereka bekerja pada semua JENIS Sequence , termasuk Collection , yang diwakili oleh struktur data Swift seperti Array , Dictionary dan Set . Di Swift 5 , fungsi tingkat tinggi juga berfungsi dengan TYPE yang sama sekali baru - Result .

map(_:)


Dalam Swift map(_:) mengambil fungsi sebagai parameter dan mengubah nilai-nilai tertentu sesuai dengan fungsi ini. Misalnya, menerapkan map(_:) ke array nilai Array , kami menerapkan fungsi parameter untuk setiap elemen array asli dan kami mendapatkan array Array , tetapi juga nilai yang dikonversi.

 //Functional Approach let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] func timesTen(_ x:Int) -> Int { return x * 10 } let result = numbers.map (timesTen) print(numbers) //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] print(result) //[10, 20, 30, 40, 50, 60, 70, 80, 90, 100] 

Dalam kode di atas, kami membuat fungsi timesTen (_:Int) , yang mengambil nilai Int integer dan mengembalikan nilai integer Int dikalikan dengan 10 , dan menggunakannya sebagai parameter input ke map(_:) urutan tinggi kami map(_:) fungsi, menerapkannya ke array kami numbers . Kami mendapatkan hasil yang kami butuhkan di array result .

Nama fungsi parameter kali. timesTen untuk fungsi timesTen tinggi seperti map(_:) tidak masalah, parameter input dan nilai kembali penting, yaitu tanda tangan (Int) -> Int parameter input fungsi. Oleh karena itu, kita dapat menggunakan fungsi anonim di map(_:) - closure - dalam bentuk apa pun, termasuk yang dengan nama argumen pendek $0 , $1 , dll.

 //Functional Approach let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] let result = numbers.map { $0 * 10 } print(numbers) //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] print(result) //[10, 20, 30, 40, 50, 60, 70, 80, 90, 100] 

Jika kita melihat fungsi map(_ :) untuk Array , itu mungkin terlihat seperti ini:

 func map<T>(_ transform: (Element) -> T) -> [T] { var returnValue = [T]() for item in self { returnValue.append(transform(item)) } return returnValue } 

Ini adalah kode penting yang sudah tidak asing lagi bagi kami, tetapi ini bukan lagi masalah pengembang, ini masalah Apple , masalah Swift . Implementasi fungsi map(_:) tingkat tinggi map(_:) dioptimalkan oleh Apple dalam hal kinerja, dan kami, para pengembang, dijamin fungsionalitas map(_:) , jadi kami hanya dapat mengekspresikan dengan benar dengan argumen fungsi transform kami inginkan tanpa khawatir tentang itu akan diterapkan. Sebagai hasilnya, kami mendapatkan kode yang dapat dibaca dengan sempurna dalam bentuk satu baris, yang akan bekerja lebih baik dan lebih cepat.

 //Functional Approach let possibleNumbers = ["1", "2", "three", "///4///", "5"] let mapped = possibleNumbers.map {str in Int(str) } print (mapped) // [Optional(1), Optional(2), nil, nil, Optional(5)] 

dikembalikan oleh fungsi parameter mungkin tidak bersamaan dengan elemen dalam koleksi asli.

Dalam kode di atas, kami memiliki bilangan bulat yang possibleNumbers , Bilangan, diwakili sebagai string, dan kami ingin mengonversinya menjadi bilangan bulat dari Int , menggunakan Int(_ :String) inisialisasi failable Int(_ :String) diwakili oleh penutup { str in Int(str) } . Kami melakukan ini menggunakan map(_:) dan mendapatkan array yang mapped dari Optional sebagai output:



Kami tidak mengonversi elemen dari array kami yang mungkinNomor ke bilangan bulat, sebagai akibatnya, satu bagian menerima nilai nil , menunjukkan ketidakmungkinan mengkonversi String ke Int integer, dan bagian lainnya berubah menjadi Optionals , yang memiliki nilai:

 print (mapped) // [Optional(1), Optional(2), nil, nil, Optional(5)] 

compactMap(_ :)


Jika fungsi parameter yang diteruskan ke fungsi orde lebih tinggi memiliki nilai Optional pada output, maka mungkin lebih berguna untuk menggunakan fungsi orde lain yang lebih tinggi, serupa artinya - compactMap(_ :) , yang melakukan hal yang sama seperti map(_:) , tetapi juga "memperluas" nilai yang diterima pada output Optional dan menghilangkan nilai nil dari koleksi.



Dalam hal ini, kami mendapatkan array TYPE [Int] compactMapped , tetapi mungkin lebih kecil:

 let possibleNumbers = ["1", "2", "three", "///4///", "5"] let compactMapped = possibleNumbers.compactMap(Int.init) print (compactMapped) // [1, 2, 5] 



Setiap kali Anda menggunakan init?() Initializer sebagai fungsi transformasi, Anda harus menggunakan compactMap(_ :) :

 // Validate URLs let strings = ["https://demo0989623.mockable.io/car/1", "https://i.imgur.com/Wm1xcNZ.jpg"] let validateURLs = strings.compactMap(URL.init) // Separate Numbers and Operations let mathString: String = "12-37*2/5+44" let numbers1 = mathString.components(separatedBy: ["-", "*", "+", "/"]).compactMap(Int.init) print(numbers1) // [12, 37, 2, 5, 44] 

Saya harus mengatakan bahwa ada lebih dari cukup alasan untuk menggunakan fungsi compactMap(_ :) tingkat tinggi compactMap(_ :) . Swift “loves” Nilai Optional , mereka dapat diperoleh tidak hanya dengan menggunakan init?()failableinit?() Initializer, tetapi juga dengan menggunakan as? "Casting":

 let views = [innerView,shadowView,logoView] let imageViews = views.compactMap{$0 as? UIImageView} 

... dan try? saat memproses kesalahan yang dilemparkan oleh beberapa metode. Saya harus mengatakan bahwa Apple khawatir bahwa penggunaan try? sangat sering mengarah ke Optional ganda dan di Swift 5 sekarang hanya menyisakan satu level Optional setelah try? .

Ada satu lagi fungsi serupa dalam nama flatMap(_ :) orde tinggi flatMap(_ :) , yang sedikit lebih rendah.

Terkadang, untuk menggunakan map(_:) fungsi tingkat tinggi map(_:) , berguna untuk menggunakan metode zip (_:, _:) untuk membuat urutan pasangan dari dua urutan yang berbeda.

Misalkan kita memiliki view di mana beberapa titik diwakili, terhubung bersama dan membentuk garis putus-putus:



Kita perlu membangun garis terputus lain yang menghubungkan titik tengah segmen dari garis terputus asli:



Untuk menghitung titik tengah segmen, kita harus memiliki koordinat dua titik: arus dan berikutnya. Untuk melakukan ini, kita dapat membuat urutan yang terdiri dari pasangan poin - saat ini dan selanjutnya - menggunakan metode zip (_:, _:) points.dropFirst() zip (_:, _:) , di mana kita akan menggunakan array dari titik awal dan array dari points.dropFirst() :

 let pairs = zip (points,points.dropFirst()) let averagePoints = pairs.map { CGPoint(x: ($0.x + $1.x) / 2, y: ($0.y + $1.y) / 2 )} 

Memiliki urutan seperti itu, kami sangat mudah menghitung titik tengah menggunakan map(_:) fungsi tingkat tinggi map(_:) dan menampilkannya pada grafik.

filter (_:)


Di Swift , filter (_:) fungsi urutan tinggi filter (_:) tersedia untuk sebagian besar yang menyediakan fungsi map(_:) . Anda dapat memfilter urutan Sequence apa pun dengan filter (_:) , ini jelas! Metode filter (_:) mengambil fungsi lain sebagai parameter, yang merupakan kondisi untuk setiap elemen dari urutan, dan jika kondisinya terpenuhi, maka elemen tersebut dimasukkan dalam hasil, dan jika tidak, itu tidak termasuk. "Fungsi lain" ini mengambil nilai tunggal - elemen dari urutan Sequence - dan mengembalikan Bool , predikat yang disebut.

Sebagai contoh, untuk array Array , filter (_:) fungsi orde tinggi filter (_:) menerapkan fungsi predikat dan mengembalikan array lain yang hanya terdiri dari elemen-elemen array asli di mana fungsi predikat input mengembalikan true .

 //Functional Approach let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] let filted = numbers.filter{$0 % 2 == 0} //[2, 4, 6, 8, 10] 

Di sini, filter (_:) fungsi urutan tinggi filter (_:) mengambil setiap elemen dari array numbers (diwakili oleh $0 ) dan memeriksa untuk melihat apakah elemen ini adalah bilangan genap. Jika ini adalah bilangan genap, maka elemen-elemen dari array numbers termasuk dalam array yang baru, jika tidak. Kami dalam bentuk deklaratif menginformasikan program ingin kami dapatkan alih-alih peduli kita harus melakukannya.

Saya akan memberikan contoh lain menggunakan filter (_:) fungsi tingkat tinggi filter (_:) untuk mendapatkan hanya 20 angka Fibonacci pertama dengan nilai < 4000 :

 let fibonacci = sequence(first: (0, 1), next: { ($1, $0 + $1) }) .prefix(20).map{$0.0} .filter {$0 % 2 == 0 && $0 < 4000} print (fibonacci) // [0, 2, 8, 34, 144, 610, 2584] 

Kami mendapatkan urutan tupel yang terdiri dari dua elemen dari urutan Fibonacci: yang ke-n dan (ke-1):

 (0, 1), (1, 1), (1, 2), (2, 3), (3, 5) … 

Untuk pemrosesan lebih lanjut, kami membatasi jumlah elemen ke dua puluh elemen pertama menggunakan prefix (20) dan mengambil elemen 0 - 0 dari tupel yang dihasilkan menggunakan map {$0.0 } , yang akan sesuai dengan urutan Fibonacci mulai dengan 0 :

 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584,... 

Kita bisa mengambil elemen 1 dari tuple yang terbentuk menggunakan map {$0.1 } , yang akan sesuai dengan urutan Fibonacci dimulai dengan 1 :

 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584,... 

Kami mendapatkan elemen yang kami butuhkan dengan bantuan filter {$0 % 2 == 0 && $0 < 4000} fungsi orde tinggi filter {$0 % 2 == 0 && $0 < 4000} , yang mengembalikan array elemen urutan yang memenuhi predikat yang diberikan. Dalam kasus kami, ini akan menjadi array bilangan bulat [Int] :

 [0, 2, 8, 34, 144, 610, 2584] 

Ada contoh berguna lain menggunakan filter (_:) untuk Collection .

Saya dihadapkan dengan satu masalah nyata , ketika Anda memiliki berbagai images yang ditampilkan menggunakan CollectionView , dan menggunakan teknologi Drag & Drop Anda dapat mengumpulkan sejumlah besar gambar dan memindahkannya ke mana-mana, termasuk memindahkannya ke " tempat sampah. "



Dalam hal ini, array indeks yang removedIndexes dibuang ke "tempat sampah" sudah diperbaiki, dan Anda perlu membuat array gambar baru, kecuali yang indeksnya berada dalam array yang removedIndexes . Misalkan kita memiliki array images integer yang meniru gambar, dan array indeks integer ini removedIndexes yang perlu dihapus. Kami akan menggunakan filter (_:) untuk menyelesaikan masalah kami:

 var images = [6, 22, 8, 14, 16, 0, 7, 9] var removedIndexes = [2,5,0,6] var images1 = images .enumerated() .filter { !removedIndexes.contains($0.offset) } .map { $0.element } print (images1) // [22, 14, 16, 9] 

Metode enumerated() mengembalikan urutan tupel yang terdiri dari indeks offset dan nilai element array.Kemudian kami menerapkan filter filterke urutan tupel yang dihasilkan, hanya menyisakan mereka yang indeksnya $0.offsettidak terkandung dalam array removedIndexes. Langkah selanjutnya, kita memilih nilai dari tuple $0.elementdan mendapatkan array yang kita butuhkan images1.

reduce (_:, _:)


Metode ini reduce (_:, _:)juga tersedia untuk sebagian besar yang tersedia map(_:)dan metode filter (_:). Metode reduce (_:, _:)"runtuh" ​​urutan Sequenceke nilai akumulasi tunggal dan memiliki dua parameter. Parameter pertama adalah nilai akumulasi awal, dan parameter kedua adalah fungsi yang menggabungkan nilai akumulasi dengan elemen urutan Sequenceuntuk mendapatkan nilai akumulasi baru.

Fungsi parameter input diterapkan ke setiap elemen dari urutan Sequence, satu demi satu, hingga mencapai akhir dan menciptakan nilai akumulasi akhir.

 let sum = Array (1...100).reduce(0, +) 

Ini adalah contoh sepele klasik menggunakan fungsi urutan yang lebih tinggi reduce (_:, _:)- menghitung jumlah elemen array Array.

     1 0 1 0 +1 = 1 2 1 2 2 + 1 = 3 3 3 3 3 + 3 = 6 4 6 4 4 + 6 = 10 . . . . . . . . . . . . . . . . . . . 100 4950 100 4950 + 100 = 5050 

Dengan menggunakan fungsi ini, reduce (_:, _:)kita dapat dengan mudah menghitung jumlah angka Fibonacci yang memenuhi kondisi tertentu:

 let fibonacci = sequence(first: (0, 1), next: { ($1, $0 + $1) }) .prefix(20).map{$0.0} .filter {$0 % 2 == 0 && $0 < 4000} print (fibonacci) // [0, 2, 8, 34, 144, 610, 2584] print(fibonacci.reduce(0,+)) // 3382 

Tetapi ada aplikasi yang lebih menarik dari fungsi urutan yang lebih tinggi reduce (_:, _:).

Misalnya, kita dapat dengan sangat sederhana dan ringkas menentukan parameter yang sangat penting untuk UIScrollView- ukuran area "yang dapat digulir" contentSize- berdasarkan ukurannya subviews:

 let scrollView = UIScrollView() scrollView.addSubview(UIView(frame: CGRect(x: 300.0, y: 0.0, width: 200, height: 300))) scrollView.addSubview(UIView(frame: CGRect(x: 100.0, y: 0.0, width: 300, height: 600))) scrollView.contentSize = scrollView.subviews .reduce(CGRect.zero,{$0.union($1.frame)}) .size // (500.0, 600.0) 

Dalam demo ini, nilai akumulasi adalah GCRect, dan operasi akumulasi adalah operasi menggabungkan unionpersegi panjang yang adalah framemilik kita subviews.

Terlepas dari kenyataan bahwa fungsi tingkat tinggi reduce (_:, _:)mengasumsikan karakter akumulatif, ia dapat digunakan dalam perspektif yang sama sekali berbeda. Misalnya, untuk membagi tupel menjadi bagian-bagian dalam array tupel:

 // Separate Tuples let arr = [("one", 1), ("two", 2), ("three", 3), ("four", 4)] let (arr1, arr2) = arr.reduce(([], [])) { ($0.0 + [$1.0], $0.1 + [$1.1]) } print(arr1) // ["one", "two", "three", "four"] print(arr2) // [1, 2, 3, 4] 

Swift 4.2memperkenalkan jenis baru fungsi orde tinggi reduce (into:, _:). Metode reduce (into:, _:)ini lebih disukai dalam efisiensi dibandingkan dengan metode reduce (:, :)jika COW (copy-on-write) , misalnya, Arrayatau digunakan sebagai struktur yang dihasilkan Dictionary.

Ini dapat digunakan secara efektif untuk menghapus nilai yang cocok dalam array bilangan bulat:

 // Remove duplicates let arrayInt = [1,1,2,6,6,7,2,9,7].reduce(into: []) { !$0.contains($1) ? $0.append($1) : () } // [1, 2, 6, 7, 9] 

... atau ketika menghitung jumlah elemen yang berbeda dalam array:

 // Count equal elements in array let arrayIntCount = [1,1,2,2,6,6,7,2,9,7].reduce(into: [:]) { counts, letter in counts[letter, default: 0] += 1 } // [6: 2, 9: 1, 1: 2, 2: 3, 7: 2] 

flatMap (_:)


Sebelum beralih ke fungsi tingkat tinggi ini, mari kita lihat demo yang sangat sederhana.

 let maybeNumbers = ["42", "7", "three", "///4///", "5"] let firstNumber = maybeNumbers.map (Int.init).first 

Jika kita menjalankan kode ini untuk mengeksekusi Playground, maka semuanya terlihat baik, dan kita firstNumberadalah sama 42:



Tapi, jika Anda tidak tahu, Playgroundsering menyembunyikan yang benar , khususnya konstanta firstNumber. Sebenarnya, konstanta firstNumbermemiliki dua hal Optional:



Ini karena map (Int.init)pada output ia membentuk sebuah array Optionaldari nilai-nilai TYPE [Int?], karena tidak setiap baris Stringdapat dikonversi ke Intdan penginisialisasi Int.intadalah "jatuh" ( failable). Kemudian kita mengambil elemen pertama dari array yang dibentuk menggunakan fungsi firstuntuk array Array, yang juga membentuk outputOptional, karena array mungkin kosong dan kami tidak akan bisa mendapatkan elemen pertama dari array. Sebagai hasilnya, kami memiliki dobel Optional, yaitu Int??.

Kami memiliki struktur bersarang Optionaldi Optionalmana itu benar-benar lebih sulit untuk dikerjakan dan yang secara alami tidak kami inginkan. Untuk mendapatkan nilai dari struktur bersarang ini, kita harus "menyelam" ke dalam dua tingkat. Selain itu, setiap transformasi tambahan dapat memperdalam level Optionallebih rendah.

Mendapatkan nilai dari double nested Optionalbenar - benar memberatkan.

Kami memiliki 3 opsi dan semuanya membutuhkan pengetahuan mendalam tentang bahasa tersebut Swift.

  • if let , ; «» «» Optional , — «» Optional :

  • if case let ( pattern match ) :



    ?? :

  • , switch :


Lebih buruk lagi, masalah bersarang tersebut muncul dalam situasi apa pun yang melibatkan genericwadah umum ( ) yang untuknya suatu operasi didefinisikan map. Misalnya, untuk array Array.

Pertimbangkan kode contoh lain. Misalkan kita memiliki teks multi-baris multilineStringyang ingin kita bagi menjadi kata-kata yang ditulis dalam huruf kecil (kecil):

 let multilineString = """  ,  ,   ;     , —  ,   :  —   ,   .   ,   ,   .    .  ,        ,  « »  .  ,  ,   ! """ let words = multilineString.lowercased() .split(separator: "\n") .map{$0.split(separator: " ")} 

Untuk mendapatkan array kata words, pertama-tama kita membuat huruf besar (besar) huruf kecil (kecil) menggunakan metode lowercased(), Kemudian kita membagi teks menjadi split(separatot: "\n")garis - garis menggunakan metode dan mendapatkan array string, dan kemudian menggunakannya map {$0.split(separator: " ")}untuk memisahkan setiap baris menjadi kata-kata yang terpisah.

Akibatnya, kami mendapatkan array bersarang:

 [["", ",", "", ","], ["", "", ";", "", "", "", "", ",", "—"], ["", ",", "", "", ":"], ["", "—", "", "", ",", "", "", "."], ["", "", ",", "", "", ","], ["", "", ".", "", ""], ["", ".", "", ",", ""], ["", "", "", ""], ["", "", ",", "", "«", "»"], ["", ".", "", ","], ["", ",", "", "", "!"]] 

... dan wordsmemiliki ganda Array:



Sekali lagi, kami menerima "bersarang" struktur data, tapi kali ini kami tidak Optional, dan Array. Jika kita ingin terus memproses kata-kata yang diterima words, misalnya, untuk menemukan spektrum huruf dari teks multi-baris ini, maka pertama-tama kita harus “meluruskan” array ganda Arraydan mengubahnya menjadi array tunggal Array. Ini mirip dengan apa yang kami lakukan dengan menggandakan Optionaluntuk demo di awal bagian ini pada flatMap:

 let maybeNumbers = ["42", "7", "three", "///4///", "5"] let firstNumber = maybeNumbers.map (Int.init).first 

Untungnya, Swiftkita tidak perlu menggunakan konstruksi sintaksis yang rumit. Swiftmemberi kami solusi siap pakai untuk array Arraydan Optional. Ini adalah fungsi urutan yang lebih tinggi flatMap! Ini sangat mirip dengan map, tetapi memiliki fungsi tambahan yang terkait dengan "pelurusan" "lampiran" yang muncul selama eksekusi map. Dan itulah mengapa disebut flatMap, itu “meluruskan” ( flattens) hasilnya map.

Mari kita coba menerapkan flatMapke firstNumber:



Kami benar-benar mendapatkan output dengan satu level Optional. Bekerja

lebih menarik flatMapuntuk sebuah array Array. Dalam ungkapan kami untuk, wordskami hanya mengganti mapdenganflatMap:



... dan kami hanya mendapatkan berbagai kata wordstanpa "bersarang":

 ["", ",", "", ",", "", "", ";", "", "", "", "", ",", "—", "", ",", "", "", ":", "", "—", "", "", ",", "", "", ".", "", "", ",", "", "", ",", "", "", ".", "", "", "", ".", "", ",", "", "", "", "", "", "", "", ",", "", "«", "»", "", ".", "", ",", "", ",", "", "", "!"] 

Sekarang kita dapat melanjutkan pemrosesan yang kita butuhkan dari susunan kata yang dihasilkan words, tetapi hati-hati. Jika kita menerapkannya sekali lagi flatMapuntuk setiap elemen array words, kita akan mendapatkan, mungkin, hasil yang tak terduga, tetapi cukup bisa dimengerti.



Kami mendapatkan satu array, bukan huruf "simbol" yang [Character]terkandung dalam frase multi-baris kami:

 ["", "", "", "", "", "", "", "", "", "", "", "", ",", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ",", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ";", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ...] 

Faktanya adalah string Stringadalah kumpulan Collectionkarakter [Character]dan, berlaku flatMapuntuk setiap kata individual, kami sekali lagi menurunkan tingkat "bersarang" dan datang ke berbagai karakter flattenCharacters.
Mungkin ini yang Anda inginkan, atau mungkin tidak. Perhatikan ini.

Menyatukan semuanya: memecahkan beberapa masalah


TUGAS 1


Kita dapat melanjutkan pemrosesan susunan kata yang diperoleh pada bagian sebelumnya yang kita butuhkan wordsdan menghitung frekuensi kemunculan huruf dalam frasa multi-baris kita. Untuk memulai, mari “tempel” semua kata dari array wordske dalam satu baris besar dan kecualikan semua tanda baca darinya, yaitu, tinggalkan hanya huruf-huruf:

 let wordsString = words.reduce ("",+).filter { "" .contains($0)} //  

Jadi, kami mendapat semua surat yang kami butuhkan. Sekarang mari kita buat kamus mereka, di mana kuncinya keyadalah huruf, dan nilainya valueadalah frekuensi kemunculannya dalam teks.

Kita bisa melakukan ini dengan dua cara.
Metode pertama dikaitkan dengan penggunaan Swift 4.2variasi baru dari fungsi urutan yang lebih tinggi yang telah muncul di reduce (into:, _:). Metode ini sangat cocok bagi kita untuk mengatur kamus letterCountdengan frekuensi kemunculan huruf dalam frasa multi-baris kami:

 let letterCount = wordsString.reduce(into: [:]) { counts, letter in counts[letter, default: 0] += 1} print (letterCount) // ["": 1, "": 18, "": 2, "": 2, "": 5, "": 7, "": 17, "": 4, "": 23, ...] 

Sebagai hasilnya, kita akan mendapatkan kamus letterCount [Character : Int]di mana kunci keyadalah karakter yang ditemukan dalam frasa yang diteliti, dan karena nilainya valueadalah jumlah karakter ini.

Metode kedua melibatkan menginisialisasi kamus menggunakan pengelompokan, yang memberikan hasil yang sama:

 let letterCountDictionary = Dictionary(grouping: wordsString ){ $0}.mapValues {$0.count} letterCount == letterCountDictionary // true 

Kami ingin mengurutkan kamus letterCountberdasarkan abjad:

 let lettersStat = letterCountDictionary .sorted(by: <) .map{"\($0.0):\($0.1)"} print (lettersStat) // [":17", ":5", ":18", ":4", ":8", ":35", ":3", ":4", ":18", ":5", ":2", ":10", ":4", ":26", ":34", ":5", ":7", ":23", ":25", ":4", ":2", ":3", ":4", ":2", ":1", ":14", ":2", ":4"] 

Tetapi kami tidak dapat mengurutkan kamus secara langsung Dictionary, karena pada dasarnya ini bukan struktur data yang diurutkan. Jika kita menerapkan fungsi sorted (by:)ke kamus Dictionary, maka itu akan mengembalikan kepada kita elemen-elemen dari urutan yang diurutkan dengan predikat yang diberikan dalam bentuk array dari nama tupel, yang mapkita ubah menjadi sebuah array string yang [":17", ":5", ":18", ...]mencerminkan frekuensi kemunculan huruf yang sesuai.

Kami melihat bahwa kali ini sorted (by:)hanya operator " <" yang dilewatkan sebagai predikat ke fungsi tingkat tinggi . Fungsi sorted (by:)mengharapkan "fungsi perbandingan" sebagai satu-satunya argumen pada input. Ini digunakan untuk membandingkan dua nilai yang berdekatan dan memutuskan apakah mereka dipesan dengan benar (dalam hal ini, pengembaliantrue) atau tidak (pengembalian false). Kami dapat memberikan fungsi "fungsi perbandingan" ini sorted (by:)dalam bentuk penutupan anonim:

 sorted(by: {$0.key < $1.key} 

Dan kita bisa memberikannya " <" operator , yang memiliki tanda tangan yang kita butuhkan, seperti yang dilakukan di atas. Ini juga fungsi, dan pengurutan berdasarkan kunci sedang berlangsung key.

Jika kita ingin mengurutkan kamus berdasarkan nilai valuedan mencari huruf mana yang paling sering ditemukan dalam frasa ini, maka kita harus menggunakan penutup untuk fungsi sorted (by:):

 let countsStat = letterCountDictionary .sorted(by: {$0.value > $1.value}) .map{"\($0.0):\($0.1)"} print (countsStat ) //[":35", ":34", ":26", ":25", ":23", ":18", ":18", ":17", ":14", ":10", ":8", ":7", ":5", ":5", ":5", ":4", ":4", ":4", ":4", ":4", ":4", ":3", ":3", ":2", ":2", ":2", ":2", ":1"] 

Jika kita melihat solusi untuk masalah menentukan spektrum huruf dari frase multiline secara keseluruhan ...

 let multilineString = """  ,  ,   ;     , —  ,   :  —   ,   .   ,   ,   .    .  ,        ,  « »  .  ,  ,   ! """ let words = multilineString.lowercased() .split(separator: "\n") .flatMap{$0.split(separator: " ")} let wordsString = words.reduce ("",+).filter { "" .contains($0)} let letterCount = wordsString.reduce(into: [:]) { counts, letter in counts[letter, default: 0] += 1} let lettersStat = letterCountDictionary .sorted(by: <) .map{"\($0.0):\($0.1)"} print (lettersStat) // [":17", ":5", ":18", ":4", ":8", ":35", ":3", ":4", ":18", ":5", ":2", ":10", ":4", ":26", ":34", ":5", ":7", ":23", ":25", ":4", ":2", ":3", ":4", ":2", ":1", ":14", ":2", ":4"] let countsStat = letterCountDictionary .sorted(by: {$0.value > $1.value}) .map{"\($0.0):\($0.1)"} print (countsStat ) //[":35", ":34", ":26", ":25", ":23", ":18", ":18", ":17", ":14", ":10", ":8", ":7", ":5", ":5", ":5", ":4", ":4", ":4", ":4", ":4", ":4", ":3", ":3", ":2", ":2", ":2", ":2", ":1"] 

... maka kita akan melihat bahwa dalam fragmen kode ini pada dasarnya tidak ada variabel (tidak var, hanya let)semua nama fungsi yang digunakan mencerminkan TINDAKAN (fungsi) atas informasi tertentu, sama sekali tidak khawatir tentang BAGAIMANA tindakan ini diimplementasikan:

split- split,
map- transform
flatMap- transform with keselarasan (dengan menghapus satu tingkat bersarang),
filter- filter,
sorted- menyortir,
reduce- untuk mengubah data menjadi struktur tertentu dengan cara operasi tertentu

dalam fragmen ini setiap baris kode menjelaskan nama fungsi yang kita gunakan jika kita berada dalam. fill "murni" transformasi digunakan mapjika kita melakukan konversi dari tingkat bersarang digunakanflatMap, jika kami hanya ingin memilih data tertentu, maka kami menggunakan filter, dll. Semua fungsi "urutan tertinggi" ini dirancang dan diuji Appledengan mempertimbangkan pengoptimalan kinerja akun. Jadi kode ini sangat andal dan ringkas - kami tidak membutuhkan lebih dari 5 kalimat untuk menyelesaikan masalah kami. Ini adalah contoh pemrograman fungsional.

Satu-satunya kelemahan menerapkan pendekatan fungsional dalam demo ini adalah bahwa, demi ketidakberdayaan, kemampuan pengujian dan keterbacaan, kami berulang kali mengejar teks kami melalui berbagai fungsi tingkat tinggi. Dalam kasus sejumlah besar barang koleksi, Collectionkinerja dapat turun drastis. Misalnya, jika kita pertama kali menggunakan filter(_:)dan, dan kemudian - first.
MasukSwift 4 Beberapa opsi fitur baru telah ditambahkan untuk meningkatkan kinerja, dan berikut adalah beberapa tips untuk menulis kode yang lebih cepat.

1. Gunakan contains, BUKANfirst( where: ) != nil


Memverifikasi bahwa suatu objek ada dalam koleksi Collectiondapat dilakukan dengan banyak cara. Performa terbaik disediakan oleh fungsi contains.

KODE YANG BENAR

 let numbers = [0, 1, 2, 3] numbers.contains(1) 

KODE YANG TIDAK BENAR

 let numbers = [0, 1, 2, 3] numbers.filter { number in number == 1 }.isEmpty == false numbers.first(where: { number in number == 1 }) != nil 

2. Gunakan validasi isEmpty, BUKAN perbandingan countdengan nol


Karena untuk beberapa koleksi, akses ke properti countdilakukan dengan mengulangi semua elemen koleksi.

KODE YANG BENAR

 let numbers = [] numbers.isEmpty 

KODE YANG TIDAK BENAR

 let numbers = [] numbers.count == 0 

3. Periksa string kosong StringdenganisEmpty


String Stringin Swiftadalah kumpulan karakter [Character]. Ini berarti bahwa untuk string Stringjuga lebih baik digunakan isEmpty.

KODE YANG BENAR

 myString.isEmpty 

KODE YANG TIDAK BENAR

 myString == "" myString.count == 0 

4. Memperoleh elemen pertama yang memenuhi kondisi tertentu


Iterasi seluruh koleksi untuk mendapatkan objek pertama yang memenuhi kondisi tertentu dapat dilakukan dengan menggunakan metode yang filterdiikuti oleh suatu metode first, tetapi metode ini adalah yang terbaik dalam hal kecepatan first (where:). Metode ini berhenti mengulangi koleksi segera setelah memenuhi kondisi yang diperlukan. Metode ini filterakan terus beralih ke seluruh koleksi, terlepas dari apakah memenuhi elemen yang diperlukan atau tidak.

Jelas, hal yang sama berlaku untuk metode ini last (where:).

KODE YANG BENAR

 let numbers = [3, 7, 4, -2, 9, -6, 10, 1] let firstNegative = numbers.first(where: { $0 < 0 }) 

KODE YANG TIDAK BENAR

 let numbers = [0, 2, 4, 6] let allEven = numbers.filter { $0 % 2 != 0 }.isEmpty 

Terkadang, ketika koleksinya Collectionsangat besar dan kinerjanya sangat penting bagi Anda, ada baiknya kembali untuk membandingkan pendekatan imperatif dan fungsional dan memilih yang cocok untuk Anda.

TUGAS 2


Ada contoh hebat lain tentang penggunaan fungsi urutan tinggi yang sangat singkat reduce (_:, _:)yang saya temui. Ini adalah game SET .
Inilah aturan dasarnya. Nama permainan SETberasal dari kata bahasa Inggris "set" - "set". Permainan ini SETmelibatkan 81 kartu, masing-masing dengan gambar unik:



Setiap kartu memiliki 4 atribut, tercantum di bawah ini:

Jumlah : setiap kartu memiliki satu, dua atau tiga karakter.
Jenis karakter : oval, belah ketupat atau gelombang.
Warna : Simbol bisa merah, hijau atau ungu.
Mengisi : karakter dapat dikosongkan, diarsir, atau diarsir.

Tujuan permainanSET: Di antara 12 kartu yang diletakkan di atas meja, Anda perlu menemukan SET(satu set) yang terdiri dari 3 kartu, di mana masing-masing dari tanda-tanda itu benar-benar bertepatan atau sepenuhnya berbeda pada ketiga kartu. Semua tanda harus sepenuhnya mematuhi aturan ini.

Misalnya, jumlah karakter pada ketiga kartu harus sama atau berbeda, warna pada ketiga kartu harus sama atau berbeda, dan seterusnya ...

Dalam contoh ini, kita hanya akan tertarik pada Model Peta SET struct SetCarddan algoritma untuk menentukan SETdengan Peta ke-3 isSet( cards:[SetCard]):

 struct SetCard: Equatable { let number: Variant // number - 1, 2, 3 let color: Variant // color - 1, 2, 3 (, , , ) let shape: Variant // symbol - 1, 2, 3 (, , , ) let fill: Variant // fill - 1, 2, 3 (, , , ) enum Variant: Int, CaseIterable { case v1 = 1 case v2 case v3 } static func isSet(cards: [SetCard]) -> Bool { guard cards.count == 3 else {return false} let sums = [ cards.reduce(0, { $0 + $1.number.rawValue }), cards.reduce(0, { $0 + $1.color.rawValue }), cards.reduce(0, { $0 + $1.shape.rawValue }), cards.reduce(0, { $0 + $1.fill.rawValue }) ] return sums.reduce(true, { $0 && ($1 % 3 == 0) }) } } 

Model setiap fitur - jumlah number , jenis simbol shape , warna color dan Filling fill - disajikan listing Variantmemiliki tiga nilai yang mungkin var1, var2dan var3yang sesuai dengan bilangan bulat 3 rawValue- 1,2,3. Dalam bentuk ini, rawValuemudah dioperasikan. Jika kita mengambil beberapa tanda, misalnya, colorkemudian menambahkan semuanya rawValueuntuk colors3 kartu, kita akan menemukan bahwa jika colorssemua 3 kartu sama, maka jumlahnya akan sama 3, 6atau 9, jika semuanya akan berbeda, maka jumlahnya akan menjadi sama 6. Dalam salah satu dari kasus ini, kami memiliki kelipatan dari jumlah ketiga rawValueuntukcolorssemua 3 kartu. Kita tahu bahwa ini adalah prasyarat untuk membuat 3 kartu SET. Agar 3 kartu menjadi benar-benar SETdiperlukan, untuk semua tanda SetCard- Jumlah number, Jenis simbol shape, Warna colordan Mengisi fill- jumlahnya harus rawValuekelipatan ke-3.

Oleh karena itu, dalam staticmetode, isSet( cards:[SetCard])pertama kita menghitung array sumsdari jumlah rawValueuntuk semua 3 peta untuk semua peta 4 kinerja menggunakan fungsi yang lebih tinggi reducedengan nilai awal sama untuk 0, dan mengumpulkan fungsi {$0 + $1.number.rawValue}, {$0 + $1.color.rawValue}, {$0 + $1.shape.rawValue}, { {$0 + $1.fill.rawValue}. Setiap elemen array sumsharus merupakan kelipatan ke-3, dan sekali lagi kita menggunakan fungsinyareduce, tapi kali ini dengan nilai awal sama dengan truedan mengakumulasikan fungsi logis " AND" {$0 && ($1 % 3) == 0}. Dalam Swift 5, untuk menguji multiplisitas dari satu angka ke yang lain, sebuah fungsi diperkenalkan isMultiply(of:)sebagai ganti operator yang %tersisa. Hal ini juga akan meningkatkan pembacaan kode: { $0 && ($1.isMultiply(of:3) }.

Kode pendek yang fantastis ini untuk mengetahui apakah 3 SetCardkartu adalah kartu yang SETdiperoleh berkat pendekatan " fungsional ", dan kita dapat memastikan bahwa itu berhasil Playground:



Cara SETmembangun antarmuka pengguna ( UI) pada Model Game ini di sini , di sini dan di sini .

Fitur murni dan efek samping


Fungsi murni memenuhi dua kondisi. Itu selalu mengembalikan hasil yang sama dengan parameter input yang sama. Dan perhitungan hasilnya tidak menyebabkan efek samping yang terkait dengan output data di luar (misalnya, ke disk) atau dengan meminjam data sumber dari luar (misalnya, waktu). Ini memungkinkan Anda untuk mengoptimalkan kode secara signifikan.

Topik ini Swiftdiatur dengan sempurna pada point.free dalam episode pertama " Fungsi " dan " Efek Samping " , yang diterjemahkan ke dalam bahasa Rusia dan disajikan sebagai " Fungsi " dan "Efek Samping . "

Komposisi fungsi


Dalam pengertian matematika, ini berarti menerapkan satu fungsi ke hasil fungsi lainnya. Dalam suatu Swiftfungsi, mereka bisa mengembalikan nilai yang bisa Anda gunakan sebagai input untuk fungsi lain. Ini adalah praktik pemrograman yang umum.

Bayangkan bahwa kita memiliki array bilangan bulat dan kami ingin mendapatkan array kuadrat dari angka genap unik pada output. Biasanya kami menerapkan kembali ini sebagai berikut:

 var integerArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 4, 5] func unique(_ array: [Int]) -> [Int] { return array.reduce(into: [], { (results, element) in if !results.contains(element) { results.append(element) } }) } func even(_ array: [Int]) -> [Int] { return array.filter{ $0%2 == 0} } func square(_ array: [Int]) -> [Int] { return array.map{ $0*$0 } } var array = square(even(unique(integerArray))) // it returns [4, 16, 36, 64] 

Kode ini memberi kami hasil yang benar, tetapi Anda melihat bahwa keterbacaan baris kode terakhir tidak begitu mudah. Urutan fungsi (dari kanan ke kiri) adalah kebalikan dari yang kita terbiasa (dari kiri ke kanan) dan ingin melihat di sini. Kita perlu mengarahkan logika kita terlebih dahulu ke bagian terdalam dari beberapa embeddings - ke array inegerArray, kemudian ke fungsi di luar array ini unique, kemudian kita naik satu level lagi - fungsi even, dan akhirnya, fungsi dalam kesimpulan square.

Dan di sini "komposisi" fungsi >>>dan operator datang untuk membantu kami |>, yang memungkinkan kami untuk menulis kode dengan cara yang sangat nyaman, mewakili pemrosesan array asli integerArraysebagai "konveyor" fungsi:

 var array1 = integerArray |> unique >>> even >>> square 

Hampir semua bahasa seperti pemrograman fungsional khusus F#, Elixirdan Elmmenggunakan operator ini untuk "komposisi" fungsi.

Tidak Swiftada operator built-in dari "komposisi" fungsi >>>dan |>, tetapi kita dapat dengan mudah mendapatkannya dengan bantuan Genericspenutupan ( closure) dan infixoperator:

 precedencegroup ForwardComposition{ associativity: left higherThan: ForwardApplication } infix operator >>> : ForwardComposition func >>> <A, B, C>(left: @escaping (A) -> B, right: @escaping (B) -> C) -> (A) -> C { return { right(left($0)) } } precedencegroup ForwardApplication { associativity: left } infix operator |> : ForwardApplication func |> <A, B>(a: A, f: (A) -> B) -> B { return f(a) } 

Meskipun ada biaya tambahan, dalam beberapa kasus ini dapat secara signifikan meningkatkan kinerja, keterbacaan dan pengujian kode Anda. Misalnya, ketika di dalam mapAnda menempatkan seluruh rangkaian fungsi menggunakan operator "komposisi" >>>alih-alih mengejar array melalui banyak map:

 var integerArray1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 4, 5] let b = integerArray1.map( { $0 + 1 } >>> { $0 * 3 } >>> String.init) print (b) // ["6", "9", "12", "15", "18", "21", "24", "27", "30", "6", "15", "18"] 

Namun tidak selalu pendekatan fungsional memberikan efek positif.

Pada awalnya, ketika muncul Swiftpada tahun 2014, semua orang bergegas untuk menulis perpustakaan dengan operator untuk "komposisi" fungsi dan menyelesaikan tugas yang sulit untuk waktu itu seperti parsing JSONmenggunakan operator pemrograman fungsional alih-alih menggunakan konstruksi bersarang tak terhingga if let. Saya sendiri menerjemahkan artikel tentang parsing fungsional JSON yang menyenangkan saya dengan solusi yang elegan dan adalah penggemar perpustakaan Argo .

Tetapi para pengembang Swiftpergi dengan cara yang sama sekali berbeda dan mengusulkan, berdasarkan teknologi berorientasi protokol, cara penulisan kode yang jauh lebih ringkas. Untuk "mengirimkan" JSONdata langsung keCukup untuk melakukan hal ini Codable, yang secara otomatis mengimplementasikan protokol ini, jika model Anda terdiri dari dikenal Swiftstruktur data: String, Int, URL, Array, Dictionary, dll

 struct Blog: Codable { let id: Int let name: String let url: URL } 

Memiliki JSONdata dari artikel terkenal itu ...
 [ { "id" : 73, "name" : "Bloxus test", "url" : "http://remote.bloxus.com/" }, { "id" : 74, "name" : "Manila Test", "url" : "http://flickrtest1.userland.com/" } ] 

... saat ini Anda hanya perlu satu baris kode untuk mendapatkan berbagai blog blogs:

 let blogs = Bundle.main.path(forResource: "blogs", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode([Blog].self, from: $0) } print ("\(blogs!)") // [id: 73 name: Bloxus test url: http://remote.bloxus.com/, // id: 74 name: Manila Test url: http://flickrtest1.userland.com/] 

Setiap orang dengan aman lupa tentang menggunakan operator dari "komposisi" fungsi untuk parsing JSON, jika ada cara lain, lebih mudah dimengerti dan mudah untuk melakukan ini menggunakan protokol.

Jika semuanya begitu mudah, maka kita dapat "mengunggah" JSONdata ke Model yang lebih kompleks. Misalkan kita memiliki file JSONdata yang memiliki nama user.jsondan terletak di direktori kita Resources.. Ini berisi data tentang pengguna tertentu:

 { "email": "blob@pointfree.co", "id": 42, "name": "Blob" } 

Dan kami memiliki Codable pengguna Userdengan inisialisasi dari data json:

 struct User: Codable { let email: String let id: Int let name: String init?(json: Data) { if let newValue = try? JSONDecoder().decode(User.self, from: json) { self = newValue } else { return nil } } } 

Kami dapat dengan mudah mendapatkan pengguna baru newUserdengan kode fungsional yang bahkan lebih sederhana:

 let newUser = Bundle.main.path(forResource: "user", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { User.init(json: $0) } 

Jelas, newUserakan ada JENIS Optional, yaitu User?:



Misalkan dalam direktori kami Resourcesada file lain dengan nama invoices.jsondan berisi data pada faktur pengguna ini.

 [ { "amountPaid": 1000, "amountDue": 0, "closed": true, "id": 1 }, { "amountPaid": 500, "amountDue": 500, "closed": false, "id": 2 } ] 

Kami dapat memuat data ini persis seperti yang kami lakukan dengan User. Mari mendefinisikan struktur sebagai Model Faktur struct Invoice...

 struct Invoice: Codable { let amountDue: Int let amountPaid: Int let closed: Bool let id: Int } 

... dan decode JSONarray faktur yang disajikan di atas invoices, hanya mengubah jalur file dan logika decoding decode:

 let invoices = Bundle.main.path(forResource: "invoices", ofType: "json") .map( URL.init(fileURLWithPath:) ) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode([Invoice].self, from: $0) } 

invoicesakan menjadi [Invoice]?:



Sekarang kami ingin menghubungkan pengguna userbersama dengan fakturnya invoices, jika faktanya tidak sama nil, dan menyimpan, misalnya, dalam struktur amplop UserEnvelopeyang dikirim ke pengguna bersama dengan fakturnya:

 struct UserEnvelope { let user: User let invoices: [Invoice] } 

Alih-alih melakukan dua kali if let...

 if let newUser = newUser, let invoices = invoices { } 

... mari kita tulis analog fungsional ganda if letsebagai Genericfungsi tambahan zipyang mengubah dua Optionalnilai menjadi sebuah Optionaltuple:

 func zip<A, B>(_ a: A?, _ b: B?) -> (A, B)? { if let a = a, let b = b { return (a, b) } return nil } 

Sekarang kita tidak punya alasan untuk menetapkan sesuatu ke variabel newUserdan invoices, kita hanya membangun semuanya ke dalam fungsi baru kita zip, menggunakan penginisialisasi UserEnvelope.initdan semuanya akan bekerja!

 let userEnv = zip( Bundle.main.path(forResource: "user", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { User.init(json: $0) }, Bundle.main.path(forResource: "invoices", ofType: "json") .map(URL.init(fileURLWithPath:)) .flatMap { try? Data.init(contentsOf: $0) } .flatMap { try? JSONDecoder().decode([Invoice].self, from: $0) } ).flatMap (UserEnvelope.init) print ("\(userEnv!)") // UserEnvelope(user: id: 42 name: Blob , // invoices: [id: 1 amountDue: 0 amountPaid: 1000 closed: true, // id: 2 amountDue: 500 amountPaid: 500 closed: false]) 

Dalam satu ekspresi tunggal, seluruh algoritme untuk mengirimkan JSONdata ke data yang kompleks dalam bentuk struktur dikemas struct UserEnvelope.

  • zip , , . user , JSON , invoices , JSON . .
  • map , , «» .
  • flatMap , , , .

Operasi zip, mapdan flatMapmewakili semacam bahasa khusus domain (DSL) untuk konversi data.

Kami selanjutnya dapat mengembangkan demo ini untuk mewakili secara asinkron membaca konten file sebagai fungsi khusus yang dapat Anda lihat di pointfree.co .

Saya bukan penggemar fanatik Pemrograman Fungsional di mana-mana dan dalam segala hal, tetapi penggunaan moderat menurut saya disarankan.

Kesimpulan


Saya memberi contoh berbagai pemrograman fungsional fitur Swft «keluar dari kotak", berdasarkan pada penggunaan fungsi tingkat tinggi map, flatMap, reduce, filterdan yang lainnya untuk urutan Sequence, Optionaldan Result. Mereka bisa menjadi "workhorses" dari pembuatan kode, ,terutama jika nilai - struktur structdan enumerasi terlibat di sana enum. Pengembang iOSaplikasi harus memiliki alat ini.

Semua demo yang dikompilasi Playgrounddapat ditemukan di Github . Jika Anda memiliki masalah dengan peluncurannya Playground, Anda dapat melihat artikel ini:

Bagaimana cara menyingkirkan kesalahan "beku" Xcode Playground dengan pesan "Launching Simulator" dan "Running Playground".

Referensi:

Functional Programming in Swift: An Introduction.
An Introduction to Functional Programming in Swift.
The Many Faces of Flat-Map: Part 3
Inside the Standard Library: Sequence.map()
Practical functional programming in Swift

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


All Articles