Skip to content

Latest commit

 

History

History
1923 lines (1332 loc) · 48.8 KB

mannual-zh.md

File metadata and controls

1923 lines (1332 loc) · 48.8 KB

Latte-lang 规范

目录

  1. 起步
    1. 基础语法
    2. 文件结构
  2. 基础
    1. 字面量
    2. 基本类型
    3. 控制流
  3. 类和对象
    1. 类与继承
    2. 接口
    3. 字段和方法
    4. 修饰符
    5. Data Class
    6. 实例化
    7. Object Class
    8. 隐式转换
  4. 函数类和Lambda
    1. 函数类
    2. 高阶函数和Lambda
  5. 其他
    1. Json 集合
    2. 范围
    3. 类型检查与转换
    4. 运算符绑定
    5. 异常
    6. 注解
    7. 过程(Procedure)
    8. 参数可用性检查
    9. 解构
    10. 模式匹配
  6. Java 交互
    1. 在Latte中调用Java代码
    2. 在Java中调用Latte代码

1. 起步

Latte-lang是一种JVM语言,基于JDK 1.6。它支持Java的所有语义,能够与Java完美互通,并提供比Java更多的函数式特性。

1.1 基础语法

Latte-lang(后文简称Latte)借鉴了主流语言的语法特征。如果您熟悉Java,或者了解过Kotlin,Scala,Python,JavaScript,Swift中的一到两种,那么阅读Latte-lang代码是很轻松的。

对Latte影响最大的语言应该是Kotlin和Scala

1.1.1 注释

单行注释使用//开头。例如

// i am a comment

多行注释以/*开头,以*/结尾。例如

/* comment */
/*
multiple line comment
*/
a/* comment */=1
/* the comment splits an expression */

1.1.2 包与导入

package lt::spec

import java::util::_

或者使用.分割

package lt.spec

import java.util._

代码段由package开始。一个latte文件只能包含一个package。它定义了其中定义的类、接口所在的“包”。包是一个java概念,可以理解为一个名字空间。

声明了这个文件中定义的所有类和接口都在lt::spec包下。子包的名称通过::,或者.分隔。

文件也可以不声明包,那么包名将被视为空字符串""

在导入包时使用import关键字,有三种导入方式:

import java::awt::_               /* 导入所有java::awt包中的所有类 */
import java::util::List           /* 导入类java::util::List */
import java::util::Collections._  /* 导入Collections类的所有静态字段和方法 */

使用.无法在语法上区分包与字段访问,所以在latte中建议使用::分割包名。但是考虑到其他JVM语言均使用.,所以也提供了.以符合使用习惯。

1.1.3 类

定义类User

class User(id: int, name: String)

定义MyList类,并继承了LinkedList类

class MyList(ls:List):LinkedList(ls)

定义抽象类MyList,并实现了List接口

abstract class MyList:List

详见3.1 类与继承

定义data class

data class User(id: int, name: String)

定义data class后,编译器会自动生成所有字段的getter/setter,类的toString/hashCode/equals方法,并继承和实现Serializable、Cloneable接口。

详见3.5 Data Class

定义object class

object Singleton

可以直接使用类名获取该对象。

详见3.7 object class

1.1.4 接口

定义接口Consumer

interface Supplier
    def supply

详见3.2 接口

1.1.5 函数类

定义函数类sum

fun sum(a, b)
    return a+b

详见4.1 函数类

1.1.6 变量

只读变量

val a:int = 1
val b = 1

可变变量

var x = 5
x += 1

可变变量的var可以省略

y = 6

1.1.7 字符串模板

main(args : []String)
    println("First argument: ${args[0]}")

1.1.8 使用条件表达式

if a > b
    return a
else
    return b

或者

result = (if a > b {a} else {b})

一个方法需要返回值,那么只要末尾一个值为表达式,Latte就会自动生成方法的return语句

详见2.3.1 If 语句

1.1.9 for循环

for item in list
    println(item)

或者

for i in 0 until list.size
    println(list[i])

详见2.3.2 For 语句

1.1.10 while循环

i = 0
while i < list.size
    println(list[i++])

详见2.3.3 While 语句

1.1.11 范围

检查x是否在范围中

if x in 1 to y-1
    print("OK")

从1到5进行循环

for x in 1 to 5
    print(x)

详见5.2 范围

1.1.12 Lambda

list.stream.
filter{it.startsWith("A")}.
map{it.toUpperCase()}.
forEach{print(it)}

f = a -> 1 + a
f(2) /* 结果为 3 */

详见4.2 高阶函数和Lambda

1.1.13 Json语法

list = [1, 2, 3]
map = [
    "a": 1
    "b": 2
]

详见5.1 Json 集合

1.1.14 返回对象的"或"

拥有JavaScript的||的功能。

a = a || 1

1.1.15 指定生成器

#js
    def method(a)
        return a+1

将会被转换成如下JavaScript代码

function method(a) {
    return a + 1;
}

1.1.16 模式匹配

val (x,y) = Bean(1,2)

详见5.9 解构

o match
    case Bean(a,b) => ...
    case People(name, age) if age > 20 => ...
    case _ => ...

详见5.10 解构

1.2 文件结构

Latte源文件以.lt或者.lts为扩展名。不过实际上后缀名并不重要,手动构造编译器时附加特定参数即可。

1.2.1 定义层次结构

编译器首先会检查文件的第一行。第一行可以标注该文件 使用缩进定义层次结构,还是 使用大括号定义层次结构。

例如:

/// :scanner-brace

