2.13. 函数

函数指针是第一类值,如整数或字符串,可以存储在表槽、局部变量、数组中,并作为函数参数传递。 函数本身是声明(很像 C++)。

2.13.1. 函数声明

函数类似于大多数其他类型化语言中的函数:

def twice(a: int): int {
    return a+a
}

完全空的函数(无参数)也可以声明:

def foo {
    print("foo")
}

//同上
def foo() {
    print("foo")
}

Daslang 始终可以推断函数的返回类型。 返回不同的类型是编译错误:

def foo(a:bool) {
    if ( a ) {
        return 1
    } else {
        return 2.0  // error, 期望为 int
    }
}

2.13.1.1. 公开

函数可以是 privatepublic

def private foo(a:bool)

def public bar(a:float)

如果未指定,则函数继承 module public(即在公共模块中,函数是 public,而在私有模块中,函数是 private)。

2.13.1.2. 函数调用

您可以通过使用函数的名称并传入其所有参数来调用函数(可能省略默认参数):

def foo(a, b: int) {
    return a + b
}

def bar {
    foo(1, 2) // a = 1, b = 2
}

2.13.1.3. 命名参数函数调用

你也可以通过使用函数的名称并传递具有显式名称的所有 aits rgument 来调用函数(可能省略 default 参数):

def foo(a, b: int) {
    return a + b
}

def bar {
    foo([a = 1, b = 2])  // 与 foo(1, 2) 相同
}

命名参数应保持相同的顺序:

def bar {
    foo([b = 1, a = 2])  // 错误, 乱序
}

命名参数调用提高了被调用方代码的可读性,并确保了现有函数重构的正确性。 它们还允许最后一个参数以外的参数使用默认值:

def foo(a:int=13, b: int) {
    return a + b
}

def bar {
    foo([b = 2])  // 与 foo(13, 2) 相同
}

2.13.1.4. 函数指针

指向函数的指针使用与 block 或 lambda 的声明类似的声明:

function_type ::= function { optional_function_type }
optional_function_type ::= < { optional_function_arguments } { : return_type } >
optional_function_arguments := ( function_argument_list )
function_argument_list := argument_name : type | function_argument_list ; argument_name : type

function < (arg1:int;arg2:float&):bool >

可以使用 @@ 运算符获取函数指针:

def twice(a:int) {
    return a + a
}

let fn = @@twice

当多个函数具有相同的名称时,可以通过显式指定 signature 来获取指针:

def twice(a:int) {
    return a + a
}

def twice(a:float) {  // 当需要此
    return a + a
}

let fn = @@<(a:float):float> twice

函数指针可以通过 invoke 或通过调用表示法调用:

let t = invoke(fn, 1)   // t = 2
let t = fn(1)           // t = 2, same as

2.13.1.5. 无名函数

可以使用类似于 lambda 或块的语法创建指向无名函数的指针 (参阅 Blocks):

let fn <- @@ ( a : int ) {
    return a + a
}

无名本地函数根本不捕获变量:

var count = 1
let fn <- @@ ( a : int ) {
    return a + count            // 编译错误,找不到变量计数
}

在内部,将生成一个常规函数:

def _localfunction_thismodule_8_8_1`function ( a:int const ) : int {
        return a + a
}

let fn:function<(a:int const):int> const <- @@_localfunction_thismodule_8_8_1`function

2.13.1.6. 泛型函数

泛型函数类似于 C++ 模板化函数。 Daslang 将在编译的 infer pass 期间实例化它们:

def twice(a) {
    return a + a
}

let f = twice(1.0)  // 2.0 float
let i = twice(1)    // 2 int

泛型函数允许使用类似于 Python 或 Lua 等动态类型语言的代码。 同时仍然享受强静态类型的性能和稳健性。

无法获取泛型函数地址。

未指定的类型也可以通过 auto 表示法来编写:

def twice(a:auto) {   // 与上面的 'twice' 相同
    return a + a
}

泛型函数可以专门化泛型类型别名,并将它们用作声明的一部分:

def twice(a:auto(TT)) : TT {
    return a + a
}

在上面的例子中,别名 TT 用于强制执行返回类型 Contract。

类型别名可以在相应的 auto 之前使用:

def summ(base : TT; a:auto(TT)[] ) {
    var s = base
    for ( x in a ) {
        s += x
    }
    return s
}

在上面的例子中, TT``是从传递的数组 ``a 的类型推断出来的,并期望作为第一个参数 base 。 返回类型是从 s``的类型推断出来的,它也是 ``TT

