上周 ,在Arrow库的帮助下,我们将Kotlin中的Scala应用程序从命令式移植到功能移植。 这很容易:为我们布置了示例。 这个星期,我想去买一些更基本的东西。
这是从命令式函数式编程的重点series.Other职位包括第2 个职位:
- 使用箭头从命令式编程到函数式编程
- 从命令式编程到函数式编程,一种方法 (本文)
- 从命令式编程到函数式编程:分组问题(以及解决方法)
- 从命令式编程到函数式编程:Dijkstra算法
在HackerRank上 ,存在以下问题:
考虑一个大小为4的楼梯:
# ## ### ####观察其底边和高度都等于n ,并使用
#
符号和空格绘制图像。 最后一行之前没有空格。编写一个程序,打印大小为n的阶梯。
一个简单的解决方案命令式的问题如下:
funstaircase(n:Int):Unit{
for(i:Intin0untiln){
for(j:Intin0untiln){
if(i+j<n-1)print(" ")
elseprint("#")
if(j==n-1)println()
}
}
}
该解决方案的主要问题是其可测试性。 从理论上讲,可以通过更改System.out
写入的位置来对其进行测试,但这很麻烦(至少可以这样说)。 另外,如果多个测试进行相同的更改,则它们不能并行运行。 如FP所述,副作用是有害的。 而写出标准是一个巨大的副作用。
将副作用推到边缘
让staircase()
函数返回一个String
,将书写部分移到代码的边缘,然后输出结果:
funstaircase(n:Int):String{
valbuilder=StringBuilder()
for(i:Intin0untiln){
for(j:Intin0untiln){
if(i+j<n-1)builder.append(" ")
elsebuilder.append("#")
if(j==n-1)builder.append(System.lineSeparator())
}
}
returnbuilder.toString()
}
这是解决可测试性问题的第一个(也是IMHO的主要步骤)。
删除循环
为了进一步走到FP路径,需要将循环迁移到递归性。
让我们首先迁移内部循环:
funstaircase(n:Int):String{
valbuilder=StringBuilder()
for(i:Intin0untiln){
row(i,0,n,builder)
}
returnbuilder.toString()
}
privatefunrow(i:Int,j:Int,n:Int,builder:StringBuilder){
builder.append(if(i+j<n-1)" "else"#")
if(j<n-1)row(i,j+1,n,builder)
elsebuilder.append(System.lineSeparator())
}
现在,到外循环:
funstaircase(n:Int):String{
valbuilder=StringBuilder()
column(0,n,builder)
returnbuilder.toString()
}
privatefuncolumn(i:Int,n:Int,builder:StringBuilder){
if(i<n){
row(i,0,n,builder)
column(i+1,n,builder)
}
}
privatefunrow(i:Int,j:Int,n:Int,builder:StringBuilder){
builder.append(if(i+j<n-1)" "else"#")
if(j<n-1)row(i,j+1,n,builder)
elsebuilder.append(System.lineSeparator())
}
优化一点
在Kotlin中, tailrec
修饰符允许通过编写命令式字节码来优化递归函数的生成字节码。 唯一的要求是,递归调用必须是函数中的最后一个调用 。 让我们在row()
函数中反转if
/ else
条件:
privatetailrecfunrow(i:Int,j:Int,n:Int,builder:StringBuilder){
builder.append(if(i+j<n-1)" "else"#")
if(j>=n-1)builder.append(System.lineSeparator())
elserow(i,j+1,n,builder)
}
同样, tailrec
可以按tailrec
应用于column()
函数。
移除状态
最后一个问题是val builder = StringBuilder()
。 状态与FP不兼容。 此外, column()
和row()
函数不会返回,也不是纯函数。
让我们将现有代码迁移到纯函数。 既然有点复杂,我们将分多个步骤进行。
第一步是显式返回StringBuilder
,同时保留累加器( 即 builder
参数):
funstaircase(n:Int):String{
returncolumn(0,n,StringBuilder()).toString()
}
privatefuncolumn(i:Int,n:Int,builder:StringBuilder):StringBuilder{
if(i<n){
row(i,0,n,builder)
returncolumn(i+1,n,builder)
}
returnbuilder
}
接下来的步骤是将类型更改为一个不变的一个- String
代替StringBuilder
:
funstaircase(n:Int):String{
returncolumn(0,n,"")
}
privatefuncolumn(i:Int,n:Int,string:String):String{
if(i<n){
valbuilder=StringBuilder(string)
row(i,0,n,builder)
returncolumn(i+1,n,builder.toString())
}
returnstring
}
现在,让我们对row()
函数使用相同的技术。 首先,介绍一个返回类型:
privatefuncolumn(i:Int,n:Int,string:String):String{
if(i<n){
valbuilder=StringBuilder(string)
valrow=row(i,0,n,builder).toString()
returncolumn(i+1,n,row)
}
returnstring
}
privatetailrecfunrow(i:Int,j:Int,n:Int,builder:StringBuilder):StringBuilder{
builder.append(if(i+j<n-1)" "else"#")
returnif(j>=n-1)builder.append(System.lineSeparator())
elserow(i,j+1,n,builder)
}
最后,让我们如上所述更改数据类型:
privatefuncolumn(i:Int,n:Int,string:String):String{
if(i<n){
valrow=row(i,0,n,string)
returncolumn(i+1,n,row)
}
returnstring
}
privatetailrecfunrow(i:Int,j:Int,n:Int,string:String):String{
valline=if(i+j<n-1)"$string "else"$string#"
returnif(j>=n-1)line+System.lineSeparator()
elserow(i,j+1,n,line)
}
挑剔
在上面的代码中,仍然有两个声明的变量row
和line
。 如果要走整个九码,则需要将其拆除。 为此,很容易引入另一个(纯)函数:
privatetailrecfuncolumn(i:Int,n:Int,string:String):String=
if(i>=n)string
elsecolumn(i+1,n,row(i,0,n,string))
}
privatetailrecfunrow(i:Int,j:Int,n:Int,string:String):String=
if(j>=n-1)line(i,j,n,string)+System.lineSeparator()
elserow(i,j+1,n,line(i,j,n,string))
}
privatefunline(i:Int,j:Int,n:Int,string:String)=
if(i+j<n-1)"$string "
else"$string#"
奖励:一些Clojure的爱
在我最近尝试学习Clojure时,这是等效的Clojure代码:
(defn line [i, j, n, string]
(if (< (+ i j) (- n 1))
(str string " ")
(str string "#")))
(defn row [i, j, n, string]
(if (>= j (- n 1))
(str (line i j n string) (System/lineSeparator))
(row i (+ j 1) n (line i j n string))))
(defn column [i, n, string]
(if (>= i n)
string
(column (+ i 1) n (row i 0 n string))))
(defn staircase [n]
(column 0 n ""))
结论
至于OOP,因此迁移到FP会使代码更难阅读和维护。 恕我直言,只要将console-write调用推到main函数之外就足够了。 当然,这取决于您(和您的团队)对FP的熟悉程度和经验。
翻译自: https://blog.frankel.ch/imperative-functional-programming/2/