รหัส Android ของคุณพิสูจน์อนาคต ส่วนที่ 1: พื้นฐานการเขียนโปรแกรมเชิงหน้าที่และเชิงโต้ตอบ
เผยแพร่แล้ว: 2022-08-31การเขียนโค้ดที่สะอาดอาจเป็นสิ่งที่ท้าทาย: ไลบรารี เฟรมเวิร์ก และ API เป็นแบบชั่วคราวและล้าสมัยอย่างรวดเร็ว แต่แนวคิดและกระบวนทัศน์ทางคณิตศาสตร์นั้นยั่งยืน พวกเขาต้องการการวิจัยทางวิชาการหลายปีและอาจอยู่ได้นานกว่าเรา
นี่ไม่ใช่บทช่วยสอนที่จะแสดงให้คุณเห็นถึงวิธีการทำ X ด้วย Library Y แต่เรามุ่งเน้นไปที่หลักการที่ยั่งยืนซึ่งอยู่เบื้องหลังการเขียนโปรแกรมเชิงฟังก์ชันและเชิงโต้ตอบ เพื่อให้คุณสามารถสร้างสถาปัตยกรรม Android ที่พิสูจน์ได้ในอนาคตและเชื่อถือได้ ตลอดจนปรับขนาดและปรับให้เข้ากับการเปลี่ยนแปลงโดยไม่กระทบต่อ ประสิทธิภาพ.
บทความนี้เป็นการวางรากฐาน และในตอนที่ 2 เราจะเจาะลึกการใช้งานโปรแกรม functional reactive programming (FRP) ซึ่งรวมทั้งการเขียนโปรแกรมเชิงฟังก์ชันและเชิงโต้ตอบ
บทความนี้เขียนขึ้นโดยคำนึงถึงนักพัฒนา Android แต่แนวคิดมีความเกี่ยวข้องและเป็นประโยชน์ต่อนักพัฒนาที่มีประสบการณ์ในภาษาโปรแกรมทั่วไป
ฟังก์ชั่นการเขียนโปรแกรม101
Functional Programming (FP) คือรูปแบบที่คุณสร้างโปรแกรมของคุณเป็นองค์ประกอบของฟังก์ชัน แปลงข้อมูลจาก $A$ เป็น $B$ เป็น $C$ ฯลฯ จนกว่าจะได้ผลลัพธ์ที่ต้องการ ในการเขียนโปรแกรมเชิงวัตถุ (OOP) คุณบอกคอมพิวเตอร์ว่าต้องทำอย่างไรตามคำสั่ง การเขียนโปรแกรมเชิงฟังก์ชันแตกต่างกัน: คุณยกเลิกขั้นตอนการควบคุมและกำหนด "สูตรของฟังก์ชัน" เพื่อสร้างผลลัพธ์แทน
FP มีต้นกำเนิดมาจากคณิตศาสตร์ โดยเฉพาะแคลคูลัสแลมบ์ดา ซึ่งเป็นระบบตรรกะของฟังก์ชันนามธรรม แทนที่จะใช้แนวคิด OOP เช่น ลูป คลาส ความหลากหลาย หรือการสืบทอด FP จะจัดการกับฟังก์ชันนามธรรมและลำดับที่สูงกว่า ฟังก์ชันทางคณิตศาสตร์ที่รับฟังก์ชันอื่นเป็นอินพุตอย่างเคร่งครัด
โดยสรุป FP มี "ผู้เล่น" หลักสองราย: ข้อมูล (แบบจำลองหรือข้อมูลที่จำเป็นสำหรับปัญหาของคุณ) และฟังก์ชัน (การแสดงพฤติกรรมและการแปลงระหว่างข้อมูล) ในทางตรงกันข้าม คลาส OOP จะเชื่อมโยงโครงสร้างข้อมูลเฉพาะโดเมน และค่าหรือสถานะที่เกี่ยวข้องกับแต่ละอินสแตนซ์ของคลาสอย่างชัดเจน กับพฤติกรรม (เมธอด) ที่มีจุดประสงค์เพื่อใช้กับโครงสร้างดังกล่าว
เราจะตรวจสอบประเด็นสำคัญสามประการของ FP อย่างใกล้ชิดยิ่งขึ้น:
- FP เป็นการประกาศ
- FP ใช้องค์ประกอบฟังก์ชัน
- ฟังก์ชัน FP นั้นบริสุทธิ์
จุดเริ่มต้นที่ดีในการดำดิ่งสู่โลกของ FP ต่อไปคือ Haskell ซึ่งเป็นภาษาที่ใช้งานได้อย่างหมดจด ฉันขอแนะนำ Learn You a Haskell for Great Good! กวดวิชาแบบโต้ตอบเป็นทรัพยากรที่เป็นประโยชน์
FP Ingredient #1: Declarative Programming
สิ่งแรกที่คุณจะสังเกตได้เกี่ยวกับโปรแกรม FP คือมันถูกเขียนในลักษณะที่เปิดเผย ตรงข้ามกับรูปแบบความจำเป็น กล่าวโดยย่อ การเขียนโปรแกรมเชิงประกาศจะบอกโปรแกรมว่าต้องทำอะไร แทนที่จะต้องทำอย่างไร มาสร้างนิยามนามธรรมนี้ด้วยตัวอย่างที่เป็นรูปธรรมของการเขียนโปรแกรมความจำเป็นกับการประกาศเพื่อแก้ปัญหาต่อไปนี้: ระบุรายชื่อ ส่งคืนรายการที่มีเฉพาะชื่อที่มีสระอย่างน้อยสามสระ และสระที่แสดงด้วยอักษรตัวพิมพ์ใหญ่
โซลูชั่นที่จำเป็น
ขั้นแรก ให้ตรวจสอบวิธีแก้ปัญหาที่จำเป็นของปัญหานี้ใน Kotlin:
fun namesImperative(input: List<String>): List<String> { val result = mutableListOf<String>() val vowels = listOf('A', 'E', 'I', 'O', 'U','a', 'e', 'i', 'o', 'u') for (name in input) { // loop 1 var vowelsCount = 0 for (char in name) { // loop 2 if (isVowel(char, vowels)) { vowelsCount++ if (vowelsCount == 3) { val uppercaseName = StringBuilder() for (finalChar in name) { // loop 3 var transformedChar = finalChar // ignore that the first letter might be uppercase if (isVowel(finalChar, vowels)) { transformedChar = finalChar.uppercaseChar() } uppercaseName.append(transformedChar) } result.add(uppercaseName.toString()) break } } } } return result } fun isVowel(char: Char, vowels: List<Char>): Boolean { return vowels.contains(char) } fun main() { println(namesImperative(listOf("Iliyan", "Annabel", "Nicole", "John", "Anthony", "Ben", "Ken"))) // [IlIyAn, AnnAbEl, NIcOlE] }
ตอนนี้เราจะวิเคราะห์โซลูชันที่จำเป็นของเราโดยคำนึงถึงปัจจัยการพัฒนาที่สำคัญบางประการ:
มีประสิทธิภาพสูงสุด: โซลูชันนี้มีการใช้งานหน่วยความจำที่เหมาะสมและทำงานได้ดีในการวิเคราะห์ Big O (อิงจากจำนวนการเปรียบเทียบขั้นต่ำ) ในอัลกอริธึมนี้ การวิเคราะห์จำนวนการเปรียบเทียบระหว่างอักขระเป็นเรื่องที่สมเหตุสมผล เพราะนั่นคือการดำเนินการหลักในอัลกอริทึมของเรา ให้ $n$ เป็นจำนวนชื่อ และให้ $k$ เป็นความยาวเฉลี่ยของชื่อ
- จำนวนการเปรียบเทียบที่แย่ที่สุด: $n(10k)(10k) = 100nk^2$
- คำอธิบาย: $n$ (loop 1) * $10k$ (สำหรับอักขระแต่ละตัว เราเปรียบเทียบกับสระที่เป็นไปได้ 10 ตัว) * $10k$ (เราดำเนินการ
isVowel()
ตรวจสอบอีกครั้งเพื่อตัดสินใจว่าจะตัวพิมพ์ใหญ่อักขระ—อีกครั้ง ใน กรณีที่แย่ที่สุด เทียบได้กับสระ 10 ตัว) - ผลลัพธ์: เนื่องจากความยาวเฉลี่ยของชื่อจะไม่เกิน 100 อักขระ เราสามารถพูดได้ว่าอัลกอริทึมของเราทำงานในเวลา $O(n)$
- ซับซ้อนและอ่านง่าย: เมื่อเทียบกับโซลูชันที่เปิดเผย เราจะพิจารณาต่อไป โซลูชันนี้ใช้เวลานานกว่าและติดตามได้ยากกว่ามาก
- เกิดข้อผิดพลาดได้ง่าย: รหัสเปลี่ยน
result
,vowelsCount
และtransformedChar
; การกลายพันธุ์ของสถานะเหล่านี้สามารถนำไปสู่ข้อผิดพลาดเล็กน้อย เช่น การลืมรีเซ็ตvowelsCount
กลับเป็น 0 โฟลว์ของการดำเนินการอาจซับซ้อนเช่นกัน และเป็นการง่ายที่จะลืมเพิ่มคำสั่งbreak
ในลูปที่สาม - การบำรุงรักษาไม่ดี: เนื่องจากโค้ดของเราซับซ้อนและมีแนวโน้มที่จะเกิดข้อผิดพลาด การปรับโครงสร้างใหม่หรือการเปลี่ยนแปลงการทำงานของโค้ดนี้อาจเป็นเรื่องยาก ตัวอย่างเช่น หากปัญหาได้รับการแก้ไขเพื่อเลือกชื่อที่มีสระสามตัวและพยัญชนะห้าตัว เราจะต้องแนะนำตัวแปรใหม่และเปลี่ยนลูป ซึ่งทำให้มีโอกาสเกิดข้อผิดพลาดมากมาย
ตัวอย่างโซลูชันของเราแสดงให้เห็นว่าโค้ดจำเป็นที่ซับซ้อนอาจดูซับซ้อนเพียงใด แม้ว่าคุณจะสามารถปรับปรุงโค้ดได้โดยการจัดโครงสร้างใหม่ให้เป็นฟังก์ชันที่เล็กกว่า
โซลูชันการประกาศ
ตอนนี้เราเข้าใจแล้วว่าการเขียนโปรแกรมเชิงประกาศ คืออะไร เรามาเปิดตัวโซลูชันการประกาศของเราใน Kotlin กัน:
fun namesDeclarative(input: List<String>): List<String> = input.filter { name -> name.count(::isVowel) >= 3 }.map { name -> name.map { char -> if (isVowel(char)) char.uppercaseChar() else char }.joinToString("") } fun isVowel(char: Char): Boolean = listOf('A', 'E', 'I', 'O', 'U', 'a', 'e', 'i', 'o', 'u').contains(char) fun main() { println(namesDeclarative(listOf("Iliyan", "Annabel", "Nicole", "John", "Anthony", "Ben", "Ken"))) // [IlIyAn, AnnAbEl, NIcOlE] }
โดยใช้เกณฑ์เดียวกันกับที่เราใช้ในการประเมินโซลูชันที่จำเป็นของเรา มาดูกันว่าโค้ดที่ประกาศมีขึ้นอย่างไร:
- มีประสิทธิภาพ: การใช้งานที่จำเป็นและการประกาศทั้งสองทำงานในเวลาเชิงเส้น แต่สิ่งที่จำเป็นนั้นมีประสิทธิภาพมากกว่าเล็กน้อยเพราะฉันใช้
name.count()
ที่นี่ ซึ่งจะนับสระต่อไปจนกว่าชื่อจะสิ้นสุด (แม้หลังจากค้นหาสระสามสระแล้ว ). เราสามารถแก้ไขปัญหานี้ได้โดยการเขียนhasThreeVowels(String): Boolean
function อย่างง่าย โซลูชันนี้ใช้อัลกอริธึมเดียวกับโซลูชันที่จำเป็น ดังนั้นจึงใช้การวิเคราะห์ความซับซ้อนแบบเดียวกันที่นี่: อัลกอริทึมของเราทำงานในเวลา $O(n)$ - กระชับและอ่านง่าย: โซลูชันที่จำเป็นคือ 44 บรรทัดที่มีการเยื้องขนาดใหญ่ เมื่อเทียบกับความยาว 16 บรรทัดของโซลูชันที่ประกาศโดยมีการเยื้องเล็กน้อย เส้นและแท็บไม่ใช่ทุกอย่าง แต่เห็นได้ชัดจากภาพรวมของทั้งสองไฟล์ว่าโซลูชันการประกาศของเราสามารถอ่านได้ง่ายกว่ามาก
- มีโอกาสเกิดข้อผิดพลาดน้อยกว่า: ในตัวอย่างนี้ ทุกอย่างไม่เปลี่ยนรูปแบบ เราแปลง
List<String>
ของชื่อทั้งหมดเป็นList<String>
ของชื่อที่มีสระตั้งแต่สามตัวขึ้นไป จากนั้นแปลงคำString
แต่ละคำให้เป็นคำString
ที่มีสระตัวพิมพ์ใหญ่ โดยรวมแล้ว การไม่มีการกลายพันธุ์ การวนซ้ำซ้อน หรือการแบ่งและการยกเลิกโฟลว์การควบคุมทำให้โค้ดง่ายขึ้นโดยมีพื้นที่สำหรับข้อผิดพลาดน้อยลง - การบำรุงรักษาที่ดี: คุณสามารถปรับโครงสร้างโค้ดที่ประกาศใหม่ได้อย่างง่ายดายเนื่องจากสามารถอ่านได้และทนทาน ในตัวอย่างก่อนหน้านี้ของเรา (สมมติว่าปัญหา ได้รับ การแก้ไขเพื่อเลือกชื่อที่มีสระสามตัวและพยัญชนะห้าตัว) วิธีแก้ปัญหาง่ายๆ คือการเพิ่มข้อความสั่งต่อไปนี้ในเงื่อนไข
filter
:val vowels = name.count(::isVowel); vowels >= 3 && name.length - vowels >= 5
val vowels = name.count(::isVowel); vowels >= 3 && name.length - vowels >= 5
.
ในแง่บวกเพิ่มเติม โซลูชันการประกาศของเราใช้งานได้จริง: แต่ละฟังก์ชันในตัวอย่างนี้บริสุทธิ์และไม่มีผลข้างเคียง (เพิ่มเติมเกี่ยวกับความบริสุทธิ์ในภายหลัง)
โซลูชันการประกาศโบนัส
มาดูการนำปัญหาเดียวกันไปใช้อย่างเปิดเผยในภาษาที่ใช้งานได้จริง เช่น Haskell เพื่อสาธิตวิธีการอ่าน หากคุณไม่คุ้นเคยกับ Haskell โปรดทราบว่าไฟล์ .
โอเปอเรเตอร์ใน Haskell อ่านว่า "หลัง" ตัวอย่างเช่น solution = map uppercaseVowels . filter hasThreeVowels
solution = map uppercaseVowels . filter hasThreeVowels
แปลว่า "สระแผนที่เป็นตัวพิมพ์ใหญ่หลังจากกรองชื่อที่มีสามสระ"
import Data.Char(toUpper) namesSolution :: [String] -> [String] namesSolution = map uppercaseVowels . filter hasThreeVowels hasThreeVowels :: String -> Bool hasThreeVowels s = count isVowel s >= 3 uppercaseVowels :: String -> String uppercaseVowels = map uppercaseVowel where uppercaseVowel :: Char -> Char uppercaseVowel c | isVowel c = toUpper c | otherwise = c isVowel :: Char -> Bool isVowel c = c `elem` vowels vowels :: [Char] vowels = ['A', 'E', 'I', 'O', 'U', 'a', 'e', 'i', 'o', 'u'] count :: (a -> Bool) -> [a] -> Int count _ [] = 0 count pred (x:xs) | pred x = 1 + count pred xs | otherwise = count pred xs main :: IO () main = print $ namesSolution ["Iliyan", "Annabel", "Nicole", "John", "Anthony", "Ben", "Ken"] -- ["IlIyAn","AnnAbEl","NIcOlE"]
โซลูชันนี้ทำงานคล้ายกับโซลูชันการประกาศของ Kotlin โดยมีประโยชน์เพิ่มเติมบางประการ: สามารถอ่านได้ ง่าย หากคุณเข้าใจไวยากรณ์ของ Haskell ใช้งานได้จริง และขี้เกียจ
ประเด็นที่สำคัญ
การเขียนโปรแกรมการประกาศมีประโยชน์สำหรับทั้ง FP และ Reactive Programming (ซึ่งเราจะกล่าวถึงในหัวข้อต่อไป)
- มันอธิบายถึง “อะไร” ที่คุณต้องการบรรลุ—แทนที่จะเป็น “วิธี” เพื่อให้บรรลุตามคำสั่งของการดำเนินการตามคำสั่งที่แน่นอน
- มันสรุปขั้นตอนการควบคุมของโปรแกรมและแทนที่จะมุ่งเน้นไปที่ปัญหาในแง่ของการแปลง (เช่น $A \rightarrow B \rightarrow C \rightarrow D$)
- สนับสนุนโค้ดที่ซับซ้อนน้อยกว่า รัดกุมกว่า และอ่านง่ายกว่า ซึ่งง่ายต่อการปรับโครงสร้างและเปลี่ยนแปลง หากโค้ด Android ของคุณไม่อ่านเหมือนประโยค แสดงว่าคุณอาจกำลังทำอะไรผิด
หากโค้ด Android ของคุณไม่อ่านเหมือนประโยค แสดงว่าคุณอาจกำลังทำอะไรผิด
ทวีต
ถึงกระนั้น การเขียนโปรแกรมเชิงประกาศก็มีข้อเสียบางประการ เป็นไปได้ที่จะจบลงด้วยโค้ดที่ไม่มีประสิทธิภาพซึ่งใช้ RAM มากกว่าและทำงานได้แย่กว่าการใช้งานที่จำเป็น การเรียงลำดับ การเผยแพร่ย้อนหลัง (ในแมชชีนเลิร์นนิง) และ "อัลกอริทึมการกลายพันธุ์" อื่นๆ ไม่เหมาะสำหรับรูปแบบการเขียนโปรแกรมที่ไม่เปลี่ยนรูปแบบและประกาศ
FP Ingredient #2: องค์ประกอบของฟังก์ชัน
องค์ประกอบของฟังก์ชันเป็นแนวคิดทางคณิตศาสตร์ที่เป็นหัวใจของการเขียนโปรแกรมเชิงฟังก์ชัน หากฟังก์ชั่น $f$ ยอมรับ $A$ เป็นอินพุตและสร้าง $B$ เป็นเอาต์พุต ($f: A \rightarrow B$) และฟังก์ชัน $g$ ยอมรับ $B$ และสร้าง $C$ ($g: B \rightarrow C$) จากนั้นคุณสามารถสร้างฟังก์ชันที่สาม $h$ ที่ยอมรับ $A$ และสร้าง $C$ ($h: A \rightarrow C$) เราสามารถกำหนดฟังก์ชันที่สามนี้เป็น องค์ประกอบ ของ $g$ กับ $f$ และยังระบุเป็น $g \circ f$ หรือ $g(f())$:
โซลูชันที่จำเป็นทุกวิธีสามารถแปลเป็นโซลูชันที่เปิดเผยได้โดยการแยกปัญหาออกเป็นปัญหาที่เล็กกว่า แก้ปัญหาอย่างอิสระ และจัดองค์ประกอบโซลูชันที่มีขนาดเล็กลงในโซลูชันสุดท้ายผ่านองค์ประกอบของฟังก์ชัน ลองดูปัญหาชื่อของเราจากส่วนก่อนหน้าเพื่อดูแนวคิดนี้ในการดำเนินการ ปัญหาเล็ก ๆ ของเราจากการแก้ปัญหาที่จำเป็นคือ:
-
isVowel :: Char -> Bool
: ให้Char
ให้ส่งคืนไม่ว่าจะเป็นสระหรือไม่ (Bool
) -
countVowels :: String -> Int
: ให้String
ส่งคืนจำนวนสระในนั้น (Int
) -
hasThreeVowels :: String -> Bool
: Given aString
ให้คืนค่าว่ามีสระอย่างน้อยสามตัว (Bool
) -
uppercaseVowels :: String -> String
: ให้String
ส่งคืนString
ใหม่ด้วยสระตัวพิมพ์ใหญ่
โซลูชันการประกาศของเรา ซึ่งทำได้โดยองค์ประกอบของฟังก์ชัน คือ map uppercaseVowels . filter hasThreeVowels
map uppercaseVowels . filter hasThreeVowels
ตัวอย่างนี้ซับซ้อนกว่าสูตร $A \rightarrow B \rightarrow C$ เล็กน้อย แต่แสดงให้เห็นถึงหลักการเบื้องหลังองค์ประกอบของฟังก์ชัน
ประเด็นที่สำคัญ
องค์ประกอบของฟังก์ชันเป็นแนวคิดที่เรียบง่ายแต่ทรงพลัง
- มีกลยุทธ์ในการแก้ปัญหาที่ซับซ้อน โดยแยกปัญหาออกเป็นขั้นตอนที่เล็กกว่า ง่ายกว่า และรวมเป็นโซลูชันเดียว
- มันมีบล็อคส่วนประกอบ ช่วยให้คุณเพิ่ม ลบ หรือเปลี่ยนส่วนต่าง ๆ ของโซลูชันขั้นสุดท้ายได้อย่างง่ายดายโดยไม่ต้องกังวลว่าจะมีอะไรแตกหัก
- คุณสามารถเขียน $g(f())$ หากผลลัพธ์ของ $f$ ตรงกับประเภทอินพุตของ $g$
เมื่อเขียนฟังก์ชัน คุณสามารถส่งผ่านข้อมูลได้ไม่เพียงเท่านั้น แต่ยังทำหน้าที่เป็นอินพุตไปยังฟังก์ชันอื่นๆ ด้วย ซึ่งเป็นตัวอย่างของฟังก์ชันที่มีลำดับสูงกว่า
FP Ingredient #3: ความบริสุทธิ์
มีองค์ประกอบสำคัญอีกหนึ่งองค์ประกอบในองค์ประกอบของฟังก์ชันที่เราต้องกล่าวถึง: หน้าที่ที่คุณเขียนต้อง บริสุทธิ์ แนวคิดอื่นที่ได้มาจากคณิตศาสตร์ ในวิชาคณิตศาสตร์ ฟังก์ชันทั้งหมดเป็นการคำนวณที่ให้ผลลัพธ์เหมือนกันเสมอเมื่อถูกเรียกด้วยอินพุตเดียวกัน นี่คือพื้นฐานของความบริสุทธิ์
มาดูตัวอย่าง pseudocode โดยใช้ฟังก์ชันทางคณิตศาสตร์กัน สมมติว่าเรามีฟังก์ชัน makeEven
ที่เพิ่มอินพุตจำนวนเต็มเป็นสองเท่าเพื่อให้เป็นเลขคู่ และโค้ดของเรารันบรรทัด makeEven(x) + x
โดยใช้อินพุต x = 2
ในวิชาคณิตศาสตร์ การคำนวณนี้จะแปลเป็นการคำนวณ $2x + x = 3x = 3(2) = 6$ เสมอ และเป็นฟังก์ชันล้วนๆ อย่างไรก็ตาม สิ่งนี้ไม่เป็นความจริงเสมอไปในการเขียนโปรแกรม—หากฟังก์ชัน makeEven(x)
กลายพันธุ์ x
โดยเพิ่มเป็นสองเท่าก่อนที่โค้ดจะส่งคืนผลลัพธ์ของเรา บรรทัดของเราจะคำนวณ $2x + (2x) = 4x = 4(2) = 8$ และที่แย่ไปกว่านั้น ผลลัพธ์จะเปลี่ยนไปตามการโทรแต่ละ makeEven
มาสำรวจประเภทของฟังก์ชันที่ไม่บริสุทธิ์กัน แต่จะช่วยให้เรากำหนดความบริสุทธิ์ให้เจาะจงมากขึ้น:
- ฟังก์ชันบางส่วน: ฟังก์ชัน เหล่านี้เป็นฟังก์ชันที่ไม่ได้กำหนดไว้สำหรับค่าอินพุตทั้งหมด เช่น การหาร จากมุมมองของการเขียนโปรแกรม ฟังก์ชันเหล่านี้เป็นฟังก์ชันที่มีข้อยกเว้น:
fun divide(a: Int, b: Int): Float
จะส่งArithmeticException
สำหรับอินพุตb = 0
ที่เกิดจากการหารด้วยศูนย์ - ฟังก์ชันทั้งหมด: ฟังก์ชัน เหล่านี้ถูกกำหนดไว้สำหรับค่าอินพุตทั้งหมด แต่สามารถสร้างเอาต์พุตหรือผลข้างเคียงที่แตกต่างกันได้เมื่อถูกเรียกด้วยอินพุตเดียวกัน โลกของ Android เต็มไปด้วยฟังก์ชันทั้งหมด:
Log.d
,LocalDateTime.now
และLocale.getDefault
เป็นเพียงตัวอย่างบางส่วนเท่านั้น
เมื่อคำนึงถึงคำจำกัดความเหล่านี้ เราสามารถกำหนด ฟังก์ชันบริสุทธิ์เป็นฟังก์ชัน ทั้งหมดโดยไม่มีผลข้างเคียง องค์ประกอบของฟังก์ชันที่สร้างขึ้นโดยใช้ฟังก์ชันบริสุทธิ์เท่านั้นจะสร้างโค้ดที่น่าเชื่อถือ คาดเดาได้ และสามารถทดสอบได้มากกว่า
เคล็ดลับ: ในการทำให้ฟังก์ชันทั้งหมดบริสุทธิ์ คุณสามารถสรุปผลข้างเคียงโดยส่งผ่านเป็นพารามิเตอร์ฟังก์ชันลำดับที่สูงกว่า ด้วยวิธีนี้ คุณสามารถทดสอบฟังก์ชันทั้งหมดได้อย่างง่ายดายโดยผ่านฟังก์ชันลำดับที่สูงกว่าที่จำลองขึ้น ตัวอย่างนี้ใช้คำอธิบายประกอบ @SideEffect
จากไลบรารีที่เราตรวจสอบในภายหลังในบทช่วยสอน Ivy FRP:
suspend fun deadlinePassed( deadline: LocalDate, @SideEffect currentDate: suspend () -> LocalDate ): Boolean = deadline.isAfter(currentDate())
ประเด็นที่สำคัญ
ความบริสุทธิ์เป็นส่วนประกอบสุดท้ายที่จำเป็นสำหรับกระบวนทัศน์การเขียนโปรแกรมเชิงฟังก์ชัน
- ระวังฟังก์ชันบางส่วน เพราะอาจทำให้แอปของคุณขัดข้องได้
- การเขียนฟังก์ชันทั้งหมดไม่ได้กำหนดไว้ มันสามารถสร้างพฤติกรรมที่คาดเดาไม่ได้
- เมื่อใดก็ตามที่เป็นไปได้ ให้เขียนฟังก์ชันบริสุทธิ์ คุณจะได้รับประโยชน์จากความเสถียรของโค้ดที่เพิ่มขึ้น
เมื่อภาพรวมของการเขียนโปรแกรมเชิงฟังก์ชันของเราเสร็จสมบูรณ์แล้ว เรามาตรวจสอบองค์ประกอบถัดไปของโค้ด Android ที่รองรับอนาคต: การเขียนโปรแกรมเชิงโต้ตอบ
การเขียนโปรแกรมเชิงโต้ตอบ101
การเขียนโปรแกรมเชิงโต้ตอบเป็นรูปแบบการเขียนโปรแกรมที่เปิดเผยซึ่งโปรแกรมตอบสนองต่อข้อมูลหรือการเปลี่ยนแปลงเหตุการณ์แทนการขอข้อมูลเกี่ยวกับการเปลี่ยนแปลง
องค์ประกอบพื้นฐานในวงจรการเขียนโปรแกรมเชิงโต้ตอบคือเหตุการณ์ ไปป์ไลน์ที่ประกาศ สถานะ และสิ่งที่สังเกตได้:
- เหตุการณ์ คือสัญญาณจากโลกภายนอก ซึ่งโดยทั่วไปจะอยู่ในรูปแบบของการป้อนข้อมูลของผู้ใช้หรือเหตุการณ์ของระบบ ที่ทริกเกอร์การอัปเดต จุดประสงค์ของเหตุการณ์คือการแปลงสัญญาณเป็นอินพุตไปป์ไลน์
- ไปป์ไลน์การประกาศ เป็นองค์ประกอบของฟังก์ชันที่ยอมรับ
(Event, State)
เป็นอินพุตและแปลงอินพุตนี้เป็นState
ใหม่ (เอาต์พุต):(Event, State) -> f -> g -> … -> n -> State
ไปป์ไลน์ต้องดำเนินการแบบอะซิงโครนัสเพื่อจัดการกับหลายเหตุการณ์โดยไม่ปิดกั้นไปป์ไลน์อื่นหรือรอให้เสร็จสิ้น - สถานะคือการแสดงรูปแบบข้อมูลของ แอ ปพลิเคชันซอฟต์แวร์ ณ เวลาที่กำหนด ตรรกะของโดเมนใช้สถานะเพื่อคำนวณสถานะถัดไปที่ต้องการและทำการอัปเดตที่เกี่ยวข้อง
- ผู้ สังเกตการณ์ รับฟังการเปลี่ยนแปลงสถานะและอัปเดตสมาชิกเกี่ยวกับการเปลี่ยนแปลงเหล่านั้น ใน Android สิ่งที่สังเกตได้มักจะถูกใช้งานโดยใช้
Flow
,LiveData
หรือRxJava
และแจ้งเตือน UI ของการอัปเดตสถานะเพื่อให้สามารถตอบสนองตามนั้น
มีคำจำกัดความและการใช้งานมากมายของการเขียนโปรแกรมเชิงโต้ตอบ ในที่นี้ ฉันได้ใช้แนวทางปฏิบัติที่เน้นการใช้แนวคิดเหล่านี้กับโครงการจริง
การเชื่อมต่อจุด: การเขียนโปรแกรมปฏิกิริยาเชิงหน้าที่
การเขียนโปรแกรมเชิงหน้าที่และเชิงโต้ตอบเป็นกระบวนทัศน์ที่ทรงพลังสองประการ แนวคิดเหล่านี้มีมากกว่าอายุสั้นของไลบรารีและ API และจะพัฒนาทักษะการเขียนโปรแกรมของคุณไปอีกหลายปี
นอกจากนี้ พลังของ FP และการเขียนโปรแกรมเชิงโต้ตอบยังทวีคูณเมื่อรวมกัน ตอนนี้เรามีคำจำกัดความที่ชัดเจนของการเขียนโปรแกรมเชิงฟังก์ชันและเชิงโต้ตอบแล้ว เราสามารถรวมส่วนต่างๆ เข้าด้วยกันได้ ในส่วนที่ 2 ของบทช่วยสอนนี้ เรากำหนดกระบวนทัศน์การเขียนโปรแกรมเชิงโต้ตอบเชิงฟังก์ชัน (FRP) และนำไปปฏิบัติด้วยตัวอย่างการใช้งานแอปและไลบรารี Android ที่เกี่ยวข้อง
บล็อก Toptal Engineering ขอขอบคุณ Tarun Goyal สำหรับการตรวจสอบตัวอย่างโค้ดที่นำเสนอในบทความนี้