2.13.1.7. 函数重载

如果函数的参数类型不同,则可以将其专用化:

def twice(a: int) {
    print("int")
    return a + a
}
def twice(a: float) {
    print("float")
    return a + a
}

let i = twice(1)    // prints "int"
let f = twice(1.0)  // prints "float"

声明具有相同精确参数列表的函数是编译时错误。

函数可以部分专用:

def twice(a:int) {      // int
    return a + a
}
def twice(a:float) {    // float
    return a + a
}
def twice(a:auto[]) {   // any array
    return length(a)*2
}
def twice(a) {          // 任何其他情况
    return a + a
}

Daslang 使用以下规则来匹配部分专用函数:

  1. Non-autoauto 更专业。

  2. 如果两者都是非 auto,那么没有 cast’ 的那个更专业。

  3. 有数组的比没有数组的更专业。如果两者都有一个数组,则具有实际值的那个比没有的那个更专业。

  4. 基类型为 autoalias 的 Counties 不太专用。如果两者都是 autoalias,则假定它们具有相同的专用化级别。

  5. 对于指针和数组,将比较子类型。

  6. 对于表、元组和变体,将比较子类型,并且所有子类型都必须相同或同等专业化。

  7. 对于函数、块或 lambda,将比较子类型和返回类型,并且所有子类型和返回类型都必须相同或同等专业化。

在匹配函数时,Daslang 会选择最专业的函数,并按替代距离排序。 如果 LSP 需要强制转换(Liskov 替换原则),则每个参数的替换距离增加 1。 最后,选择距离最短的函数。如果留下多个函数供选取,则会报告编译错误。

函数专用化可以由 Contract (Contract 宏) 限制:

[expect_any_array(blah)]  // array<foo>, [], or dasvector`.... or similar
def print_arr ( blah ) {
    for ( i in range(length(blah)) ) {
        print("{blah[i]}\n")
    }
}

在上面的示例中,将仅匹配数组。

可以对合约进行布尔逻辑运算:

[expect_any_tuple(blah) || expect_any_variant(blah)]
def print_blah ...

在上面的示例中,print_blah 将接受任何 Tuples 或 variant。 可用的逻辑作包括`!`, &&, ||^^

可以通过 explicit 关键字为特定函数参数明确禁止 LSP:

def foo ( a : Foo explicit ) // 将接受 Foo,但不接受 Foo 的任何子类型

2.13.1.8. 默认参数

Daslang 的函数可以具有默认参数。

具有 default 参数的函数声明如下:

def test(a, b: int, c: int = 1, d: int = 1) {
    return a + b + c + d
}

当调用函数 test 且未指定参数 cd 时, 编译器将生成对 unspecified 参数的 default 值调用。默认参数可以是任何有效的编译时 const Daslang 表达式。表达式在编译时计算。

为最后一个参数以外的参数声明默认值是有效的:

def test(c: int = 1, d: int = 1, a, b: int) { // valid!
    return a + b + c + d
}

使用默认参数调用此类函数需要命名参数调用:

test(2, 3)           // 无效调用,缺少 a、b 参数
test([a = 2, b = 3]) // 有效调用

默认参数可以与重载结合使用:

def test(c: int = 1, d: int = 1, a, b: int) {
    return a + b + c + d
}
def test(a, b: int) { // 现在 test(2, 3) 是有效的调用
    return test([a = a, b = b])
}

2.13.2. OOP 样式的调用

Daslang 中没有结构的方法或函数成员。 但是,通过使用正确的管道运算符 |> 可以很容易地编写代码 “OOP 样式”:

struct Foo {
    x, y: int = 0
}

def setXY(var thisFoo: Foo; x, y: int) {
    thisFoo.x = x
    thisFoo.y = y
}
...
var foo:Foo
foo |> setXY(10, 11)   // 这是 setXY(foo, 10, 11) 的语法糖
setXY(foo, 10, 11)     // 与上述行完全相同

(参阅 Structs).

2.13.3. 尾递归

尾递归是一种将程序中的递归部分转换为迭代的方法:当函数中的递归调用是该函数中最后执行的语句时(就在返回之前),它适用。

目前,Daslang 不支持 tail 递归。 这意味着 Daslang 函数始终返回。

2.13.4. 作员重载