表示使用大括号定义层次结构。

/// :scanner-indent

表示使用缩进定义层次结构。

默认为缩进。提供这个选项是为了满足不同人的编码喜好。这两者的选择在词法上完全一样。

注意:Latte并不限定缩进数量。比上一行“更大的缩进”均代表一个新层次。

1.2.2 层次结构

使用大括号定义层次结构时,有如下几个符号会开启一个新层:

(
[
{

当其对应的符号出现时,这个开启的层将关闭:

)
]
}

使用缩进定义层次结构时,还有两个符号会开启一个新层: ->=>

由于层将在缩进“变得更小”时关闭。所以,如下代码不加上括号也可以正确的工作:

f = x->x+1
f()

注:使用大括号定义层次结构时,->不会开启新层,若要书写多行语句请使用{...}。此时规则和java完全一致。

这里有一个示例,帮助理解“层”的概念:

┌───────────────────────┐
│       ┌─┐             │
│classA(│a│):B          │
│       └─┘             │
│    ┌─────────────────┐│
│    │       ┌────────┐││
│    │method(│arg0    │││
│    │    ┌──┘        │││
│    │    │arg1       │││
│    │    │arg2, arg3 │││
│    │    └───────────┘││
│    │):Unit           ││
│    │    ┌────┐       ││
│    │    │pass│       ││
│    │    └────┘       ││
│    └─────────────────┘│
└───────────────────────┘

该源码将被解析成如下结构:

-[class]-[A]-[(]-[│]-[)]-[:]-[B]-[│]
                  └[a]-    ┌──────┘
┌──────────────────────────┘
└────[method]-[(]-[│]-[)]-[│]-
┌──────────────────┘       └───────[pass]-
│
└──[arg0]-[EndNode]-[arg1]-[EndNode]-[arg2]-[StrongEndNode]-[arg3]

注意: 本文除非注明,否则均使用“缩进”进行描述。在注明为“大括号”时,会在示例代码开头标注/// :scanner-brace

1.2.4 层次控制字符

Latte支持直接使用{}定义层次结构。不过要注意,如果写为{}则表示空Map(映射)

例如:

if a > b {1} else {2}
/* 相当于 */
if a > b
    1
else
    2
var map = {} /* 这是一个map */
/* 可以正常编译,定义了一个方法,其中定义一个局部变量,赋值一个map */
def method {
    map = [
        "a": 1
        "b": 2
    ]
}

2. 基础

2.1 字面量

Latte中有6种字面量:

  1. number
  2. string
  3. bool
  4. array
  5. map

2.1.1 number

数字可以分为整数和浮点数

例如:

1
1.2

1是一个整数,1.2是一个浮点数。

整数字面量可以赋值给任意数字类型,而浮点数字面量只能赋值给floatdouble。详见 2.2 基本类型

2.1.2 string

字符串可以以'"开头,并以同样的字符结尾。

例如:

'a string'
"a string"

使用\作为转义字符

例如:

'escape \''
"escape \""

字符串字面量分为两种,若字符串长度为1,则它可以赋值给char类型或java.lang.String类型。否则只能赋值给java.lang.String类型。

2.1.3 bool

布尔型有下述4种书写形式:

true
false
yes
no

trueyes表示逻辑真,falseno表示逻辑假。

布尔值字面量只能赋值给bool类型。

2.1.4 array

数组以[开头,并以]结尾,其中包含的元素可以用,分隔,也可以通过换行分隔。

例如:

[1,2,3]

[
    object1
    object2
    object3
]

[
    object1,
    object2,
    object3
]

2.1.5 map

映射(字典)像swift一样,以[开头,以]结尾。

键值对通过类型符号:分隔,不同的entry通过,或者换行进行分隔。

例如:

['a':1, 'b':2, 'c':3]

[
    'a':1
    'b':2
    'c':3
]

[
    'a':1,
    'b':2,
    'c':3
]

2.2 基本类型

Latte保留了Java的8种基本类型。由于Latte是动态类型语言,并且可以在编译期和运行时自动装包和拆包,所以基本类型与包装类型可以认为没有差异。

八种基本类型:

int
long
float
double
short
byte
char
bool

其中int/long/float/double/short/byte为数字类型。

注意,与Java不同的是,Latte使用bool而非boolean

2.2.1 基本类型转换

在转换时,所有基本类型都可以互相转换(除了bool,它可以被任何类型转换到,但不能转换为其他类型,详见5.8 参数可用性检查)而不会出现任何错误。但是,在高精度向低精度转换时可能会丢失信息。例如:

i:int = 3.14   /* i == 3 丢失了小数部分 */

b:bool = 10  /* b == true 除了数字不是0外,信息都丢失了 (只有0在转换为bool时才会是false) */

2.2.2 基本类型运算

Latte支持所有Java的运算符,并在其基础上有所扩展

对于数字的基本运算,其结果均为“精度较高的值”的类型,且最低为int型。例如:

r1 = (1 as long) + (2 as int)    /* r1 是 long */
r2 = (1 as byte) + (2 as short)  /* r2 是 int */

由于Latte可以任意转换基本类型,所以这么写也可以正常编译并运行:

a:short = 1
a+=1  /* a == 2 */
a++   /* a == 3 */

由于Latte支持基本类型的互相转换,你可以直接把int的结果赋值给short

Latte支持所有Java运算符,当然也包括位运算。和java一样,位运算必须作用于整数上。Latte支持所有整数类型的位运算:int/long/short/byte
此外,Latte还支持乘方运算:

