Mojo🔥学习笔记(1)—— 函数

函数的组成部分

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)
  1. 如果一个参数赋予了默认值,则在上层调用这个函数的时候可以不传递这个参数。如果不传递,就会以默认值为基准进行计算。
  2. 可选参数只能位于必须参数之后。
  3. 可选参数不能是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在决定使用哪个函数时,不会考虑返回值以及调用测的上下文信息,只会考虑泛型参数和参数的类型。

有以下几条规则:

  1. 优先考虑需要最少数量的类型隐式转换的函数
  2. 优先考虑没有可变参数的函数
  3. 优先考虑没有可变泛型参数的函数
  4. 优先考虑具有最短签名的函数
  5. 优先考虑非静态函数

返回值

在函数签名的末尾,返回值以 -> 的形式声明。

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)
相关推荐
iOS大前端海猫11 小时前
Swift 中的async和await
ios·编程语言
这里有鱼汤20 小时前
Python数据结构深入讲解:列表、字典、元组,彻底搞懂!
后端·python·编程语言
Mirageef2 天前
aardio 简单交互程序
编程语言
Mirageef5 天前
aardio内置函数
编程语言
楽码5 天前
只需一文!深入理解闭包的实现
后端·go·编程语言
神经星星7 天前
【TVM教程】在支持 CMSIS-NN 的 Arm(R) Cortex(R)-M55 CPU 和 Ethos(TM)-U55 NPU 裸机上运行 TVM
深度学习·开源·编程语言
Mirageef7 天前
aardio函数返回值
编程语言
DeepLink8 天前
Python小练习系列:用装饰器记录函数执行时间
python·编程语言
Mirageef8 天前
aardio条件判断
编程语言