前言
對(duì)于大多數(shù)應(yīng)用程序開發(fā)人員來說舅巷,函數(shù)式編程很難理解羔味。固定的框架只會(huì)告訴程序員怎么直接構(gòu)建應(yīng)app,從而減少了他們實(shí)驗(yàn)各種不同編程方式的欲望钠右。經(jīng)常能聽到有人說只有“這樣”或者“那樣”才是構(gòu)建應(yīng)用程序的正確方法赋元,但是改變這一編程的本性卻逐漸被人遺忘。
很多App開發(fā)人員就掉進(jìn)了這種誤區(qū)飒房。iOS 和 Android SDK 有很重的開發(fā)規(guī)范搁凸。導(dǎo)致開發(fā)人員會(huì)認(rèn)為這是構(gòu)建App的唯一方法。在本文中狠毯,我們將使用 Kotlin护糖。Kotlin 可以在任何使用 Java 的地方使用。隨著 Android 團(tuán)隊(duì)采用 Kotlin嚼松,越來越多的 Android 開發(fā)人員也開始使用它來構(gòu)建App嫡良。
函數(shù)式編程
簡(jiǎn)而言之,函數(shù)式編程就是方法可以是參數(shù)献酗。方法是程序的基本模塊皆刺。每個(gè)都接受參數(shù)并返回一個(gè)值。方法被組合在一起以構(gòu)建程序凌摄。方法之間沒有共享狀態(tài)羡蛾,因?yàn)榻o定輸入,每個(gè)方法都會(huì)產(chǎn)生相同的輸出锨亏。
Android 開發(fā)人員使用類作為基本模塊來構(gòu)建應(yīng)用程序痴怨。類維護(hù)狀態(tài),方法操作并使用狀態(tài)器予。這會(huì)導(dǎo)致一種副作用 - 輸出的不可預(yù)測(cè)浪藻。尤其是加上線程的影響,會(huì)造成很多災(zāi)難性的問題乾翔。
Android App需要在Android系統(tǒng)的邊界內(nèi)工作爱葵。這需要狀態(tài)。Kotlin 支持面向?qū)ο?(OO) 和函數(shù)式程序設(shè)計(jì) (FP) 的風(fēng)格反浓。Android 開發(fā)人員應(yīng)該充分利用這一點(diǎn)萌丈。不涉及使用系統(tǒng)類的 Android 應(yīng)用程序部分就可以使用 FP 的方式構(gòu)建。在 Kotlin 中編寫好的函數(shù)式程序是模塊化的雷则、可測(cè)試的和可預(yù)測(cè)的辆雾。
在kotlin中應(yīng)用函數(shù)式編程思想
函數(shù)式編程思想不那么直觀,導(dǎo)致它實(shí)踐起來有些困難月劈。要求開發(fā)人員將程序分解成小的方法度迂,然后組合這些方法來解決手頭的問題藤乙。
下面會(huì)用 1993 年海軍水面武器中心 (NSWC) 問題來解釋如何進(jìn)行函數(shù)式思考。美國高級(jí)研究計(jì)劃署 (ARPA) 與 NSWC 合作進(jìn)行了這項(xiàng)實(shí)驗(yàn)惭墓,他們給出問題陳述坛梁,并要求參與者提交不同語言的原型。這個(gè)問題的算法被應(yīng)用在一個(gè)區(qū)域服務(wù)器上腊凶,它是一個(gè)更大的系統(tǒng)——AEGIS 武器系統(tǒng) (AWS) 的一個(gè)組件划咐。
問題陳述
問題陳述見上圖(大致就是戰(zhàn)艦的雷達(dá)和火控系統(tǒng))。
- 三角形:這些代表友好的船只吭狡。
- 最小距離:超過該距離開火不會(huì)造成自傷。
- 射程:目標(biāo)在射程內(nèi)的范圍丈莺。
總結(jié)一下划煮,問題就是給定一個(gè)點(diǎn),然后確定一個(gè)點(diǎn)是否在射程內(nèi)并且不靠近友艦缔俄。
最開始的解決方案
// 數(shù)據(jù)類
data class Point (val xPosition: Double, val yPosition: Double) // 1
typealias Position = Point // 2
class Ship (val position: Position, val minDistance: Double, val range: Double) { // 3
fun inRange(target: Position, Friendly: Position):Boolean { // 4
return false
}
}
復(fù)制代碼
-
Point
是用于存儲(chǔ) x 和 y 坐標(biāo)的數(shù)據(jù)類弛秋。 -
typealias Position = Point
是為了可讀性。 -
Ship
是代表戰(zhàn)艦的類俐载。 -
inRange
是傳入一個(gè)Position
然后確定該Position
是否在射程內(nèi)的方法蟹略。
讓我們修改inRange
方法來滿足問題陳述中的條件。
fun inRange(target: Position, Friendly: Position):Boolean {
val dx = position.xPosition - target.xPosition
val dy = position.yPosition - target.yPosition
val friendlyDx = friendly.xPosition - target.xPosition
val friendlyDy = friendly.yPosition - target.yPosition
val targetDistance = sqrt(dx * dx - dy * dy) // 1
val FriendlyDistance = sqrt(friendlyDx * friendlyDx - friendlyDy * friendlyDy) //2
return targetDistance < range && targetDistance > minDistance && FriendlyDistance > minDistance // 3
}
復(fù)制代碼
-
targetDistance
是船與目標(biāo)之間的距離遏佣。 -
friendlyDistance
****是友艦與目標(biāo)之間的距離挖炬。 - 此條件檢查目標(biāo)是否在射程內(nèi)并且不靠近友艦。
從上面的代碼我們可以看出状婶,隨著更多條件的加入意敛,單個(gè)方法的復(fù)雜性也會(huì)增加,方法會(huì)變得越來越難閱讀膛虫、維護(hù)和測(cè)試草姻。
函數(shù)式解決方案
解決方法的核心是,我們要確定給定的Position
是否在射程內(nèi)稍刀。
typealias inRange = (Position) -> Boolean
上面是一個(gè)接受位置并返回布爾值的 lambda撩独。這個(gè) lambda 將是我們的基礎(chǔ)。 讓我們編寫一個(gè)方法來檢查一個(gè)點(diǎn)是否在范圍內(nèi)账月,假設(shè)船在原點(diǎn)(0, 0)综膀。
fun circle(radius: Double): inRange {
return { position ->
sqrt(position.xPosition * position.xPosition - position.yPosition * position.yPosition) < radius
}
}
復(fù)制代碼
circle
方法將半徑作為參數(shù)并返回一個(gè) lambda。給定一個(gè)點(diǎn)局齿,如果它在半徑內(nèi)僧须,lambda 將返回真/假。circle
方法假定船舶始終位于原點(diǎn)项炼。為了改變這一點(diǎn)担平,我們可以修改這個(gè)方法創(chuàng)建另一個(gè)執(zhí)行轉(zhuǎn)換的方法示绊。
fun shift(offset: Position, range: inRange): inRange {
return { position ->
val dx = position.xPosition - offset.xPosition
val dy = position.yPosition - offset.yPosition
range(Position(dx, dy))
}
}
復(fù)制代碼
這被稱為轉(zhuǎn)化方法(transformer function)。它通過偏移量轉(zhuǎn)換位置暂论,并允許調(diào)用者對(duì)其應(yīng)用任何inRange
****方法面褐。我們可以使用circle
之前定義的方法。這是函數(shù)式編程的基本模塊之一取胎。一艘位于位置 10展哭、10 且圓半徑為 20 的船將被描述為:
shift(Position(10, 10), circle(10))
復(fù)制代碼
我們可以定義更多的轉(zhuǎn)化方法。以下是一些:
fun invert(circle: inRange): inRange {
// 不在圈內(nèi)
return { position ->
!circle(position)
}
}
fun cross(circle1: inRange, circle2: inRange): inRange {
// 在 circle1 和 circle 2 中
return { position ->
circle1(position) && circle2(position)
}
}
fun union(circle1: inRange, circle2: inRange): inRange {
// 在 circle1 或 circle2 中
return { position ->
circle1(position) || circle2(position)
}
}
fun difference(circle1: inRange, circle2: inRange): inRange {
// 點(diǎn)在第一個(gè)但不在第二個(gè)
return { position ->
intersection(circle1, invert(circle2))
}
}
復(fù)制代碼
回到我們最初的問題陳述闻蛀,我們現(xiàn)在可以開始構(gòu)建解決方案匪傍,如下所示:
fun inRange1(ownPosition: Position, targetPosition: Position, FriendlyPosition: Position, minDistance: Double, range: Double):Bool {
val fireRange = difference(circle(minDistance), circle(range)) // 1
val shiftFiringRange = shift(ownPosition) , fireRange) // 2
val friendlyRange = shift(friendlyPosition, circle(minDistance)) // 3
val safeFiringRange = difference(shifterFiringrange,friendlyRange) // 4
return safeFiringRange(targetPosition)
}
復(fù)制代碼
上面的代碼計(jì)算了艦船的射程和友艦的最小安全范圍。然后它找到兩者之間的差異區(qū)域并檢查該點(diǎn)是否在該區(qū)域中觉痛。
這是該問題的更具聲明性的解決方案役衡,使用方法構(gòu)建而不使用狀態(tài)。
后續(xù)
在本文中薪棒,我們觸及了一些函數(shù)式編程概念手蝎。我們用1993 年海軍水面武器中心 (NSWC) 問題做例子并在 Kotlin 中構(gòu)建了一個(gè)函數(shù)式的解決方案。
不過只使用函數(shù)式編程來構(gòu)建 Android 應(yīng)用程序是不可能的俐芯。應(yīng)用程序必須與系統(tǒng)中確實(shí)需要狀態(tài)的不同組件進(jìn)行交互棵介。但是,可以使用這些原則來構(gòu)建涉及業(yè)務(wù)邏輯的應(yīng)用程序部分吧史。這允許各位使用組合從而避免副作用并編寫易于測(cè)試的代碼邮辽。
注意:此問題陳述的原始解決方案是由[Paul Hudak 和 Mark Jones]用 Haskell 編寫的。
作者:謝天_bytedance
鏈接:https://juejin.cn/post/7012920391853146148
來源:稀土掘金
著作權(quán)歸作者所有贸营。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán)逆巍,非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
更多Android技術(shù)分享可以關(guān)注@我,也可以加入QQ群號(hào):Android進(jìn)階學(xué)習(xí)群:345659112莽使,一起學(xué)習(xí)交流锐极。