a ^^ b

结果均为double型。

本质来说,Latte不存在“运算符”。所有运算符都是方法调用。基本类型的运算是由其包装类型隐式转换后调用方法完成的。

2.3 控制流

2.3.1 If 语句

和Java一样,if是一个语句而非像Kotlin,Scala那样作为表达式。

但是,Latte支持Procedure,并且可以自动添加返回语句,所以使用起来和作为表达式区别并不大。

if a > b
    return 1
else
    return 2

val result = (if a>b {1} else {2})

2.3.2 For 语句

for语句格式如下:

for item in iter
    ...

其中iter可以是数组、Iterable对象、Iterator对象、Enumerable对象、Map对象。

iter为前4种时,for语句将把其包含的对象依次赋值给item并执行循环体。当iter为Map对象时,item是一个Entry对象,它来自Map#entrySet()

你也可以使用to或者until,并在循环体内使用下标来访问元素:

for i in 0 until arr.length
    val elem = arr[i]
    ...

2.3.3 While 语句

while语句格式如下:

while boolExp
    ...

do
    ...
while boolExp

它的含义和Java完全一致。

2.3.4 break, continue, return

在循环中可以使用 breakcontinue 来控制循环。break将直接跳出循环,continue会跳到循环末尾,然后立即开始下一次循环。它的含义与Java完全一致。

return可以用在lambda、方法(包括“内部方法”)、Procedure、脚本、函数类中:

/* lambda */
foo = ()->return 1

/* 方法 */
def bar()
    return 2

/* Procedure */
(
    return 3
)

/* 函数类 */
fun Fun1
    return 4

脚本中的return语句表示将这个值返回到外部,在require这个脚本时将返回这个值。

如果return是这个函数/方法最后的一条语句,或者该函数任意一条逻辑分支的最末尾,那么return都可以被省略:

fun add(a, b)
    a+b

val result = add(1, 2)
/* result is 3 */

转换方式很简单,首先取出这个函数/方法的最后一条语句,如果是表达式,而且这个函数/方法要求返回值,则直接将其包装在AST.Return中。
如果最后一条语句是if,那么对其每一个逻辑分支进行该算法。

3. 类和对象

3.1 类与继承

3.1.1 类

类通过class关键字进行定义

当类内部不需要填充任何内容时,可以非常简单的书写为:

class Empty

当需要提供构造函数参数时,写为:

class User(id, name)

当然,你也可以为参数指定类型:

class User(id:int, name:String)

如果不指定类型则类型视为java.lang.Object

Latte不支持在类内部再定义构造函数,不过,你可以指定参数默认值来创建多个构造函数:

class Rational(a:int, b:int=1)

此时你可以使用Rational(1)或者Rational(1, 2)来实例化这个类。

--

构造函数内容直接书写在class内:

class Customer(name: String)
    logger = Logger.getLogger('')
    logger.info("Customer initialized with value ${name}")

直接定义在类中的变量,以及构造函数参数,将直接视为字段(Field)。也就是说,上述例子中定义的name和logger都是字段。详见 3.3 字段和方法

--

使用private修饰符来确保类不会被实例化:

private class DontCreateMe

在Latte中,所有类都是public的,所以,在class前的任何“访问关键字”均为该类构造函数的访问关键字。

3.1.2 继承

和Java一样:Latte是单继承,并且所有的类都默认继承自java.lang.Object。你可以使用类型符号:来指定继承的类。继承的规则和Java完全一致。

class Base(p:int)
class Derived(p:int) : Base(p)

父类的构造函数参数直接在父类类型后面的括号中指定。

如果使用了父类的无参构造函数,那么可以省略括号:

class Example : Object

如果想指定一个类不可被继承,那么需要在它前面加上val修饰符:

val class NoInher

3.1.3 抽象类

使用abstract关键字定义抽象类:

abstract class MyAbsClass
    abstract f()

抽象类规则与Java完全一致。抽象类可以拥有未实现的方法。

继承一个抽象类:

class MyImpl : MyAbsClass
    @Override
    def f=1

3.1.4 静态成员

使用static定义静态成员。static可以“看作”一个修饰符,也可以“看作”一个结构块的起始。例如:

class TestStatic
    static
        public val STATIC_FIELD = 'i am a static field'
    static func()=1

3.2 接口

Latte接口遵循Java的接口定义。使用interface关键字:

interface MyInterface
    foo()=...

定义了abstract方法foo()

让一个类实现接口,也使用类型符号:

class Child : MyInterface
    foo()=456

child = Child
child.foo /* result is 456 */
child.bar /* result is 123 */

接口可以拥有字段,但是和Java规则一样,字段必须是static public val(默认也是)。

interface MyInterface
    FLAG = 1

接口也可以拥有static方法(和Java一样)

interface TestStaticMethod
    static
        method()=111

TestStaticMethod.method() /* result is 111 */

3.3 字段和方法

3.3.1 定义字段

你可以在类或接口中定义字段:

class Address(name)
    public street
    public city
    public state
    public zip

interface MyInterface
    FLAG = 1

在类中定义的字段默认被private修饰,可选的访问修饰符还有public, protected, internal

字段可以为static,只要写在static块中即可(接口默认就是static的,不需要修改)。也可以为不可变的,使用val修饰即可。由于构造函数参数也是字段,所以这些修饰符可以直接写在构造函数参数中。例如:

class User(protected val id, public val name)

使用字段很简单,直接使用.符号访问即可,和Java一致。

val user = User(1, 'latte')
user.name   /* result is 'latte' */

