从命令式编程到函数式编程:Dijkstra算法

本周,我将首先实现Dijkstra算法 ,然后将代码迁移到功能更友好的设计中。

这是从命令式函数式编程的重点series.Other职位包括4 职位:

  1. 使用箭头从命令式编程到函数式编程
  2. 从命令式编程到函数式编程,一种方法
  3. 从命令式编程到函数式编程:分组问题(以及解决方法)
  4. 从命令式编程到函数式编程:Dijkstra算法 (本文)

几句话的Dijkstra算法

节点边组成 。 一条边链接两个节点。

图有不同类型:在加权图中,每个边都有关联的权重。

在有图中,边缘只能沿一个方向行进; 在无向图中,它可以沿任何方向传播。 因此,

Dijkstra的算法允许找到图中的最短路径。 未加权图可以视为所有权重都相同的加权图,而无向图中的边等于有向图中每个方向上的两个边的等效值-权重相同。 因此,任何图都是该算法的候选结构,无论加权与否,是否有向。 唯一的要求是权重必须为正。

算法如下:

  1. 将起始节点设置为当前节点
  2. 重复以下操作,直到当前节点为结束节点为止:
    1. 查找当前节点的边可访问的所有节点
    2. 计算访问未访问节点的总权重
    3. 将当前节点标记为已访问
    4. 前往重量较轻的节点

天真但可行的方法

这是该算法的直接实现:

classGraph(privatevalweightedPaths:Map<String,Map<String,Int>>){

  privatevalevaluatedPaths=mutableMapOf<String,Int>()

  funfindShortestPath(start:String,end:String):Int{
    varcurrent=start
    while(current!=end){
      current=updateShortestPath(current)
    }
    returnevaluatedPaths.getValue(end)
  }

  privatefunupdateShortestPath(node:String):String{
    valsubGraph=weightedPaths[node]
    for(entryinsubGraph!!){
      valcurrentDistance=evaluatedPaths
                               .getOrDefault(entry.key,Integer.MAX_VALUE)
      valnewDistance=entry.value+evaluatedPaths.getOrDefault(node,0)
      if(newDistance<currentDistance)
        evaluatedPaths[entry.key]=newDistance
    }
    evaluatedPaths.remove(node)
    returnevaluatedPaths.minBy{it.value}?.key!!
  }
}

该代码段绝对不是FP友好的,因为它包含以下两个方面:

  • 可变状态, 例如 evaluatedPaths 路径
  • 循环 -一个while循环和一个for循环

迁移到函数式编程

迁移到FP意味着要替换:

  1. 递归循环
  2. 具有传递参数的全局可变状态

另外,Kotlin程序员可能已经注意到!!两种用法!! 运算符:尽管类型系统推断该值可以为 ,但作为开发人员,我们告诉编译器不能。

删除!! 拐杖

有两次出现!! 。 发生这两种情况的原因是,当从Map获取数据时,没有什么可以保证类型系统有相应的键:

valmap=mapOf("A"to1)
vala=map.get("A")// 1
valb=map.get("B")// null

get()返回的类型get()null

为了使它以更惯用的方式不可为空 ,如果返回的值为null ,我们可以抛出一个异常:

valmap=mapOf("A"to1)
vala=map.get("A")?:throwRuntimeException("Cannot happen")
valb=map.get("B")?:throwRuntimeException("Will happen")

对于编译器, ab现在为String类型,即使在运行时,也会抛出“ B”。 尽管异常使函数的部分和部分功能不符合FP,但这是一个很好的步骤。

对于迭代,它甚至更容易。 以下两行是等效的,但第二行更为惯用:

for(entryinsubGraph!!){}
subGraph?.forEach{}

用递归替换循环

代码中存在两个循环:一个while循环和一个for循环。 让我们用它们的等效功能编程-递归替换它们。 递归是一个调用自身的函数。

findShortestPath()实现可以替换为:

funfindShortestPath(start:String,end:String):Int{
  recurseFindShortestPath(start,end)
  returnevaluatedPaths.getValue(end)
}

recurseFindShortestPath()函数是递归的-顾名思义。 递归函数提供了不同的分支,最重要的一个是stop分支。 在我们的情况下,这里的stop分支是node到达end

