作者丨ErikEngheim 译者|重点是科学计算这个比较小众的领域,所以关注度不如Python等流行语言。然而,这些事实都无法掩盖Julia在科学计算领域的巨大优势。 多重分派是Julia编程语言的杀手级特性,但很少有开发人员听说过它,更不知道它是什么以及它是如何工作的。这并不奇怪,因为很少有语言支持多重分派,而那些支持多重分派的语言往往会很好地隐藏它。所以,在我谈论多重分派的力量之前,我必须先解释一下它是什么。 先给大家提示一下:跟函数的调用方式有关系,我们退一步详细解释一下。 程序运行遇到函数调用,必须想办法跳转到代码处执行。在某些过程式编程语言(如C或Pascal)中,这个过程很简单。每个函数都被实现为一个在内存中具有唯一位置的子程序,调用一个函数只是简单地跳转到子程序的内存地址并执行每条指令,直到处理器遇到返回指令。 在处理函数指针时,事情变得有点棘手。我们跳转到的子程序可以在运行时更改,因为代码允许更改存储在函数指针中的子程序地址。我为什么要提到这些细节?因为我想表达的是,调用一个函数并决定执行什么代码并不总是一件微不足道的事情。 考虑在面向对象编程中调用方法的复杂性。1.warrior.attack(knight) 比如我们定义一个类Warrior,名字叫“Warrior”。Warrior类中的成员函数attack没有对应指定内存地址的子程序。当在warrior对象上调用attack方法时,决定跳转到哪个子程序的复杂过程开始。我们必须确定Warrior类的哪个实例正在调用攻击方法。您可以想象为不同类型的层次结构(例如弓箭手、枪手或骑士)实例化“战士”类的对象。上图是不同属性的“战士”类对象的类型结构 由于弓箭手的攻击方式与枪兵或骑士不同,所以不同的“战士”对象的攻击方式不同.通过称为单一分派的过程,我们决定调用哪个方法。从底层的角度来看,我们试图确定在执行warrior.attack(knight)语句时跳转到哪个子程序。 单一分派的工作方式取决于我们谈论的是动态类型语言还是静态类型语言。让我们看看它在动态类型语言中是如何工作的,因为我们将把这个过程与Julia进行比较,它也是一种动态类型语言。 假设我们有两个Warrior类的实例,warriora和warriorb,warriora正在攻击warriorb。我们的第一步是确定a是什么类型的战斗机。在动态类型语言中,每个类对象都知道它的类型是什么。以Object-C语言为例,每个对象都有一个名为“isa”的属性,它指向一个类对象,用来描述当前对象是什么类型。在下图中,我们模拟了这个过程。战士是Archer类的实例化对象。Archer类包含每个已实现方法的函数指针。为了找到正确的方法,我们对“攻击”方法执行字典查找。动态类型语言使用singledispatch来定位要执行的代码 上图中方法名末尾的感叹号可能看起来很奇怪。别担心,这只是一种命名约定,在Lisp和Julia中流行,用于更改函数。它没有语义。 严格来说,在大多数动态语言中谈论函数指针是错误的。例如,在Ruby中,您实际上并不指向任何带有机器代码的子例程,而是指向由解析方法生成的抽象语法树(AST)。Ruby解释器解释AST以运行方法中的代码。y=4*(2+x)的语法树(AST) 我们刚才讲的叫做singledispatch(单分派),因为我们是根据单个对象来决定调用什么方法的。对象b的类型不会以任何方式影响方法查找过程。相比之下,对于多重分派,函数调用中的每个参数都在决定调用哪个方法方面发挥作用。我知道这听起来很奇怪,所以让我通过解释单次分派的问题来激励您使用多分派。多重分派解决什么问题? 我们写了一场战斗!Julia中的函数,通过调用attack!来模拟两个战士a和b的战斗!函数,并根据结果打印出信息。下面的大部分代码都是不言自明的。在Julia中,我们使用::将变量名与变量类型分开。因此,在示例代码中,a::Warrior告诉Julia这场战斗!函数有一个名为Warrior类型的参数。1.functionbattle!(a::Warrior,b::Warrior)2.attack!(a,b)3.ifa.health==0&&b.health==04.println(a.name,"and",b.name,"destroyedeachother")5.elseifa.health==06.println(b.name,"defeated",a.name)7.elseifb.health==08.println(a.name,"defeated",b.name)9.else10.println(b.name,"survivedattackfrom",a.name)11.end12.end 看上面的代码问你自己一个如此简单的问题:类似的代码在C++或Java中有效吗?乍一看,这似乎是可能的。两种语言都允许你定义多个同名但参数不同的函数,你可以像下面这样的Julia代码编写代码:1.functionattack!(a::Archer,b::Archer)2.ifa.arrows>03.shoot!(a)4.damage=6+rand(1:6)5.b.health=max(b.health-damage,0)6.结束7.a.健康,B。health8.end9.10.functionattack!(a::Archer,b::Knight)11.ifa.arrows>012.shoot!(a)13.damage=rand(1:6)14.ifb.mounted15.damage+=316.end17.b.health=max(b.health-damage,0)18.结束19.a.健康,B。健康20.结束21.功能攻击22!(a::Knight,b::Knight)23.a.health=max(a.health-rand(1:6),0)24.b.health=max(b.health-rand(1:6),0)25.a.health,b.health26.end 代码细节不重要。我想让你从这个代码示例中理解的是,我们定义了三种攻击!功能。每个定义都接受不同类型的参数。在C++和Java中,我们称之为函数重载。在编译时,编译器将通过在调用点检查每个输入参数的类型来选择要调用的适当函数。 重点是:C++编译器是不可能猜到是哪种攻击的!函数被战斗调用!函数,因为它不知道实参a和b的具体类型。编译器只知道这两个参数都是Warrior类型的某个子类型。具体是哪个子类型,只有在代码真正运行起来的时候才能确定。这很遗憾,因为函数重载只在编译时起作用。 在这种情况下,多重分派可以做单分派和函数重载都做不到的事情:它可以在运行时根据参数a和b的类型选择正确的代码。多重分派是如何工作的? 还记得单次调度是如何通过在运行时找到正确的方法来完成的吗?Multipledispatch也是讲如何选择正确的方法。攻击!你刚才看到的definition其实不是函数定义,而是方法定义。定义攻击时!功能,你可以写:功能攻击!end 为什么没有参数?因为在Julia中函数没有参数,所以只有方法有参数。与面向对象的语言不同,Julia中的方法附加到函数而不是类。 因此,Julia中的函数调用首先通过查找被调用的函数来执行。Julia为每个函数注册了一个方法表。从上到下搜索此表以查找接受与函数调用站点提供的输入参数类型相匹配的参数类型的方法。Julia如何使用多重分派 来定位要在调用函数时执行的正确代码1。当Julia文件加载到内存中时,每个方法的源代码都会被解析并转换为抽象语法树(AST)。 2。每个方法的AST存储在正确函数的正确方法表中。 3。在运行时,当一个方法被定位时,我们首先获得AST,它被JIT编译器转换成机器码并缓存起来供以后查找。 这个过程实际上比我在这里展示的要复杂得多。如您所见,抽象语法树可以非常通用。它可以是为数字参数定义的计算。无论参数是16位无符号整数还是32位有符号整数,执行的计算都是相同的。但是,这些情况的汇编代码看起来不同。因此,同一个AST可以产生多个机器码子程序。Julia将在方法表中为每个案例添加一个条目。因此,方法表不限于为其编写源代码的方法的数量。是什么让Julia的multipledispatch独一无二 每次调用Julia中的函数时,都会执行一次方法查找。或者更确切地说,从Julia开发人员的角度来看,这就是它的样子。代码每次都这样运行。 在其他支持multipledispatch的语言中,不是这样的。只有以特殊方式标记的函数才使用多重分派。否则,将执行常规函数调用。为什么其他语言限制使用multipledispatch?因为在Julia来之前,multipledispatch很慢。 不难想象为什么多重调度会变慢。您可能希望O(N)时间复杂度用于通过大型表进行线性搜索,而不是O(1)用于恒定时间内的单个字典查找。函数可以有一个巨大的方法表。 Julia是如何规避这个问题的?Julia的设计理念是尽可能保持类型稳定。在像Python或JavaScript这样的语言中,情况并非如此。可以在运行时添加或删除字段和方法。可以更改各个字段的类型。在Julia中,类型被设计得更加固定。在定义复合类型时,需要固定字段的数量和类型。 这种设计选择如何影响多重调度?这意味着JuliaJIT编译器完成的代码分析变得更加容易。代码的行为变得更加可预测,这使得识别更多情况成为可能,调用函数时应该定位的方法变得完全确定和可预测。请记住,如果函数调用的参数类型保持不变,Julia将始终寻找相同的方法。如果代码分析可以确定一个函数的哪些参数永远不会改变,那么JIT编译器可以用直接函数调用代替多分派查找。如果代码很短,甚至可以内联。 因此,Julia成功地将一开始的性能劣势转变为性能优势。因此,Julia函数调用通常比面向对象语言中的单分派调用快得多。 一旦你达到闪电般的速度,在你的编码风格发生变化的任何地方使用多重分派。始终如一地维护多重分派对Julia社区的软件工程实践产生了深远的影响。通过多次分派重用代码 面向对象语言的用户通过继承类和实现接口来重用代码,这允许将新代码插入到现有框架中。Julia的方法是在函数级别进行复用。不同的开发人员都可以向同一个函数添加方法。我们不扩展类,我们扩展功能。因为函数存在于较低的粒度级别,所以我们有更多的代码重用机会。 这种灵活性的一个简单示例是Julia标准库中定义的show函数。Julia使用它来在不同的上下文中显示一个值。上下文可以是REPL(交互式命令行)、笔记本或IDE环境。可以在show函数中加入匹配以下两个签名的方法:1.show(io::IO,mime,x)2.show(io::IO,x) io对象表示用来显示的值x目标。io可以是控制台窗口、文件、文本字符串、套接字或图形显示。值x可以是简单的数字、日期、文本字符串或更复杂的对象,例如字典或数组。 与面向对象的编程语言不同,您可以沿多个维度扩展显示功能。您可以将show方法添加到全新的IO子类型,以在新的上下文中显示现有的值类型。假设我们创建特殊类型来表示温度单位摄氏度、华氏度和开尔文。可以添加一种方法来显示,以便以正确的单位显示表示温度的数字。 请注意,在Julia中,您可以使用等号来定义单行函数。1.show(io::IO,t::Celsius)=print(io,t.value,"°C")2.show(io::IO,t::Fahrenheit)=print(io,t.value,"°F")3.show(io::IO,t::Kelvin)=print(io,t.value,"K") 要理解为什么这个扩展机制如此强大,请允许我指出解决一些当您尝试使用面向对象编程复制此扩展机制时遇到的问题。你设计了一个系统,其中每个对象都必须实现一个show方法才能显示,但这种选择会导致几个问题: 所有类都必须继承一个具有show方法的基类。 每个对象将在每个IO对象类型上获得相同的表示。 也就是说:许多面向对象的系统最终都会有过于复杂的基类。原因是您希望每个对象支持太多功能: 在不同的上下文中可视化对象,例如在调试器中 打印或存储到文件文本表示 允许使用对象在使用哈希函数的集合中 例如,您可以在Java和Objective-C中找到这种模式。这种方法是死板的。如果基类设计错误,将对所有相关代码造成严重后果。 更不用说,如果语言设计者忘记添加show方法,就没有简单的方法来改进它。只有对标准库的更新才能修复它。作为第三方开发人员,您无法改造解决方案。反之,如果Julia标准库没有定义show函数,你可以很容易的自己定义,发布一个实现公共对象可视化的库,你可以分发给其他人。 u和v是向量,A到F是点。表示点之间差异的向量。u是F点和E点之间的差值。 让我们多谈谈I/O系统。假设您已经创建了一个名为Vector2D的二维矢量类型。在控制台中使用时,如果I/O对象表示图形显示,您可能希望将向量显示为[4,8]和箭头。这两个选项在Julia中都是可能的,因为您可以为io参数是图形显示而x参数是二维向量的情况编写专门的方法。相比之下,面向对象的语言只能根据io或x的类型来选择执行哪个方法,而不能两者兼而有之。请记住,对于单次分派,在运行时调用的方法是根据单个参数的类型而不是多个参数的类型来选择的。 当然,您可以加入一个switch-case语句来处理不同的类型,但那是不可扩展的。每次添加新类型时,都必须修改switch-case语句。这将阻止您将代码作为可重用库进行分发。库用户不应修改第三方库的源代码来扩展它。multipledispatch 模拟不同类型战斗机之间的战斗或编写I/O系统的效用当然只是可以简化编码的少数情况。当我在视频游戏中编写碰撞检测代码时,它首先发现我需要这样的东西。不同的游戏对象由不同的几何形状表示。问题是计算两个圆、两个正方形或一个圆和一个完全不同的正方形的交集。您不能仅通过查看一个参数来决定使用哪种算法,您需要两个参数。如果没有多重分派,您的解决方案将变得混乱。Multipledispatch天生适合组合不同的几何对象 Multipledispatch也很适合任何数值工作。数字运算通常是二进制的。仅仅通过查看第一个数字的类型来决定如何组合两个数字没有多大意义。 总之,multipledispatch就像一把瑞士军刀:它帮助程序运行得更快,让你优雅地解决很多问题,并提供代码重用的高级手段。这听起来可能有点夸张,但我坚信多重分派将定义未来的编程范式。译者简介卢新望,社区编辑,编程语言爱好者,对数据库、架构、云原生有浓厚兴趣。原文链接:https://itnext.io/what-makes-julia-unique-f3ad184fa4a2