val address = Address('home')
address.city = 'hz'

Latte提供所谓的property支持:使用gettersetter来定义property。详情见 3.3.3 Accessor

3.3.2 方法

Latte支持多种定义方法的语法,先看一个最完整的方法定义:

def foo(x:int, y:int):int
    return x + y

如果方法没有参数,那么可以省略里面的内容,甚至省略括号:

/* 无参数 */
def foo:int
    return 1

返回类型可以不指定,默认为java.lang.Object

def foo
    return 1

如果方法体只有一行并返回一个值,可以把在方法定义后直接接=value

def foo = 1

如果方法体不存在(即空方法),可以只写一个方法名称(如果有参数再把参数加上)

def foo

上述定义的缩写方式可以混合使用。

此外,如果在定义方法的时候(使用了括号)并且(附带一个修饰符/注解,或者定义了返回值,或者使用了=语法),那么可以省略def

bar():int
foobar()='hello'
fizz():int=1

--

如果明确方法不返回值,那么可以附加Unit类型。Latte中只能写作Unit,代表了Java中的void
但是,在Latte中所有方法都会返回一个值,对于Unit类型的方法,虽然会被编译为void类型,但是依然会返回Unit,它是lt.lang.Unit类型。

和构造函数参数一样,方法参数也可以设定默认值:

foo(a, b=1)=a+b
foo(1)  /* result is 2 */

注意def实际上是一个“修饰符”(虽然它什么都不做),并不属于“关键字”。设置def是为了和“省略参数的lambda”进行语法上的区分。

例如:

foo(x)
    ...

实际上会被转化为4.2.2 Lambda中描述的形式:

foo(x)(it->...)

所以使用类似于这种方式(VALID_NAME ( [PARAM [, PARAM, ...]] ) { ... })定义的方法,如果没有注解,也没有其他修饰符,会造成歧义,所以不可省略def

方法返回类型为Unit时,你依然可以书写return value,这个value会被求值,但是不会被返回。
方法非Unit时,你也可以直接书写return,这时默认返回一个Unit。当然,如果返回类型不匹配,编译期依然会报错。

内部方法

Latte支持“内部方法”。所谓内部方法是指在方法内部再定义一个方法。

def outer
    def inner

内部方法可以访问外部的所有变量,并且可以修改它们。

实际上,Lambda、Procedure都基于“内部方法”特性。所以它们也可以修改捕获到的所有变量。

3.3.3 Accessor

accessor分为两种,一种是取值:getter,一种是赋值:setter。

对于getter有两种定义方式:

  1. 定义为get{Name}()
  2. 定义为{name}()

对于setter只有一种定义方式:定义为set{Name}(name)

class User(id, name)
    def getId=id
    setId(id)
        this.id = id
    def name=name  /* 放心,这么写是正确的 */
    setName(name)
        this.name = name

如此定义就可以像直接访问field一样来调用这几个方法了。

user = User(1, 'latte')
println("user_id is ${user.id} and user_name is ${user.name}")
user.id = 2
user.name = 'jvm'

不过要注意的是:如果字段暴露给访问者,那么还是优先直接取字段或者对字段赋值。

此外,还有一对特殊的accessor:

  • set(String, ?) 方法签名要求方法名为"set",第一个参数接受一个String,第二个参数接受一个值,类型没有限制。(只不过使用时只能赋值为该类型的子类型)。
  • get(String) 方法签名要求方法名为"get",第一个参数接受一个String。

定义有上述accessor的类的实例,在取o.field时,将转换为o.get('field')。在设置o1.field = o2时,将转换为o1.set('field', o2)

3.4 修饰符

3.4.1 访问修饰符

Latte有4种访问修饰符:

  • public 公有,所有实例均可访问
  • protected 受保护,包名相同的类型,或者子类可访问
  • internal 包内可访问,包名相同的类型可以访问
  • private 私有,只有本类型可访问

访问修饰符可以用来修饰:

  • 接口
  • 字段
  • 方法
  • 构造函数的参数

其中,类访问修饰符并不是规定给类用的。Latte中,类的访问修饰符永远为public,这个修饰符是作为构造函数而存在的。

位置 public protected internal private
接口
字段
方法
构造函数参数

3.4.2 其他修饰符

Latte支持所有Java的修饰符,但是名称可能有改动:

  • var 表示可变变量(可省略,默认即为可变)
  • val 表示不可变变量,或者不可被重载的方法,或者不可被继承的类
  • abstract 抽象类/方法
  • native 本地方法
  • synchronized 同步方法
  • transient 不持久化的字段
  • volatile 原子性的字段
  • strictfp 方法内的符点计算完全遵循标准
  • data 类是一个data class:详见 3.5 data class

val 其实就是Java的 final

3.5 Data Class

编译器会为data class的每一个字段生成一个getter和setter。并生成无参构造函数,toString(), hashCode(), equals(Object)方法。此外,还会实现Serializable和Cloneable接口。

data class User(val name: String, val age: int)

user = User('cass', 22)
user.toString()   /* result is User(name='cass', age=22) */

你也可以定义自己的getter/setter/toString/hashCode/equals,编译器将跳过对应方法的生成。

3.6 实例化

Latte不需要new关键字就可以实例化一个类。对于无参数的实例化,甚至不需要附加括号:

class Empty
empty = Empty

class User(id, name)
user = User(1, 'latte')

当然,Latte也允许你加上new

empty = new Empty
user = new User(1, "latte")