privatefunrecurseFindShortestPath(node:String,end:String):String{
  returnif(node==end)end
  // else ...
}

另一个分支包含与以前相同的代码,但有一个小的变化:我们通过与更新的节点调用相同的函数来计算下一个节点。

privatefunrecurseFindShortestPath(node:String,end:String):String{
  returnif(node==end)end
  else{
    // ... same code here
    valnextNode=evaluatedPaths.minBy{it.value}?.key
      ?:throwRuntimeException("Map was empty")
      recurseFindShortestPath(nextNode,end)
  }
}

递归的最大问题是当调用在堆栈上相互堆叠时。 因为它基于真实世界的硬件,所以堆栈有一个限制,无论它有多高。 达到目标后,就会发生臭名昭著的StackOverflowError 。 为了避免这种情况,编译器实际上可以生成等效的非递归字节码

这要求:

  • tailrec标记的功能
  • 该函数的最后一条指令应该是递归调用

因为我们的函数符合第二个条件,所以只需向其添加tailrec关键字并优化生成的字节码 ,这是tailrec

一次删除全局状态

用参数替换状态比用递归替换循环更难。 因此,我们将逐步进行操作:第一步是将全局状态移动到局部状态,但暂时保持可变状态。

这是初始代码:

classGraph(privatevalweightedPaths:Map<String,Map<String,Int>>){

  privatevalevaluatedPaths=mutableMapOf<String,Int>()

  funfindShortestPath(start:String,end:String):Int{
    recurseFindShortestPath(start,end)
    returnevaluatedPaths.getValue(end)
  }

  privatetailrecfunrecurseFindShortestPath(node:String,end:String):String{
    // ...

    recurseFindShortestPath(nextNode,end)
  }
}

移动状态后,代码变为如下所示:

classGraph(privatevalweightedPaths:Map<String,Map<String,Int>>){

  funfindShortestPath(start:String,end:String):Int{
      valpaths=mutableMapOf<String,Int>() (1)
      recurseFindShortestPath(start,end,paths) (2)
      returnpaths.getValue(end)
  }

  privatetailrecfunrecurseFindShortestPath(node:String, (2)
                                               end:String,
                                            paths:MutableMap<String,Int>):String{
    // ...

    recurseFindShortestPath(nextNode,end,paths) (2)
  }
}
  1. 在函数内部移动可变状态
  2. 更改功能签名以接受状态并在每次调用时传递它

将状态添加到返回值

因为我们需要不可变状态,所以下一步就是还要沿着现有的返回值返回映射。

classGraph(privatevalweightedPaths:Map<String,Map<String,Int>>){

  funfindShortestPath(start:String,end:String):Int{
    val(_,paths)=recurseFindShortestPath(start,end,mutableMapOf()) (1)
    returnpaths.getValue(end)
  }

  privatetailrecfunrecurseFindShortestPath(node:String,
                                               end:String,
                                             paths:MutableMap<String,Int>):
                                      Pair<String,MutableMap<String,Int>>{ (2)
    returnif(node==end)endtopaths (3)
    else{
      // ...
      recurseFindShortestPath(nextNode,end,paths) (4)
    }
  }
}
  1. 使用Kotlin解构将Map分配给专用变量
  2. 更改函数签名以返回带有StringMap ,将二者包装Pair
  3. StringMap组装在一起,以同时返回
  4. 根据新签名更改递归调用

限制变异

在继续之前,我们将recurseFindShortestPath()函数的很大一部分提取到专用函数中,以将可变性包含在较小的范围内:

classGraph(privatevalweightedPaths:Map<String,Map<String,Int>>){

  funfindShortestPath(start:String,end:String):Int{/* */}

  privatetailrecfunrecurseFindShortestPath(node:String,
                                               end:String,
                                             paths:MutableMap<String,Int>):
                                      Pair<String,MutableMap<String,Int>>{
      returnif(node==end)endtopaths
      else{
          valupdatedPaths=updatePaths(node,paths)
          valnextNode=updatedPaths.minBy{it.value}?.key
              ?:throwRuntimeException("Map was empty")
          recurseFindShortestPath(nextNode,end,updatedPaths)
      }
  }

  privatefunupdatePaths(node:String,
                         paths:MutableMap<String,Int>):MutableMap<String,Int>{
    // ...
  }
}

