函数的组成部分
mojo中函数由5个部分组成,分别是函数名,泛型参数,参数、返回值和函数体。
scss
def function_name[ parameters ...]( arguments ...) -> return_value_type:
function_body
泛型参数
由方括号定义的部分类似于其他语言的泛型参数,如果上层调用这个函数式,在泛型参数的部分传入了不同的值,mojo编译器会在编译阶段就生成多个函数。这是mojo实现零成本抽象的一个方法。
由于这个特性,在调用泛型参数的时候,不可以使用局部变量。如果使用了局部变量,编译会报错。
但是这个参数又跟其他语言的泛型不太一样,rust语言中,在调用方调用函数是,需要给泛型指明类型;但是在mojo中,调用是需要传入具体的值。
"泛型参数"四个字是我自己的翻译,英文原文是"parameter",一旦后续官方出版了权威的翻译方案,就以官方的翻译方案为准。

图1:使用局部变量传入泛型参数会报错。

图2:传入字面量就可以正常编译通过。
泛型参数可以结合trait使用泛型约束。
参数
python
def my func(pos_only,/, pos_or_keyword,*, keyword_only):
pass
def my func2(*names,**attributes):
pass
mojo语言中有两种函数声明方式,不同的声明方式,对于参与要不要声明类型要求不同,下文会详述。
mojo语言的函数参数支持可选参数,位置参数,命名参数,可变参数等概念。这些概念基本与python语言中的定义一致。
可选参数
csharp
fn my_pow(base: Int, exp: Int=2)-> Int:
return base** exp
fn use_defaults():
# Uses the default value for `exp`
var z= my_pow(3)
print(z)
- 如果一个参数赋予了默认值,则在上层调用这个函数的时候可以不传递这个参数。如果不传递,就会以默认值为基准进行计算。
- 可选参数只能位于必须参数之后。
- 可选参数不能是mut的
命名参数
csharp
fn my_pow(base: Int, exp: Int = 2) -> Int:
return base ** exp
fn use_keywords():
# Uses keyword argument names (with order reversed)
var z = my_pow(exp=3, base=2)
print(z)
如果在调用参数时指明了参数名,则参数的顺序不需要一定与定义时的顺序一致。
可变参数
sql
fn sum(*values: Int) -> Int:
var sum: Int = 0
for value in values:
sum = sum + value
return sum
fn main():
var result:Int = sum(1,2,3,4,5);
print(result)
在参数前面加一个星号,则上层调用这个函数的时候,可以传递任意多个同类型的元素。声明的参数变量会被视作一个List数组。
可变参数后面定义的参数只能以命名参数的形式出现。
目前这个List 存储的是Int这一类的寄存器存储类型的处理逻辑,和存储的String这一类内存存储类型的表现会有所不同。(我理解就是传值类型和传引用类型)。
传值类型(如Int)在for in 循环中,拿到的是一个具体的value:
mojo
fn sum(*values: Int) -> Int: var sum: Int = 0 for value in values: sum = sum+value return sum
传引用类型(如String)在for in循环中,拿到的是一个指针,需要用一对空的方括号解引用:
less
def make_worldly(mut *strs: String): # Requires extra [] to dereference the pointer for now. for i in strs: i[] += " world"
这个差异会在后期抹平。
不同类型的可变参数需要借助trait + 泛型约束来实现。
命名可变参数
在参数前面加两个星号,可以将参数转换为命名可变参数,上层调用的命名参数,会被当做一个dict来处理。
php
fn print_nicely(**kwargs: Int) raises:
for key in kwargs.keys():
print(key[], "=", kwargs[key[]])
# prints: # `a = 7` # `y = 8`
print_nicely(a=7, y=8)
不过这个特性有以下这些限制:
- 命名可变参数始终会以owned(所有权描述,类似于Rust 的 move,后文详述)的形式传递,所以是用read方式来传递会报错。
- 可变命名参数所有的值必须是同一个类型。
- 所有的值必须得同时是 movable的和copyable的(跟所有权机制相关,后文详述)。
- python的解包功能还不支持
- 泛型部分还不支持命名可变参数
位置参数和命名参数的分界
可以在参数列表里加入一个 / 或者* 来分界,/ 符号之前的参数必须是位置参数,* 后面的参数必须是命名参数。
kotlin
fn my_function(a:Int,b:Int,/,c:Int,d:Int,*,e:Int,f:Int) -> Int:
pass
my_function(1,2,3,4,f=5,e=60);
my_function(1,2,c=3,d=4,f=5,e=6);
def 声明和 fn 声明
mojo语言有两种函数声明方法:def和fn。这两种方法没有本质的区别,def 声明更多是为了兼容python的旧语法,相对来说限制比较少;而 fn 语法更像是python语法与rust语法的一种融合,结合了所有权机制,会有比较强的限制和校验,但是能带来更好的安全性和性能。
异同点
def | fn |
---|---|
函数参数并不强制声明类型,未声明类型的参数会以Object类型来传递,下文详述。 | 必须声明类型(除了方法里的self) |
不必声明返回类型,默认以Object返回 | 必须声明返回类型(除非返回的是void),如果不指定返回类型,默认返回NOne |
函数参数默认以可变(mut)的形式传递- 如果是引用类型的参数,会以可变引用的形式传递 | |
如果是值类型的参数,会以值类型传递 | 参数默认以只读引用的方式传递 1. 如果需要修改这个值,需要显式的声明mut |
不需要显式的声明 raises | 需要显式的声明raises |
object type
object type需要在运行时进行类型推断,所以如果错误使用object type会导致运行时异常。
所以object type无论在安全性和性能上都逊色于其他类型,应尽量避免使用。
错误处理
在def 函数中,函数不需要显示的声明raise。如果一个错误在一个函数内没有得到处理,这个错误就会冒泡到上一层的函数中进行处理,上一层如果也没处理,就会继续向上冒泡。如果main函数也没处理,进程就会退出。
在mojo语言的错误处理机制中,一个fn函数要么通过try catch捕获这个error,要么需要显式的声明错误,让上层处理。
此部分后文详述。
方法重载
def
对于def 函数来说,由于不需要在一开始就指定参数的类型和参数的数量,所以完全没有必要实现方法的重载。这一点也类似于python。
fn
类似于java,同一个方法可以针对不同的参数类型和参数数量,可以用不同的逻辑实现。
rust
fn add(x: Int, y: Int) -> Int:
return x + y
fn add(x: String, y: String) -> String:
return x + y
如果对同一个函数进行了重载定义,那调用时,mojo编译器会自动选择一个最接近实参情况的函数去调用。
之所以是最接近而不是"完全等于",是因为mojo编译器支持隐式的类型转换。所以一个函数的参数是Float,传入一个Int也是可以正常工作的。
而如果mojo无法选出来一个明确的函数执行,就会报出一个编译时错误。
less
@value
struct MyString:
@implicit
fn __init__(out self, string: StringLiteral):
pass
fn foo(name: String):
print("String")
fn foo(name: MyString):
print("MyString")
fn call_foo():
alias string: StringLiteral = "Hello"
# foo(string) # error: ambiguous call to 'foo' ... This call is ambiguous because two `foo` functions match it
foo(string)
上述代码定义了两个foo函数,一个接受string,另一个接受MyString。而这两个类型都接受StringLiteral的隐式转化,所以mojo在编译时无法确定该选用哪个函数,此时会报错。
重载的选择
mojo在决定使用哪个函数时,不会考虑返回值以及调用测的上下文信息,只会考虑泛型参数和参数的类型。
有以下几条规则:
- 优先考虑需要最少数量的类型隐式转换的函数
- 优先考虑没有可变参数的函数
- 优先考虑没有可变泛型参数的函数
- 优先考虑具有最短签名的函数
- 优先考虑非静态函数
返回值
在函数签名的末尾,返回值以 -> 的形式声明。
rust
fn test_fn() -> String:
pass
函数的返回值默认以 owned (跟所有权机制有关,后文详述)的形式传递给上层,并且有可能进行隐式类型转换。
out 命名返回值
我本来以为这个特性只是一个语法糖,就像golang语言可以给返回值指定名称以不弄混一样。
但我发现其实这个特性意义重大。
mojo的函数返回值不仅可以通过 -> 来声明,也可以通过一个有名称,并且标注了 out 的参数来实现。
kotlin
fn my_test_function(owned a:Int,out res:Int):
res = a + 10
以这段代码为例,res被声明了out。此时res是一个未赋值的变量,需要遵循未赋值变量(后文详述)的使用规范。
在声明了out 参数的函数里,不需要显式的编写return语句来进行返回,函数会在运行结束时自动将 res 的值返回给上层函数。
特殊之处在于,out声明的变量的所有权是属于上层函数的,所以out 参数可以实现一些不可移动不可复制的变量的传递。
arduino
def create_immovable_object(owned name: String) -> ImmovableObject :
var obj = ImmovableObject(name^)
obj.name += "!"
return obj
上述这段代码是用 -> 来声明返回值的,所以这个函数在编译时会报错。因为 obj 是这个函数的内部变量,mojo 的内存管理类似于rust的内存管理,在函数运行结束后,函数的局部变量会释放。所以上层拿到这个参数会有内存风险,所以编译器不允许通过。
arduino
def create_immovable_object(owned name: String, out obj: ImmovableObject):
obj = ImmovableObject(name^)
obj.name += "!"
# obj is implicitly returned
这段代码是用out 声明了一个obj,这个obj在声明之处,所有权就是属于上层函数的。所以函数执行结束后,这个变量的内存并不会释放,而是会给到上层函数,就可以避免移动和复制的问题了。
错误声明
对于 def 函数来说,如果函数内部发生了一个错误,首先要看这个函数内部有没有捕获处理这个错误。如果没有捕获处理,函数会中断执行并且把错误抛给上层函数处理。如果上层函数也没有处理,就会继续向上层抛错误,直到main函数。
而对于 fn 函数来说,一个函数想要把自己内部的错误抛给上层函数,必须得显式的声明。如果它不声明,就必须得在自己内部捕获并且处理这个错误。
这样就意味着,对于fn函数来说,一个错误要么需要声明向上层抛,要么需要声明在内部处理。
php
# This function will not compile
fn unhandled_error():
raises_error() # Error: can't call raising function in a non-raising context
fn handle_error():
try:
raises_error()
except e:
print("Handled an error," e)