Daslang 允许您重载运算符,这意味着您可以在与自己的数据类型一起使用时为运算符定义自定义行为。 要重载运算符,您需要定义一个特殊函数,其中包含要重载的运算符的名称。语法如下:

def operator <operator>(<arguments>) : <return_type>
    # Implementation here

在此语法中, <operator> 是要重载的运算符的名称(例如 +、-、*、/、== 等), <arguments> 是运算符函数采用的参数,<return_type>是运算符函数的返回类型。

例如,下面介绍如何为名为 iVec2 的自定义结构重载 == 运算符:

struct iVec2 {
    x, y: int
}

def operator==(a, b: iVec2) {
    return (a.x == b.x) && (a.y == b.y)
}

在此示例中,我们定义了一个名为 iVec2 的结构体,其中包含两个整数字段(x 和 y)。

然后,我们定义一个 operator== 函数,它接受两个 iVec2 类型的参数(a 和 b)。此函数返回一个 bool 值,该值指示 a 和 b 是否相等。 该实现使用 == 运算符检查 a 和 b 的 x 和 y 分量是否相等。

重载此运算符后,您现在可以使用 == 运算符来比较 iVec2 对象,如下所示:

let v1 = iVec2(1, 2)
let v2 = iVec2(1, 2)
let v3 = iVec2(3, 4)

print("{v1==v2}") // prints "true"
print("{v1==v3}") // prints "false"

在此示例中,我们创建三个 iVec2 对象并使用 == 运算符对它们进行比较。第一次比较 (v1 == v2) 返回 true,因为 v1 和 v2 的 x 和 y 分量相等。 第二个比较 (v1 == v3) 返回 false,因为 v1 和 v3 的 x 和 y 分量不相等。

2.13.5. 重载 ‘.’ 和 ‘?.’ 运算符

Daslang 允许您重载点 .运算符,用于访问 structure 或 class 的字段。 要重载点 .运算符,您需要定义一个名称为 operator . 的特殊函数,语法如下:

def operator.(<object>: <type>, <name>: string) : <return_type>
    # 在此处实现

或者,您可以显式指定字段:

def operator.<name> (<object>: <type>) : <return_type>
    # 在此处实现

在此语法中, <object> 是要访问的对象, <type> 是对象的类型, <name> 是要访问的字段的名称,<return_type>并且是运算符函数的返回类型。

算子?。以类似的方式工作。

例如,以下是重载点 .运算符,用于名为 Goo 的自定义结构:

struct Goo {
    a: string
}
def operator.(t: Goo, name: string) : string {
    return "{name} = {t . . a}"
}
def operator. length(t: Goo) : int {
    return length(t . . a)
}

在此示例中,我们定义了一个名为 Goo 的结构体和一个名为 a 的字符串字段。

然后我们定义两个运算符。功能:

第一个参数采用两个参数 (t 和 name) 并返回一个字符串值,其中包含正在访问的字段或方法的名称 (name) 和 Goo 对象的 a 字段的值 (t.a)。 第二个参数采用一个参数 (t) 并返回 Goo 对象的 a 字段的长度 (t.a)。 重载这些运算符后,您现在可以使用点 .运算符访问 Goo 对象的字段和方法,如下所示:

var g = Goo(a ="hello")
var field = g.a
var length = g.length

在此示例中,我们创建 Goo 结构的实例,并使用点 .运算符。 重载运算符.function 并返回字符串 “world = hello”。 我们还使用点 .运算符。 重载运算符。length 函数,并返回 Goo 对象的 a 字段的长度(在本例中为 5)。

这..语法用于访问结构或类的字段,同时绕过重载作。

2.13.6. 重载访问器

Daslang 允许您重载访问器,这意味着您可以定义自定义行为来访问您自己的数据类型的字段。 下面是一个如何重载名为 Foo 的自定义结构的访问器的示例:

struct Foo {
    dir : float3
}
def operator . length ( foo : Foo ) {
    return length(foo.dir)
}
def operator . length := ( var foo:Foo; value:float ) {
    foo.dir = normalize(foo.dir) * value
}
[export]
def main {
    var f = Foo(dir=float3(1,2,3)))
    print("length = {f.length} // {f}\n")
    f.length := 10.
    print("length = {f.length} // {f}\n")
}

它现在有访问器 length,可以用来获取和设置 dir 字段的长度。

类也允许重载属性的访问器:

class Foo {
    dir : float3
    def const operator . length {
        return length(dir)
    }
    def operator . length := ( value:float ) {
        dir = normalize(dir) * value
    }
}