在这一点上,我们希望开始从可变状态迁移到不可变状态。 让我们更改recurseFindShortestPath()函数的签名:

privatetailrecfunrecurseFindShortestPath(node:String,
                                             end:String,
                                           paths:Map<String,Int>):
                                    Pair<String,Map<String,Int>>{ (1)
  returnif(node==end)endtopaths
  else{
    valupdatedPaths=updatePaths(node,paths.toMutableMap()) (2)
    valnextNode=updatedPaths.minBy{it.value}?.key
        ?:throwRuntimeException("Map was empty")
    recurseFindShortestPath(nextNode,end,updatedPaths) (3)
  }
}
  1. 将签名更改为使用不可变类型而不是可变类型, MutableMapMap
  2. 传递可变类型,因为调用的函数需要它
  3. 函数调用无变化

下一步是以相同的方式更改被调用函数的签名。 为了使更改很小,我们将在函数本身内部继续使用可变状态:

privatefunupdatePaths(node:String,
                       paths:Map<String,Int>):Map<String,Int>{ (1)
  varupdatedPaths=paths (2)
  weightedPaths[node]?.forEach{
    valcurrentDistance=paths.getOrDefault(it.key,Integer.MAX_VALUE)
    valnewDistance=it.value+paths.getOrDefault(node,0)
    if(newDistance<currentDistance)
      updatedPaths=updatedPaths+(it.keytonewDistance) (3)
  }
  returnupdatedPaths-node (4)
}
  1. 仅在函数签名中使用不可变类型
  2. Map分配给var ,以便可以将其重新分配
  3. 通过将Pair实例添加到现有Map来创建新Map ,然后重新分配var
  4. 创建一个新的Map通过删除其关键是入口nodeMap 从路径中删除当前节点,所以它不会再次评估

此时,可以将单个路径的更新提取到其自己的函数中。 这将有助于我们消除可变性的所有痕迹:

privatefunupdatePaths(node:String,paths:Map<String,Int>):Map<String,Int>{
  varupdatedPaths=paths
  weightedPaths[node]?.forEach{
    updatedPaths=updatedPaths+updatePath(paths,it,node)
  }
  returnupdatedPaths-node
}

提取的函数根本没有可变状态,仅使用不可变的类型:

privatefunupdatePath(paths:Map<String,Int>,
                       entry:Map.Entry<String,Int>,
                        node:String):Map<String,Int>{ (1)
  valcurrentDistance=paths.getOrDefault(entry.key,Integer.MAX_VALUE)
  valnewDistance=entry.value+paths.getOrDefault(node,0)
  returnif(newDistance<currentDistance)
    paths+(entry.keytonewDistance) (2)
  else
    paths (2)
}
  1. 该函数的签名仅使用不可变的类型
  2. 由于地图是不可变的,因此可以使用+运算符来组成。 这会返回一个新的地图实例,该条目是两个组成的地图的条目

最后一步是,既然它变得更容易理解,就从updatePaths()的实现中删除可变性。 为此,可以将在可变状态下工作的forEach()迁移到一系列转换中:

privatefunupdatePaths(node:String,paths:Map<String,Int>)=
  (weightedPaths[node]
    ?.map{updatePath(paths,it,node)} (1)
    ?.fold(emptyMap()){acc,item->acc+item} (2)
    ?:emptyMap<String,Int>()) (3)
    -node
  1. 第一个转换是使用updatePath()函数将Map项映射到另一个Map
  2. 将所有Map条目收集到一个Map
  3. 如果weightedPaths[node]null则返回一个空Map 。 这使得评估表达式的类型成为不可为null的Map ,然后可以将其组成。 否则,我们将无法编译下一行,因为-无法在可为null的类型上调用

到那时,源代码具有函数式编程的所有功能:不变性和递归。

结论

从命令式迁移到功能式主要基于:

  • 通过递归替换循环
  • 将可变状态移动到累加器参数和返回值

如果不习惯函数式编程,那么简单的步骤会很有帮助。

这篇文章的完整源代码可以在Github上找到。

翻译自: https://blog.frankel.ch/imperative-functional-programming/4/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值