รหัส Android ของคุณพิสูจน์อนาคต ส่วนที่ 1: พื้นฐานการเขียนโปรแกรมเชิงหน้าที่และเชิงโต้ตอบ

เผยแพร่แล้ว: 2022-08-31

การเขียนโค้ดที่สะอาดอาจเป็นสิ่งที่ท้าทาย: ไลบรารี เฟรมเวิร์ก และ API เป็นแบบชั่วคราวและล้าสมัยอย่างรวดเร็ว แต่แนวคิดและกระบวนทัศน์ทางคณิตศาสตร์นั้นยั่งยืน พวกเขาต้องการการวิจัยทางวิชาการหลายปีและอาจอยู่ได้นานกว่าเรา

นี่ไม่ใช่บทช่วยสอนที่จะแสดงให้คุณเห็นถึงวิธีการทำ X ด้วย Library Y แต่เรามุ่งเน้นไปที่หลักการที่ยั่งยืนซึ่งอยู่เบื้องหลังการเขียนโปรแกรมเชิงฟังก์ชันและเชิงโต้ตอบ เพื่อให้คุณสามารถสร้างสถาปัตยกรรม Android ที่พิสูจน์ได้ในอนาคตและเชื่อถือได้ ตลอดจนปรับขนาดและปรับให้เข้ากับการเปลี่ยนแปลงโดยไม่กระทบต่อ ประสิทธิภาพ.

บทความนี้เป็นการวางรากฐาน และในตอนที่ 2 เราจะเจาะลึกการใช้งานโปรแกรม functional reactive programming (FRP) ซึ่งรวมทั้งการเขียนโปรแกรมเชิงฟังก์ชันและเชิงโต้ตอบ

บทความนี้เขียนขึ้นโดยคำนึงถึงนักพัฒนา Android แต่แนวคิดมีความเกี่ยวข้องและเป็นประโยชน์ต่อนักพัฒนาที่มีประสบการณ์ในภาษาโปรแกรมทั่วไป

ฟังก์ชั่นการเขียนโปรแกรม101

Functional Programming (FP) คือรูปแบบที่คุณสร้างโปรแกรมของคุณเป็นองค์ประกอบของฟังก์ชัน แปลงข้อมูลจาก $A$ เป็น $B$ เป็น $C$ ฯลฯ จนกว่าจะได้ผลลัพธ์ที่ต้องการ ในการเขียนโปรแกรมเชิงวัตถุ (OOP) คุณบอกคอมพิวเตอร์ว่าต้องทำอย่างไรตามคำสั่ง การเขียนโปรแกรมเชิงฟังก์ชันแตกต่างกัน: คุณยกเลิกขั้นตอนการควบคุมและกำหนด "สูตรของฟังก์ชัน" เพื่อสร้างผลลัพธ์แทน

สี่เหลี่ยมสีเขียวทางด้านซ้ายที่มีข้อความ "อินพุต: x" มีลูกศรชี้ไปที่สี่เหลี่ยมผืนผ้าสีเทาอ่อนที่ระบุว่า "ฟังก์ชัน: f" ภายในสี่เหลี่ยมสีเทาอ่อน มีกระบอกสูบสามกระบอกพร้อมลูกศรชี้ไปทางขวา อันแรกเป็นสีน้ำเงินอ่อนเขียนว่า "A(x)" อันที่สองเป็นสีน้ำเงินเข้มเขียนว่า "B(x)" และอันที่สามเป็นสีเทาเข้มเขียนว่า "C (x)" ทางด้านขวาของสี่เหลี่ยมสีเทาอ่อน จะมีสี่เหลี่ยมสีเขียวที่มีข้อความ "Output: f(x)" ด้านล่างของสี่เหลี่ยมสีเทาอ่อนมีลูกศรชี้ไปที่ข้อความ "ผลข้างเคียง"
รูปแบบการเขียนโปรแกรมเชิงฟังก์ชัน

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())$:

กล่องสีน้ำเงินที่เขียนว่า "A" มีลูกศร "f" ชี้ไปที่กล่องสีน้ำเงินที่เขียนว่า "B" ที่มีลูกศร "g" ชี้ไปที่กล่องสีน้ำเงินที่เขียนว่า "C" กล่อง "A" มีลูกศรคู่ขนาน "g o f" ชี้ไปที่ช่อง "C" โดยตรง
ฟังก์ชั่น f, g และ h องค์ประกอบของ g กับ f

โซลูชันที่จำเป็นทุกวิธีสามารถแปลเป็นโซลูชันที่เปิดเผยได้โดยการแยกปัญหาออกเป็นปัญหาที่เล็กกว่า แก้ปัญหาอย่างอิสระ และจัดองค์ประกอบโซลูชันที่มีขนาดเล็กลงในโซลูชันสุดท้ายผ่านองค์ประกอบของฟังก์ชัน ลองดูปัญหาชื่อของเราจากส่วนก่อนหน้าเพื่อดูแนวคิดนี้ในการดำเนินการ ปัญหาเล็ก ๆ ของเราจากการแก้ปัญหาที่จำเป็นคือ:

  1. isVowel :: Char -> Bool : ให้ Char ให้ส่งคืนไม่ว่าจะเป็นสระหรือไม่ ( Bool )
  2. countVowels :: String -> Int : ให้ String ส่งคืนจำนวนสระในนั้น ( Int )
  3. hasThreeVowels :: String -> Bool : Given a String ให้คืนค่าว่ามีสระอย่างน้อยสามตัว ( Bool )
  4. uppercaseVowels :: String -> String : ให้ String ส่งคืน String ใหม่ด้วยสระตัวพิมพ์ใหญ่

โซลูชันการประกาศของเรา ซึ่งทำได้โดยองค์ประกอบของฟังก์ชัน คือ map uppercaseVowels . filter hasThreeVowels map uppercaseVowels . filter hasThreeVowels

แผนภาพด้านบนมีกล่อง "[สตริง]" สีน้ำเงินสามกล่องเชื่อมต่อกันด้วยลูกศรชี้ไปทางขวา ลูกศรแรกมีชื่อว่า "filter has3Vowels" และลูกศรที่สองมีชื่อว่า "map uppercaseVowels" ด้านล่าง แผนภาพที่สองมีกล่องสีน้ำเงินสองกล่องทางด้านซ้าย "Char" ด้านบน และ "String" ด้านล่าง ซึ่งชี้ไปที่กล่องสีน้ำเงินทางด้านขวา "Bool" ลูกศรจาก "Char" ถึง "Bool" จะมีป้ายกำกับว่า "isVowel" และลูกศรจาก "String" ถึง "Bool" จะมีป้ายกำกับว่า "has3Vowels" กล่อง "สตริง" ยังมีลูกศรชี้ไปที่ตัวเองที่มีข้อความว่า "ตัวพิมพ์ใหญ่Vowels"
ตัวอย่างองค์ประกอบของฟังก์ชันโดยใช้ชื่อปัญหา

ตัวอย่างนี้ซับซ้อนกว่าสูตร $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

การเขียนโปรแกรมเชิงโต้ตอบเป็นรูปแบบการเขียนโปรแกรมที่เปิดเผยซึ่งโปรแกรมตอบสนองต่อข้อมูลหรือการเปลี่ยนแปลงเหตุการณ์แทนการขอข้อมูลเกี่ยวกับการเปลี่ยนแปลง

กล่องสีน้ำเงินหลักสองกล่อง "สังเกตได้" และ "สถานะ" มีสองเส้นทางหลักระหว่างพวกเขา อย่างแรกคือผ่าน "สังเกต (ฟังการเปลี่ยนแปลง)" ประการที่สองคือผ่าน "แจ้ง (สถานะล่าสุด)" ไปยังกล่องสีน้ำเงิน "UI (API ในส่วนหลัง)" ซึ่งผ่าน "แปลงอินพุตของผู้ใช้เป็น" เป็นกล่องสีน้ำเงิน "เหตุการณ์" ซึ่งผ่าน "ทริกเกอร์" เป็นสีน้ำเงิน กล่อง "องค์ประกอบฟังก์ชัน" และสุดท้ายผ่าน "ผลิต (สถานะใหม่)" จากนั้น "สถานะ" จะเชื่อมต่อกลับไปที่ "องค์ประกอบฟังก์ชัน" ผ่าน "ทำหน้าที่เป็นอินพุตสำหรับ"
วงจรการเขียนโปรแกรมปฏิกิริยาทั่วไป