不过,Latte中的new的“优先级”非常低,java中的new X().doSth()的写法在Latte中必须写为(new X).doSth这样的写法。

Latte中建议不要写new

此外,Latte提供另外一种特殊的实例化方式
调用无参构造函数,并依次赋值:

class open(file, mode)
    public encoding

f = open('/User/a', "r", encoding='utf-8')

这是一个语法糖,相当于如下Latte代码:

f = open('/User/a', "r")
f.encoding = 'utf-8'

即:首先使用不带=的参数进行类型的实例化,然后把剩余“参数”看作对accessor的赋值操作。

这个语法糖不光适用于Latte定义的data class,还可以支持任意具有无参构造函数,并有可访问的field或者accessor的对象。例如标准Java Bean就可以使用这个语法。

class User {
    private int id;
    private String name;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

3.7 Object Class

object DataProviderManager
    def registerDataProvider(provider: DataProvider)
        ...
    val allDataProviders: Collection = ...

本质上,object class定义了一个类,但是这个类不能拥有构造函数的参数,构造函数为private,并拥有一个static public val的字段来存放单例。

object class可以继承父类、实现接口,其规则与普通的class完全相同

object DefaultListener : MouseAdapter()
    def mouseClicked(e: MouseEvent):Unit
        ...
    def mouseEntered(e: MouseEvent):Unit
        ...

你可以直接使用类名来获取这个单例对象:

val o = DataProviderManager

3.8 隐式转换

隐式转换可以帮助你为对象扩展方法,从而更灵活的进行编程。

使用隐式转换分为四步

1. 定义一个类用来表示隐式转换后的类型

这一步是可选的,您可以直接隐式转换到已有的类型。这里为了说明方便,单独定义一个:

class RichInteger(i:Integer)
    minutes() = i + " minute" + ( if i != 1 { "s" } else { "" } )

2. 定义一个隐式对象:

隐式对象是一个普通的object class,不过需要用implicit修饰它。
在其中定义一些"隐式方法",用来进行隐式类型转换。

“隐式方法”就是普通的方法,不过需要用implicit修饰它。
隐式方法有一些要求:参数只能有一个,其类型为隐式转换的源类型(或源类型的父类型);返回类型为目标类型。

例如:我们想要将Integer转换为RichInteger

implicit object TestImplicitCast
    implicit def cast(i:Integer):RichInteger = RichInteger(i)

3. 启用隐式类

import implicit TestImplicitCast

使用import implicit引入隐式类。这里注意,隐式类必须使用类名引入,而且,即使隐式类与使用者定义在同一个包下,也必须显式引入。这样错用几率会比较小。

4. 使用

val x = 30 minutes

此时,x的值即为"30 minutes"。运行时会寻找可用的隐式转换,并判断转换后是否能够调用指定方法:发现Integer => RichInteger可用,且RichInteger可以调用minutes()方法;那么最终执行时的代码相当于val x = TestImplicitCast.cast(30).minutes()

4. 函数类和Lambda

4.1 函数类

4.1.1 函数式接口 和 函数式抽象类

Java8中规定了:只有一个未实现方法的接口为“函数式接口”。例如:

interface Consumer {
    void consume(Object o);
}

只有一个consume方法没有被实现,所以它是函数式接口。

类似的,Latte中除了支持Java的函数式接口外,还支持定义“函数式抽象类”。

如果一个类有无参的public构造函数,且只有一个未被实现的方法,那么这个类是一个函数式抽象类。例如:

abstract class F
    abstract apply()=...

4.1.2 定义函数类

要使用函数式接口/抽象类,必须有特定的实现。Latte提供一种简便的方式来书写其实现类:

fun Impl(x, y)
    return x + y

使用fun关键字定义“函数类”,参数即为函数式类型未实现方法的参数,内部语句即为实现的方法的语句。

函数类默认可以不附加类型,它在编译期将被视为FunctionX,其中X为参数个数。在运行时,FunctionX可以转化为任何参数个数相同的类型。

如果确定其实现的类型,可以使用类型符号:定义:

fun MyTask : Runnable
    println('hello world')

4.1.3 使用函数类

函数类会被编译为Java的类(class),所以,Latte中,它既有函数的特征,又有类的特征:

  • 函数类可以被import(就像类一样)
  • 可以直接调用这个类,例如:Impl(1, 2)。这条语句将实例化Impl并执行它实现的方法(这个用法就像lambda一样)
  • 可以将它作为值赋值给变量(实际上是调用了无参数构造函数,就像类一样)

总之,对于函数类,你完全可以把它当做一个变量来考虑。

Thread(MyTask).start()

同时由于它是正常的类,所以也具有父类型的所有字段/方法:

task = MyTask
task.run()

4.1.4 函数式对象

在Latte中,对于所有的函数式对象,都可以使用“像调用方法那样的语法”。

task()

一个对象是“函数式对象”,有三种情形:

  1. 它的类(.getClass)的直接父类为函数式抽象类
  2. 它的类(.getClass)只实现了一个接口,且这个接口是函数式接口
  3. 这个对象具有apply(...)方法

所以,在Latte中,函数式对象就是函数。

4.2 高阶函数和Lambda

4.2.1 高阶函数

如果一个函数可以接受另一个函数作为参数,或者返回一个函数,那么这个函数就是高阶函数。

4.1.3 使用函数类 中提到过“函数式对象”就是“函数”,所以,任何能够接收(并处理)函数式对象,或者返回函数式对象的函数/方法,就是高阶函数。

如下代码是Java使用stream api的做法:

List<String> strList = ...;
strList.stream().
	map(s->Integer.parseInt(s)).
	filter(n->n>10).
	collect(Collectors.toList())

可以看到,map,filter都接受一个函数作为参数,所以它们也可以看作高阶函数。

在Latte中,上述代码可以用Latte的lambda语法来表达:

/// :scanner-brace

strList = ...
strList.stream.
	map { Integer.parseInt(it) }.
	filter { it > 10 }.
	collect(Collectors.toList())

4.2.2 Lambda

Latte支持和Java完全一样的Lambda语法:

strList.stream().
map(s->Integer.parseInt(s)).
filter(n->n>10).
collect(Collectors.toList())

从外观上看不出任何差别?没错,语法上完全一致(特别是使用大括号区分层次的时候)。
使用缩进的情况下,多行lambda可以这么写:

strList.stream.map(
    s-> s = s.subString(1)
        Integer.parseInt(s)
)

注:可以不写return,因为Latte会帮你把需要的return补上。这个特性适用于任何“编译为JVM方法”的语法。

如果lambda只有一个参数(例如上述代码),那么名称和->可以被省略。其中,名称会被标记为it

strList.stream.map
    it = it.subString(1)
    Integer.parseInt(it)

这个特性可以让代码更简洁,同时也可以构造更灵活的内部DSL

latteIsWrittenInJava
    if it is great
        star the repo

做一丁点处理后,这是可以正常编译的代码!(不需要hack编译器)

可以写成一行

latteIsWrittenInJava { if it is great { star the repo }}

此外Lambda的变量捕捉机制和Java不同。Latte可以在Lambda中的任何地方修改被捕获的变量。

var count = 0
(1 to 10).forEach { count+=it }
println(count)

5. 其他

5.1 Json 集合

Latte支持Json格式的字面量。

Json数组:

var list = [1, 2, 3, 4]

使用Json的数组语法,可以创建一个java.util.LinkedList实例,也可以创建一个数组。这取决于你将它赋值给什么类型的变量,或者使用as符号把它转换为什么类型。

int数组:

[1, 2, 3, 4] as []int

Object数组:

[1, 2, 3, 4] as []Object

在Latte中,你可以将一个java.util.List类型的对象转换为其它种类的Object。该特性将尝试使用无参构造函数构造目标类型对象,然后对每一个List中的元素,调用add方法。

class JsonArray
    list = []
    def add(o)=list.add(o)
res = [1,2,3] as JsonArray
/*
 same as:
 res = JsonArray()
 for item in [1,2,3]
     res.add(item)
 */

--

Json对象:

var map = [
    'one': 1,
    'two': 2,
    'three', 3
]

其中,是不必须的。“换行”和,都可以用来来分割list的元素,以及map的entry

在Latte中,你还可以把一个"所有键都是string"的map转换为指定类型的对象。

data class Bean(hello, foo)
res = [
    "hello" : "world"
    "foo"   : "bar
] as Bean

/* res will be Bean(hello=world, foo=bar) */

该转换将首先用无参构造函数构造指定类型,然后对map中每一个键,进行Latte的赋值操作。
不光可以显式的转换,还可以作为方法参数隐式转换过去。

5.2 范围

在Latte中可以使用to或者until运算符来定义一个“范围”(range)。这两个运算符只接受整数作为参数。它的结果是一个java.util.List实例。

使用to可以定义一个包含头和尾的范围,使用until可以定义一个只包含头,不包含尾的范围:

oneToTen = 1 to 10   /* [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] */
oneToNine = 1 until 10  /* [1, 2, 3, 4, 5, 6, 7, 8, 9] */

range也支持尾比头更小:

tenToOne = 10 to 1   /* [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] */
tenToTwo = 10 to 1   /* [10, 9, 8, 7, 6, 5, 4, 3, 2] */

范围的实现基于“隐式类型转换”,Latte默认定义的RichInt提供了tountil方法。

5.3 类型检查与转换

5.3.1 类型检查

Latte为静态类型和动态类型混合的语言。总体来说,类型检查比较宽松,并且不一定在编译期检查。

Latte不要求显式转换,在赋值、方法return时,值会自动的尝试转换为需要的类型。

class Base
class Sub:Base

var x:Base = Sub
var y:Sub = x

其中在赋值给y时,自动增加了一个类型转换。

5.3.2 类型转换

在调用方法时,自动转换并不会做,因为并不确定变量的类型,也不确定方法参数需要哪种类型。
如果编译期没有找到方法,则会在运行时再获取参数对象的类型并进行方法的寻找。如果一定要在编译期确定调用何种方法,可以手动转换类型。

class Data
    i:int
    def setI(i:int) { this.i=i }

fun call(x)
    var data:Data = Data
    data.setI(x as int)

上述例子中,x类型不确定,但是可以显式地转化为int。

5.4 运算符绑定

Latte支持运算符“绑定”。Latte的运算符绑定策略非常简单。每个运算符都看作方法调用。a+b看作a.add(b)a*b看作a.multiply(b)!a看作a.not()

这些运算符绑定依照BigIntegerBigDecimal的命名,所以你可以直接使用运算符来计算大数。

a = BigInteger(3)
b = BigInteger(4)
c = a + b

若需要绑定运算符,只需要写出签名相符的方法即可,例如定义一个Rational类来表示分数:

class Rational(a, b)
    add(that: Rational)=Rational(this.a * that.b + that.a * this.b, this.a * that.b)
    toString():String="${a}/${b}"

a = Rational(1, 4)  /* 1/4 */
b = Rational(3, 7)  /* 3/7 */
c = a + b           /* 19/28 */

有一些运算符是“复合”的,即:它们可以由多个操作构成。例如++a,就可以由(a = a + 1 , return a)构成。这类运算符不提供绑定,如果有需要,请绑定它们的展开式用到的运算符。

下表描述了所有提供绑定的运算符,以及一些展开式规则:

运算符 方法签名
a:::b a.concat(b)
a * b a.multiply(b)
a / b a.divide(b)
a % b a.remainder(b)
a + b a.add(b)
a - b a.subtract(b)
a << b a.shiftLeft(b)
a >> b a.shiftRight(b)
a >>> b a.unsignedShiftRight(b)
a > b a.gt(b)
a < b a.lt(b)
a >= b a.ge(b)
a <= b a.le(b)
a == b a.eq(b)
a != b !a.ne(b)
a in b b.contains(a)
a & b a.and(b)
a ^ b a.xor(b)
a b
!a a.logicNot()
~a a.not()
-a a.negate()
a[0] a.get(0)
a[0, 1] a.get(0, 1)

+a 这种用法在Latte中不对+做任何处理,当做a
上面的a[0, 1]用法,如果a是一个二维数组,则相当于java的a[0][1]
Latte对所有对象均可隐式转换到RichObject,在其中提供了==!=的绑定,其中会调用对象的equals方法

Latte的运算符优先级和Java完全一致,而Latte特有的运算符优先级如下:

  • in== 优先级相同
  • ::: 优先级最高

由于==被绑定到equals方法,所以检查引用相同使用===,引用不同使用!==
此外,Latte还提供两个运算符isnot,它除了可以检查引用、equals,在右侧对象是一个Class实例时还可以检查左侧对象是否为右侧对象的实例。

虽然==绑定到equals方法,但是如果写为null==x,编译器能够知道左侧一定为null,这时会检查null值而不是调用equals !=同理。

运算符 展开式
a?=b a = a ? b
a++ tmp = a , a = a + 1 , return tmp
++a a = a + 1 , return a
a-- tmp = a , a = a - 1 , return tmp
--a a = a - 1 , return a

其中?=?代表任何二元运算符


Latte中,和普通的方法调用不同,运算符前不需要附加.,也不需要对参数包裹括号。但是因为Latte的运算符和方法调用是一回事,所以为了一致性,普通方法调用也可以将方法名看作运算符来书写:

list isEmpty // list.isEmpty()
map put "Feb", 2 // map.put("Feb", 2)
.println o // println(o)

使用逗号分隔多个参数。使用.表示直接调用方法(而不是在某个对象或者某个类上调用)。

5.5 异常

Latte和Java总体上是类似的,但是仍有多处不同:

  • Latte没有checked exception
  • Latte可以throw任何类型的对象,比如throw 'error-message'
  • Latte可以catch任何类型的对象
  • 由于上一条,Latte不提供catch(Type e)这种写法,需使用if-elseif-else来处理
fun isGreaterThanZero(x)
    if x <= 0 { throw '${x} is littler than 0' }

var a = -1
try
    isGreaterThanZero(a)
catch e
    e.toCharArray  /* succeed. `e` is a String */
finally
    a = 1

5.6 注解

Latte使用annotation关键字定义注解

annotation Anno
    a:int = 1
    b:long

Latte中的注解默认为运行时可见的(而Java中默认不可见)。
可以使用java::lang::annotation::Retention注解重新规定可见性。
同时默认为可以标注在所有地方(和Java一样)。
可以使用java::lang::annotation::Target注解重新规定标注位置。

因为annotation是一个关键字,所以导入包时需要用 点号 包围 annotation这个单词。


Latte的注解使用方式和Java一致

class PrintSelf
    @Override
    toString():String='PrintSelf'

不过有一点要注意,Latte的注解不能和其所标注的对象在同一行,除非加一个逗号,比如:

@Anno1,@Anno2,method()=...

和Java一样,注解的value参数可以省略value这个键本身。

@Value1('value')
@Value2(value='value')

5.7 过程(Procedure)

Latte支持把一组语句当做一个值,这个特性称作“过程”。
过程由小括号开始,小括号结束。

用这个特性可以省略不必要的中间变量声明。

class Rational(a, b)
    toString():String = a + (
        if b == 1
            return ""
        else
            return "/" + b
    )

过程最终也是编译为“方法”的,可以省略最后的return,所以可以写为:

;; :scanner-brace
class Rational(a, b)
    toString():String = a + ( if b==1 {""} else {"/" + b} )

5.8 参数可用性检查

Latte支持参数上的null值或“空”值检查,分别使用nonnullnonempty修饰符。

由于Latte的Unit方法也返回一个值(Unit),所以在nonnull中不光会检查null值,还会检查Unit。
如果出现null则会立即抛出java.lang.NullPointerException异常
如果出现Unit则会立即抛出java.lang.IllegalArgumentException异常

def add(nonnull a, nonnull b)= a + b

add(null, 1)      /* 抛出NullPointerException */
add(Unit, 2) /* 抛出IllegalArgumentException */

对于nonempty,检查的范围更广。首先Latte会将这个值转换为bool类型(Latte中任何类型都可以转为bool)
如果结果为false则会抛出异常java.lang.IllegalArgumentException

def listNotEmpty(nonempty list)

listNotEmpty([])  /* 抛出 IllegalArgumentException */

在转换为bool时,Latte会尝试

