Java使用Aviator表达式学习记录(十三)匿名函数

这是我参与8月更文挑战的第15天,活动详情查看:8月更文挑战

匿名函数和闭包

匿名函数 lambda

你可以通过 lambda 语法定义一个匿名函数:

## examples/lambda.av

let three = (lambda (x,y) -> x + y end)(1, 2);
println(three);
复制代码

lambda (x,y) -> x + y end 定义了一个匿名函数,接受参数 x 和 y,返回两者的和。

匿名函数的基本定义形式是

lambda (参数1,参数2...) -> 参数体表达式 end
复制代码

定义了匿名函数后,我们直接传入参数 1 和 2,两者相加的结果赋值给 three  变量并打印,也就是结果 3 。

匿名函数可以用赋值给一个变量:

## examples/lambda.av

let add = lambda (x,y) ->
  x + y 
end;

three = add(1, 2);
println(three);
复制代码

我们将上述匿名函数赋值给了变量 add ,其实相当于定义了一个 add 函数,然后调用 add(1, 2) ,同样返回结果 3 。

从 5.2.4 开始,匿名函数的定义也可以用 fn 语法:

let add = fn(x, y) { x + y};

p(add(1, 2));
复制代码

事实上,上一节通过 fn 定义函数本质上就是类似的步骤,先用 lambda 定义一个匿名函数,然后赋值给函数名指定的变量

函数是一等公民 (First Class)

在 AviatorScript 中,函数也是一种类型,可以作为参数来传递,可以作为函数的返回值等等。

## examples/function_first_class.av

fn square(x) {
  return x * 2;
}

let add = lambda(x, y, f) ->
  f(x) + f(y)
end;

let add_n = lambda(x) ->
  lambda(y) -> 
    x + y
  end
end;

println(type(square));
println(type(add));
println(type(add_n));
复制代码

我们通过 fn 和 lambda 分别定义了三个函数,通过 type 检测他们的类型:

function
function
function
复制代码

都是 function 函数类型。

这里的 add 函数接受的第三个参数 f 也是一个函数,我们用它调用 x 和 y,然后再相加,可见函数是可以作为参数来使用:

let s = add(1, 2, square);
println(s);
复制代码

我们将 square 函数作为 f 传入到 add,执行的结果就是 square(1) + square(2) ,结果等于 6:

6
复制代码

add_n 演示了函数作为返回值的情况, add_n 接受一个参数 x,然后返回一个匿名函数:

 lambda(y) -> 
    x + y
  end
复制代码

这个匿名函数接受另一个参数 y,他将 x 和 y 相加并返回:

let add_3 = add_n(3);
println(type(add_3));
println(add_3(1));
println(add_3(99));
println(add_3(' test'));
复制代码

我们将 add_n(3) 的返回值赋值给了变量 add_3 ,他的类型是 function,任何时候调用 add_3 函数,它都将和 3 相加,并返回结果:

function
4
102
3 test
复制代码

这个例子也演示了闭包,我们在下一节谈到。

闭包 Closure

AviatorsScript 的函数都支持闭包 closure,函数将捕获当前的上下文环境,哪怕在脱离这个环境的上下文中仍然可以访问到,一个经典的例子,定义一个计数器:

## examples/closure.av

let counter = lambda() ->
  let c = 0;
  lambda() ->
    let result = c;
    c = c + 1;
    return result;
  end
end;

let c1 = counter();
let c2 = counter();

println("test c1...");
for i in range(0, 10) {
  x = c1();
  println(x);
}

println("test c2...");
for i in range(0, 10) {
  x = c2();
  println(x);
}
复制代码

counter 是一个函数,它首先初始化了一个局部变量 c,然后返回结果是另一个匿名函数,这个结果匿名函数每次调用返回 c 的值,并递增。

接下来我们调用 counter 两次,赋值给了两个变量 c1 和 c2,分别调用 10 次,输出:

test c1...
0
1
2
3
4
5
6
7
8
9
test c2...
0
1
2
3
4
5
6
7
8
9
复制代码

可见, c1 和 c2 的值完全独立,分别用于自增计数,互不干扰。

c 在函数 counter 中定义,理论上说局部变量在 counter 函数返回后就“销毁”了,但是匿名的结果函数却“捕获”(closure over)了局部变量 c,哪怕在 counter 返回后,仍然可以继续访问到变量 c ,这就称之为闭包(closure), c 就是所谓自由变量。

闭包模拟 OOP

闭包可以“保存”状态,因此可以用于模拟 OOP:

## examples/closure_oop.av

## a function to return a rectangle instance.
fn rectangle(x, y) {
  let r = seq.map("x", x, "y", y);
  
  r.area = lambda() -> 
    return r.x * r.y;
  end;
  r.circum = lambda() ->
    return 2 * (r.x + r.y);
  end;
  
  return r;
}

let s1 = rectangle(3, 4);

println("s1 info:");
println(s1.x);
println(s1.y);
println(s1.area());
println(s1.circum());

let s2 = rectangle(9, 10);

println("s2 info:");
println(s2.x);
println(s2.y);
println(s2.area());
println(s2.circum());
复制代码

rectangle 函数用于返回一个 x 和 y 长和宽定义的长方形对象,它有两个方法 area  和 circum 分别用于计算长方形的面积和周长。这里我们用一个 map 来模拟对象,将 x 和 y 作为 key 放入 map,然后定义了两个匿名函数分别赋值给了 r.area 和 r.circum ,这就相当于定义了对象的方法,最后返回结果 map。两个匿名函数是闭包,捕获了自由变量 r。

接下来我们定义了两个长方形 s1 和 s2,并且打印了他们的基本信息。可以看到类似 s1.x 和 s1.area() 这样的调用,接近于面向对象的使用方式。得益于闭包,两个对象的信息是完全独立的:

s1 info:
3
4
12
14
s2 info:
9
10
90
38
复制代码

甚至,我们可以修改对象的属性:

s2.x = 100;
s2.y = 200;
println("s2 info after setting:");
println(s2.x);
println(s2.y);
println(s2.area());
println(s2.circum());
复制代码

输出:

s2 info after setting:
100
200
20000
600
复制代码