作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
伊利扬是一名安卓开发者和首席技术官,他创立了四家初创公司,开发了几款顶级应用, 包括Ivy Wallet, 它获得了10个YouTube技术社区“最佳UI/UX”奖. 他擅长函数式编程、UX、芬兰湾的科特林和Haskell.
编写干净的代码可能具有挑战性:库, 框架, 和api都是临时的,很快就会过时. But mathematical concepts 和 paradigms are lasting; they require years of academic research 和 may even outlast us.
这不是向您展示如何使用库Y执行X的教程. 而不是, 我们专注于函数式和响应式编程背后的持久原则,因此您可以构建面向未来且可靠的程序 安卓系统架构,在不影响效率的情况下扩大规模并适应变化.
这篇文章奠定了基础, 在第2部分, 我们将深入研究函数式响应式编程(FRP)的实现。, 它结合了函数式编程和响应式编程.
这篇文章是由 安卓开发者 记住, 但是,这些概念对任何具有通用编程语言经验的开发人员都是相关且有益的.
函数式编程 (FP)是一种将程序构建为函数组合的模式, 将数据从A$转换为B$, 美元加元, 等.,直到达到期望的输出. 在面向对象编程(OOP)中,你一条指令一条指令地告诉计算机该做什么. 函数式编程则不同:放弃控制流,定义“函数配方”来生成结果.
具体来说,FP源自数学 微积分,一个功能抽象的逻辑系统. 而不是像循环这样的OOP概念, 类, 多态性, 或继承, FP严格处理抽象和高阶函数, 接受其他函数作为输入的数学函数.
简而言之, FP有两个主要的“参与者”:数据(模型), (或问题所需的信息)和功能(行为的表示和数据之间的转换). 相比之下, OOP类显式地将特定于领域的数据结构(以及与每个类实例相关联的值或状态)绑定到打算与之一起使用的行为(方法).
我们将更仔细地研究计划生育的三个关键方面:
进一步深入FP世界的一个很好的起点是 Haskell一种强类型的纯函数式语言. 我推荐 学习Haskell为伟大的好! 交互式教程是一种有益的资源.
关于FP程序,您会注意到的第一件事是它是用 声明,而不是命令式. 简而言之,声明性编程告诉程序需要做什么,而不是如何做. 让我们以一个命令式编程与声明式编程的具体示例作为这个抽象定义的基础,以解决以下问题, 返回一个列表,其中仅包含至少有三个元音且元音字母为大写字母的名称.
首先,让我们检查一下这个问题的命令式解决方案 芬兰湾的科特林:
fun 名字sImperative(input: List<字符串>): List<字符串> {
val 结果 = mutableListOf<字符串>()
val元音=自然(“A”,“E”,‘我’,‘O’,‘你’,‘‘,‘E’,‘我’,‘O’,‘U’)
For (名字 in input){//循环1
var vowelsCount = 0
For (char in 名字){//循环2
if (isVowel(char, vowel)) {
vowelsCount + +
if (vowelsCount == 3) {
val uppercaseName = 字符串Builder()
for (final字符 in 名字){//循环
var transformmedchar = final字符
//忽略第一个字母可能是大写
if (isVowel(final字符,元音)){
transformmedchar = final字符.uppercase字符 ()
}
uppercaseName.追加(transformed字符)
}
结果.add (uppercaseName.to字符串 ())
打破
}
}
}
}
返回结果
}
fun isVowel(char: 字符, vowels: List<字符>): 保龄球ean {
返回元音.包含(char)
}
Fun main() {
println(名字sImperative(listOf(“Iliyan”,“Annabel”,“Nicole”,“John”,“Anthony”,“Ben”,“Ken”)))))
// [IlIyAn, AnnAbEl, NIcOlE]
}
现在,我们将根据几个关键的开发因素来分析我们的命令式解决方案:
最有效的: 此解决方案具有最佳的内存使用,并且在Big O分析中表现良好(基于最少的比较次数)。. 在这个算法中, 分析字符之间比较的次数是有意义的,因为这是我们算法中的主要操作. 设$n$为名称的数目,设$k$为名称的平均长度.
isVowel ()
再次检查以决定是否将字符大写-再次, 在最坏的情况下, 这与10个元音相比).结果
, vowelsCount
, transformed字符
; these state mutations can lead to subtle errors like forgetting to reset vowelsCount
回到0. 执行流程也可能变得复杂,而且很容易忘记添加 打破
语句.我们的示例解决方案说明了命令式代码看起来有多么复杂, 尽管您可以通过将其重构为更小的函数来改进代码.
现在我们明白了 声明性编程 不是,让我们在芬兰湾的科特林中展示我们的声明式解决方案:
fun 名字sDeclarative(input: List<字符串>): List<字符串> = input.过滤器 { 名字 ->
名字.count(::isVowel) >= 3
}.map { 名字 ->
名字.map { char ->
if (isVowel(char)) char.uppercase字符 () else char
}.joinTo字符串 (" ")
}
fun isVowel(char: char): 保龄球ean =
自然(“A”、“E”,‘我’,‘O’,‘你’,‘“,‘E’,‘我’,‘O’,‘U’).包含(char)
Fun main() {
println(名字sDeclarative(listOf(“Iliyan”,“Annabel”,“Nicole”,“John”,“Anthony”,“Ben”,“Ken”))))))
// [IlIyAn, AnnAbEl, NIcOlE]
}
使用与评估命令式解决方案相同的标准, 让我们看看声明性代码是如何运作的:
名字.count ()
在这里,它将继续计数元音直到名字的结尾(即使找到三个元音)。. 我们可以通过编写一个简单的 hasThreeVowels (字符串):布尔
函数. 此解决方案使用与命令式解决方案相同的算法, 所以同样的复杂性分析也适用于这里:我们的算法运行 $O(n)$ time.List<字符串>
在所有的名字中 List<字符串>
包含三个或更多元音的名字,然后对每个元音进行转换 字符串
逐字逐句 字符串
有大写元音的单词. 整体, 没有突变, 嵌套循环, 或中断和放弃控制流使代码更简单,出错的空间更小.过滤器
条件: Val元音= 名字.count(::isVowel); vowels >= 3 && 名字.length - vowels >= 5
.作为一个额外的积极因素, 我们的声明式解决方案是纯函数式的:本例中的每个函数都是纯函数,没有副作用. (稍后会详细介绍纯度.)
让我们看一下在纯函数式语言(如Haskell)中对相同问题的声明式实现,以演示它是如何读取的. 如果您不熟悉Haskell,请注意 .
操作符在Haskell中读作“after”.“例如, solution = 地图uppercaseVowels . 过滤器hasThreeVowels
翻译为“在过滤包含三个元音的名称后将元音映射为大写字母”.”
导入数据.字符 (toUpper)
名字sSolution :: [字符串] -> [字符串]
名字sSolution = map uppercasevwell . 过滤器hasThreeVowels
hasThreeVowels :: 字符串 -> 保龄球
hasThreeVowels s = count isVowel s >= 3
uppercaseVowels :: 字符串 -> 字符串
uppercaseVowel = map uppercaseVowel
在哪里
uppercaseVowel :: 字符 -> 字符
uppercaseVowel c
|是元音c =元音c
|否则= c
isVowel :: 字符 -> 保龄球
isVowel c = c ' elem '元音
元音::[字符]
元音= [' A ',‘E’,‘我’,‘O’,‘你’,‘‘,‘E’,‘我’,‘O’,‘U’)
count :: (a -> 保龄球) -> [a] -> Int
Count _ [] = 0
Count pred (x:xs)
| predx = 1 + count predxs
|否则= count pred xs
main:: IO ()
main = print $ 名字sSolution ["Iliyan", "Annabel", "Nicole", "John", "Anthony", "Ben", "Ken"]
——(“IlIyAn”,”安娜贝利”,“妮可”)
此解决方案的执行类似于我们的 芬兰湾的科特林 声明式解决方案, 还有一些额外的好处:它是可读的, 如果你理解Haskell的语法,就会觉得很简单, 纯粹的功能, 懒惰的.
声明式编程对FP和响应式编程都很有用(我们将在后面的部分介绍).
如果你的安卓代码读起来不像一个句子,你可能做错了什么.
不过,声明式编程也有一些缺点. 最终可能会产生效率低下的代码,消耗更多的RAM,并且比命令式实现的性能更差. 排序, 反向传播(在机器学习中), 而其他的“变异算法”并不适合不可变, 声明式编程风格.
函数组合是函数式编程的核心数学概念. 如果函数$f$接受$A$作为其输入并产生$B$作为其输出($f: A \右转B$), 函数$g$接受$B$并产生$C$ ($g: B \右转C$), 然后可以创建第三个函数, $h$, 它接受$A$并产生$C$ ($h: A \右转C$). 我们可以把第三个函数定义为 作文 $g$与$f$的关系,也记作$g \circ f$或$g(f())$:
通过将问题分解为更小的问题,每个命令式解决方案都可以转化为声明式解决方案, 独立解决问题, 通过函数组合将小的解重新组合成最终解. 让我们看看前一节中的名称问题,看看这个概念的实际应用. 命令式解决方案的小问题是:
isVowel :: 字符 -> 保龄球
给定一个 字符
,返回是否为元音(保龄球
).countVowels :: 字符串 -> Int
给定一个 字符串
,返回其中的元音数目(Int
).hasThreeVowels :: 字符串 -> 保龄球
给定一个 字符串
,返回是否至少有三个元音(保龄球
).uppercaseVowels :: 字符串 -> 字符串
给定一个 字符串
,返回一个新的 字符串
用大写元音.通过函数组合实现的声明性解决方案是 地图uppercaseVowels . 过滤器hasThreeVowels
.
这个例子比简单的$ a \右箭头B \右箭头C$公式要复杂一些, 但是它展示了函数组合背后的原理.
函数组合是一个简单而强大的概念.
组合函数时, 您不仅可以传递数据,还可以将函数作为输入传递给其他函数——这是一个高阶函数的例子.
对于函数组合,还有一个我们必须解决的关键因素:所组合的函数必须是 纯是另一个源自数学的概念. 在数学, all 函数s are computations that always yield the same output 当使用相同的输入调用时; this is the basis of purity.
让我们看一个使用数学函数的伪代码示例. 假设我们有一个函数, 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
由除以0得到.日志.d
, LocalDateTime.现在
, 语言环境.getDefault
这只是几个例子吗.记住这些定义,我们就可以定义 纯函数 作为整体功能,没有副作用. 仅使用纯函数构建的函数组合更可靠, 可预测的, 以及可测试的代码.
提示: 使总函数纯净, 您可以通过将其作为高阶函数参数传递来抽象其副作用. 通过这种方式,您可以通过传递模拟的高阶函数轻松地测试整个函数. 此示例使用 @SideEffect
注释来自我们稍后在教程中检查的库,Ivy FRP:
暂停游戏截止日期(
最后期限:LocalDate,
@SideEffect
currentDate: suspend () -> LocalDate
): 保龄球ean = deadline.isAfter (currentDate ())
纯粹性是函数式编程范式所需的最后一个要素.
完成了函数式编程的概述, 让我们检查下一个面向未来的组件 安卓代码: 反应性编程.
反应性编程 是一种声明性编程模式,其中程序对数据或事件更改作出反应,而不是请求有关更改的信息.
响应式编程周期的基本元素是事件, 声明性管道, 州, 可见:
(事件、状态)
作为输入,并将这个输入转换成一个新的 状态
(输出): (事件、状态) -> f -> g -> … -> n -> 状态
. 管道必须异步执行以处理多个事件,而不阻塞其他管道或等待它们完成.流
, LiveData
, or RxJava
,它们将状态更新通知UI,以便UI能够做出相应的反应.有许多响应式编程的定义和实现. 在这里,我采取了一种务实的方法,专注于将这些概念应用到实际项目中.
函数式编程和响应式编程是两个强大的范例. 这些概念超越了库和api的短暂生命周期, 并将在未来几年提高您的编程技能.
此外,FP和响应式编程的能力在结合使用时会成倍增加. 现在我们已经对函数式编程和响应式编程有了清晰的定义, 我们可以把碎片拼起来. In 第2部分 本教程的, 我们定义了功能反应性编程(FRP)范式, 并通过示例应用程序实现和相关安卓库将其付诸实践.
Toptal 工程博客向 塔伦Goyal 查看本文中提供的代码示例.
函数式编程的核心是函数组合, 其中数据从A转换为B, C, 到期望的输出. 它还有另外两个关键元素:它是声明性的,它的函数是纯的.
响应式编程是一种基于两个概念的编程模式:声明式编程和响应性.
响应式程序直接响应数据或事件更改,而不是请求有关更改的信息.
世界级的文章,每周发一次.
世界级的文章,每周发一次.