องค์ประกอบพื้นฐานในวงจรการเขียนโปรแกรมเชิงโต้ตอบคือเหตุการณ์ ไปป์ไลน์ที่ประกาศ สถานะ และสิ่งที่สังเกตได้:

  • เหตุการณ์ คือสัญญาณจากโลกภายนอก ซึ่งโดยทั่วไปจะอยู่ในรูปแบบของการป้อนข้อมูลของผู้ใช้หรือเหตุการณ์ของระบบ ที่ทริกเกอร์การอัปเดต จุดประสงค์ของเหตุการณ์คือการแปลงสัญญาณเป็นอินพุตไปป์ไลน์
  • ไปป์ไลน์การประกาศ เป็นองค์ประกอบของฟังก์ชันที่ยอมรับ (Event, State) เป็นอินพุตและแปลงอินพุตนี้เป็น State ใหม่ (เอาต์พุต): (Event, State) -> f -> g -> … -> n -> State ไปป์ไลน์ต้องดำเนินการแบบอะซิงโครนัสเพื่อจัดการกับหลายเหตุการณ์โดยไม่ปิดกั้นไปป์ไลน์อื่นหรือรอให้เสร็จสิ้น
  • สถานะคือการแสดงรูปแบบข้อมูลของ แอ ปพลิเคชันซอฟต์แวร์ ณ เวลาที่กำหนด ตรรกะของโดเมนใช้สถานะเพื่อคำนวณสถานะถัดไปที่ต้องการและทำการอัปเดตที่เกี่ยวข้อง
  • ผู้ สังเกตการณ์ รับฟังการเปลี่ยนแปลงสถานะและอัปเดตสมาชิกเกี่ยวกับการเปลี่ยนแปลงเหล่านั้น ใน Android สิ่งที่สังเกตได้มักจะถูกใช้งานโดยใช้ Flow , LiveData หรือ RxJava และแจ้งเตือน UI ของการอัปเดตสถานะเพื่อให้สามารถตอบสนองตามนั้น

มีคำจำกัดความและการใช้งานมากมายของการเขียนโปรแกรมเชิงโต้ตอบ ในที่นี้ ฉันได้ใช้แนวทางปฏิบัติที่เน้นการใช้แนวคิดเหล่านี้กับโครงการจริง

การเชื่อมต่อจุด: การเขียนโปรแกรมปฏิกิริยาเชิงหน้าที่

การเขียนโปรแกรมเชิงหน้าที่และเชิงโต้ตอบเป็นกระบวนทัศน์ที่ทรงพลังสองประการ แนวคิดเหล่านี้มีมากกว่าอายุสั้นของไลบรารีและ API และจะพัฒนาทักษะการเขียนโปรแกรมของคุณไปอีกหลายปี

นอกจากนี้ พลังของ FP และการเขียนโปรแกรมเชิงโต้ตอบยังทวีคูณเมื่อรวมกัน ตอนนี้เรามีคำจำกัดความที่ชัดเจนของการเขียนโปรแกรมเชิงฟังก์ชันและเชิงโต้ตอบแล้ว เราสามารถรวมส่วนต่างๆ เข้าด้วยกันได้ ในส่วนที่ 2 ของบทช่วยสอนนี้ เรากำหนดกระบวนทัศน์การเขียนโปรแกรมเชิงโต้ตอบเชิงฟังก์ชัน (FRP) และนำไปปฏิบัติด้วยตัวอย่างการใช้งานแอปและไลบรารี Android ที่เกี่ยวข้อง

บล็อก Toptal Engineering ขอขอบคุณ Tarun Goyal สำหรับการตรวจสอบตัวอย่างโค้ดที่นำเสนอในบทความนี้