Engee 文档
Notebook

C++重载与Julia multimethods的比较

Julia语言非常擅长重用代码来处理不同的数据类型,这使您可以轻松定义新类型并使用现有函数来处理它们,或者为现有类型定义新函数。 允许实现此目的的主要机制称为multimethods,或multiple dispatch

在这个例子中,我们将比较Julia multimethod机制和来自"pure OOP languages"的更熟悉的函数重载机制,并尝试演示其优势。

重载函数和调度参数

函数重载机制的出现是为了使程序员的工作更容易。 多亏了他,平方根函数 sqrt 或加法运算符 + 它可以针对不同的数据类型重新定义,并根据参数列表强制执行不同的操作。 这种机制在"纯OOP语言"(Python,C++或Delphi)中使用,其中重载函数和运算符是静态多态的工具。

*没有dispatching(C)的语言允许您只有一个函数实现,也就是说,它们不允许您根据传递给它的参数通过相同的名称调用不同的函数。
*单个或动态调度语言(C++,Python。..)他们依靠其中一个参数来确定被调用的函数的具体实现(第一,第二。..).
*具有多个调度(Lisp,Julia)的语言根据列表中的所有参数(第一个和第三个,或优先级的任何组合)选择函数的特定实现。

乍一看,Julia的多重调度看起来像函数重载,但实际上工作方式不同。 我们建议深入研究一个示例,该示例应显示多重调度机制的工作原理。

Julia中的实施

在这个例子中,我们将省略类型和字段的英文名称,以简化与C++源代码的比较,我们也将在稍后编写。

首先,我们创建一个抽象类型。 Pet 我们从它继承类型。 DogCat. 我们不能设置字段 name 一个抽象类型,所以我们为每个类设置这个字段,允许一些重复的代码。 然后我们定义一个通用函数 encounter,其描述了两种动物的相互作用。 首先,她发现哪个动词适用于这种交互,调用另一个通用函数。 meets,其尚未确定。

In [ ]:
abstract type Pet end

struct Dog <: Pet; name::String end
struct Cat <: Pet; name::String end

# Мы можем определить метод для работы с данными родительского типа,
# но в этой нашей демонстрации он не будет вызываться
meets( c::Pet, d::Pet ) = "GENERIC"

function encounter( a::Pet, b::Pet )
    verb = meets(a, b)
    println( "$(a.name) встречает $(b.name) и $verb" )
end
Out[0]:
encounter (generic function with 1 method)

最后一步是函数 encounter 打印事件报告:哪些动物遇到并替换必要的动词。

我们正在创建函数的四个变体 meets 使用不同的参数,我们定义将插入到最终报告(函数)中的不同动词 encounter 我们还有一个)。 根据两种动物的类型,动词会有所不同。

In [ ]:
meets( a::Dog, b::Dog ) = "обнюхивает"
meets( a::Dog, b::Cat ) = "прогоняет"
meets( a::Cat, b::Dog ) = "шипит"
meets( a::Cat, b::Cat ) = "подкрадывается"
Out[0]:
meets (generic function with 6 methods)

然后我们创建对象(特定宠物)的实例,然后调用函数 encounter 以他们为论据。

In [ ]:
fido = Dog( "Fido" )
rex = Dog( "Rex" )
whiskers = Cat( "Whiskers" )
spots = Cat( "Spots" )
Out[0]:
Cat("Spots")

请注意,Julia中的抽象类型仅用于创建类型之间的依赖关系图。 字段不能在其中声明。 可能是什么原因?:

  1. 可能是Julia继承了一个类型,而不是一个类。
  2. 或者说,如果你在抽象类中声明一个字段,那么子类将无法摆脱它。
  3. 或者,当在继承类型中定义字段时,代码变得更加自我记录。

C++中的实现

让我们在C++中实现相同的教科书示例。 它的代码在文件中 pets.cpp.

我们立即看到一个优点:在C++中,您可以向抽象类型添加字段。 然后我们必须定义方法 meets 因为没有它,编译器会给我们一个错误。 我们不能重新定义一个尚未定义的函数。 没有它,编译器返回错误。 In function 'void encouter(Pet,Pet)': error: 'meets' was not declared in this scope.

``'C++
类宠物{
公众人士:
字符串名称;
};

string meets(Pet a,Pet b)

虚空相遇(宠物a,宠物b){
字符串动词=满足(a,b);
cout<<a.name <<"遇见"<<b.name <<"和"<<动词<<endl;
}


然后我们创建子类型。 DogCat 无需在其定义中指定字段 name 因为抽象类有它。 下面我们重新定义了方法,但确切地说,这些不是方法,而是重载函数。

``'c++
类狗:公共宠物{};
类猫:公共宠物{};

string meets(Dog a,Dog b){return"sniffs";}
string meets(Dog a,Cat b){return"drives away";}
string meets(Cat a,Dog b){return"hisses";}
string meets(Cat a,Cat b)


和功能内部 main 我们创建对象的所有实例,给它们命名,并确定调用时会发生什么。 encounter 以不同的宠物为论据。

``'c++
int主(){
狗fido;fido.name ="Fido";
狗雷克斯;fido.name ="雷克斯";
猫须;whiskers.name ="胡须";
猫斑;spots.name ="斑点";

遭遇(菲多,雷克斯);
遭遇(fido,须);
遭遇(胡须,雷克斯);
遭遇(胡须,斑点);

返回0;

}


让我们编译这段代码。 结果,我们应该得到一个二进制文件。 pets,从而可以运行并且可以接收输出。

In [ ]:
;g++ pets.cpp -o pets

实现的比较

现在让我们比较一下多重调度机制和它设计开发的功能重载机制的操作。

1. Julia中的实施

让我们使用Julia函数设置几个"会议" encounter 让我们看看输出。

In [ ]:
encounter( fido, rex )
encounter( fido, whiskers )
encounter( whiskers, rex )
encounter( whiskers, spots )
Fido встречает Rex и обнюхивает
Fido встречает Whiskers и прогоняет
Whiskers встречает Rex и шипит
Whiskers встречает Spots и подкрадывается

一切都如我们所料。

2. C++中的实现

让我们运行编译成二进制文件的代码。 pets.

In [ ]:
;./pets
Fido встречает Rex и GENERIC
Fido встречает Whiskers и GENERIC
Whiskers встречает Rex и GENERIC
Whiskers встречает Spots и GENERIC

为什么会这样

这里的瓶颈是功能 encounter( Pet a, Pet b ). 我们可以重新定义函数 encounter 对于所有可能的参数:

``'c++
void encounter(狗a,猫b){
字符串动词=满足(a,b);
cout<<a.name <<"遇见"<<b.name <<"和"<<动词<<endl;
}


但这只会表明我们不能继承新数据类型的行为。 要在C++中实现动态多态,使用了虚拟方法,但也需要在每个子类中重新定义它们,这也不会简化代码继承。

结论

函数重载机制不允许使用动态定义的类型,所有类型都是静态定义的。 Julia使用多重分派机制来解决继承问题,我们已经证明Julia中的继承对于父数据类型和子数据类型都非常有效。

这个例子的灵感来自Julia语言的创建者之一的演示文稿。: The Unreasonable Effectiveness of Multiple Dispatch | Stefan Karpinski | JuliaCon 2019.