  1. 如果是null,则返回false
  2. 如果是Unit,则返回false
  3. 如果是Boolean类型,则返回其对应的bool
  4. 如果是数字类型,则:如果转换为double的结果是0,那么返回false,否则返回true
  5. 如果是Character类型,则:如果转换为int的结果是0,那么返回false,否则返回true
  6. 如果这个对象带有def isEmpty:bool或者def isEmpty:Boolean方法,那么调用之,并返回相应结果
  7. 返回true

5.9 解构

5.9.1 解构用法

解构指的是将一个对象分解为其组成部分的多个对象。

例如有如下定义和实例化:

data class Bean(a,b)

val bean = Bean(1,2)

可以知道,bean是由12组成的,它应当被分解为(1,2)。
Latte提供这样简化的分解:

val (x,y) = bean

定义了x和y,并分别赋值为1、2。

5.9.2 解构实现方式

使用解构,首先需要定义一个static方法unapply

class X {
    static {
	    unapply(o)=...
	}
}

这个方法需要接受一个参数,表示被解构的对象,并返回null或一个java::util::List实例。

如果返回null则说明解构失败,如果返回List实例,则表示会被分解为存在于列表中的对象。

如果解构失败,则解构表达式返回false,否则返回true

可以指定使用“带有unapply方法的类”来执行解构:

Bean(x,y) <- bean

如果没有指定,则尝试使用右侧对象的类中的unapply方法进行解构。

(x,y) <- bean /* 相当于 Bean(x,y) <- bean */

如果没有指定类型,则可以将<-替换为=

解构可以放在if中使用:

if List(a,b,c) <- o
    println("result is ${a},${b},${c}")
else
    println("destruct failed!")

5.10 模式匹配

scala一样,Latte不提供javaswitch语句,但是提供更强大的模式匹配。

def doMatch(o) = o match
    case 1 => ... /* 根据值匹配 */
    case b:Apple => ... /* 检查类型并定义一个新的变量 */
    case _:Banana => ... /* 根据类型匹配 */
    case Bean(x,y) => ... /* 根据解构匹配 */
    case Bean(1, Bean(x, _:Integer)) => ... /* 多重模式 */
    case Bean(x,y) if x > 0 => ... /* 解构后再做判断 */
    case _ => ... /* 匹配所有(默认行为) */

模式匹配会从上到下依次尝试匹配。如果匹配成功则进入该分支执行语句,最终返回一个值(也可能返回Unit)。如果匹配失败,则会抛出lt::lang::MatchError

任何匹配模式都可以添加if语句,仅当if判断成立时才会进入执行。

6. Java交互

在设计时就考虑了Latte和Java的互操作。所以它们基本是无缝衔接的。

6.1 在Latte中调用Java代码

实际上这里不会出现任何问题。Latte源代码最终是编译到Java字节码的,所以Latte调用Java就像Latte调用自己一样。
而设计时也考虑到了互通性,几乎所有Latte特性都可以通过编写Java源代码来模拟。

这里给出一些Latte与Java相同语义的表达:

1. 规定变量和它的类型

在Field操作时会有交互。

java:

      Integer integer;
      List    list;
final int     anInt;
      Object  obj;

latte:

    integer : Integer
    list    : List
val anInt   : int
    obj

Latte可以使用var表示可变变量,不过也可以不写,默认即为可变变量。Object类型不需要写,同样也是默认值。

2. 定义方法、参数类型和返回类型

在调用方法时会有交互。

java:

void method1() {}
Object method2() { return null; }
int method3(int x) { return x; }

latte:

method1():Unit=...
method2()=null
method3(x:int):int = x

3. 获取java类,判断类型

java:

Class c = Object.class;

if (s instanceof String) {}

latte:

c = type Object

if s is type String

6.2 在Java中调用Latte代码

如果是已编译的Latte二进制文件,那么加载到class-path中,直接在Java中调用即可。Latte在设计时非常小心的不暴露任何“不一致状态”给Java,所以除了反射访问private外,尽管放心的调用吧。

6.2.1 在Java中编译Latte

如果是Latte源文件,则需要使用Latte-compiler编译。

import lt.repl.Compiler;

Compiler compiler = new Compiler();
ClassLoader cl = compiler.compile(new HashMap<String, Reader>(){{
		put('source-name.lt', new InputStreamReader(...));
}});
Class<?> cls = cl.loadClass('...');

6.2.2 在Java中执行eval

Latte支持eval,在latte代码中eval('...')即可。在Java中,你也可以直接调用

lt.lang.Utils.eval("[\"id\":1,\"lang\":\"java\"]");

或者使用Evaluator获取完整的eval支持:

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

Evaluator evaluator = new Evaluator(new ClassPathLoader(Thread.currentThread().getContextClassLoader()));
evaluator.setScannerType(Evaluator.SCANNER_TYPE_BRACE);
evaluator.put("list", list); // 把list对象放进Evaluator上下文中
Evaluator.Entry entry = evaluator.eval("" +
	"import java::util::stream::Collectors._\n" +
	"list.stream.filter{it > 0}.collect(toList())");
List newList = (List) entry.result;
// newList is [3, 4, 5]

6.2.3 在Java中执行Latte脚本

Latte支持脚本,脚本以源代码形式呈现。所以你可以构造一个ScriptCompiler来执行并取得脚本结果。

ScriptCompiler.Script script = scriptCompiler.compile("script", "return 1");
script.run().getResult();
// 或者 run(new String[]{...}) 来指定启动参数

ScriptCompiler有多个compile的重载,各种情况都可以方便的调用。