Skip to main content

Expressions and Operators(表达式与运算符)

Expressions and Operators(表达式与运算符)

ProtoGraph 中的表达式提供了生成、使用和组合数据及控制流的方法。所有表达式都求值为一个记录(record):一个命名的元组,即一个有序的键/值对列表。记录中的键是名称,值是某些数据。ProtoGraph 中的记录还有一个 this 字段,用于指定记录的默认值。单独使用记录名称即可提供 this。使用点号 . 可以访问其他成员。

下面是一个记录类型的示例。这不是实际的 ProtoGraph 代码,而是描述了被调用模块返回的记录类型:

TimeDelayWithData: {
    // 调用节点本身提供
    this: AsyncOperation
    // 使用 `.DelayedValue` 访问延迟的值
    DelayedValue: T
}

RayPlaneIntersection: {
    // 只有一个默认值
    // 节点结果本身
    this: float3
}

Unpack_Float4: {
    // 没有 `this`
    // 必须使用 `.` 获取字段
    X: float
    Y: float
    Z: float
    W: float
}

// 字面量是只有 `this` 的记录
"MyString": {
    this: string
}

DataModelBooleanToggle: {
    this: bool // 当前状态
    Set: Operation
    Reset: Operation
    Toggle: Operation
}

Literal(字面量)

表示 ProtoFlux 中常量值的输入节点。示例包括:

  • 布尔值truefalse
  • 整数1-2
  • 浮点数3.142e30
  • 字符'A''\n'
  • 字符串"Hello World!""123-465-7890"

信息:默认情况下,数字类型被解释为 32 位整数或 32 位浮点数(如果使用小数点或指数)。其他数字类型可以通过添加后缀来创建,例如 123uL 表示 123 作为无符号长整数(64 位)。有关完整表格,请参见类型与变量章节。

在内部,字面量是只有 this 值的记录。

Identifier(标识符)

一个由字母数字组成的大写字符串,代表程序中的其他内容。这些可以引用 Froox 引擎中的模块/节点或用户定义的值。

  • Froox Node: ImpulseDisplay(脉冲显示)
  • Value: MyValueX

Module Invocation(模块调用)

通过编写模块的标识符可以调用一个模块(创建节点)。这将在没有任何输入连接的情况下创建该模块。输入可以在 ( ) 内指定。模块的输入之间用逗号分隔。

输入可以有标签或无标签提供,但位置参数必须放在任何命名参数之前。通过使用命名参数,您可以以不同于模块声明的顺序指定参数。任何未提供的参数将保持未连接状态;但是,未连接的参数仅允许用于 Froox 引擎定义的节点。此外,所有脉冲参数必须是命名参数。

模块调用的值是其输出的记录。可以使用点号 . 字段访问运算符访问输出。模块可能公开一个默认值,记作 this,可以通过使用模块名称本身(不带点号)来访问。这对应于 ProtoFlux 节点上的 * 端口。

If; // 没有任何连接的 If
If(); // 等同于 `If`
If(Condition=true, OnTrue=ImpulseDisplay, OnFalse=ImpulseDisplay); // 全部命名
If(false, OnFalse=ImpulseDisplay); // 部分位置,部分命名
If(OnTrue=ImpulseDisplay, Condition=false); // 命名,不同顺序 

Module Aliases(模块别名)

一些常见模块有一个关键字,您可以使用更简单的语法来调用它们:

// ValueDisplay<_>(true) 或 ObjectDisplay<_>(...) 
display(true); 

Value Binding(值绑定)

可以使用 = 将值绑定到名称上,就像在数学中一样。整个表达式的值将是一个空记录。当一个值绑定到名称时,该名称会被添加到当前上下文的范围内,并且该名称可以在该上下文的后续表达式中用作标识符。

通过指定多个名称,可以从记录中解包多个值。解包时,名称的数量必须与记录中的值数量匹配,并且顺序必须相同。

PackedNumbers = Pack_Int2(100, 200);
ANumber, BNumber = Unpack_Int2(PackedNumbers);
AString = "String";
AValue = 1 + ANumber;
ManyStrings = MultiplyString(AString, AValue);

Type Annotation(类型注解)

注解通常是可选的,为类型检查器和程序未来的维护者提供关于值应该是什么类型的额外信息。在编译时,如果绑定的值与注解的类型不匹配,编译器会警告用户。在这种情况下,类型检查器会假定注解的类型是实际类型,并继续检查程序的其余部分。如果您遇到类型错误问题,添加注解有助于验证您的假设是否正确。它们也可以作为活跃文档和验证测试的一种形式。

以下是之前的代码,但添加了注解:

PackedNumbers: int2 = Pack_Int2(100, 200);
ANumber: int, BNumber: int = Unpack_Int2(PackedNumbers);
AString: string = "String";
AValue: int = 1 + ANumber;
ManyStrings: string = MultiplyString(AString, AValue);

Context Expression(上下文表达式)

当您需要将多个表达式组合成一个表达式时,使用上下文表达式。上下文表达式用 { } 包围其表达式,并用分号分隔包含的表达式。在大括号之前,可以指定上下文表达式的类型;如果未提供,则默认为 froox context

整个上下文表达式的值是上下文中的最后一个表达式。此外,与最终表达式暴露的值不冲突的具体值(模块或字面量的 this)的内部绑定也将包含在上下文表达式的结果中。

module Context
out this: string
out A: string
out B: string

where { // 整个模块主体只是一个表达式
A = "A"; // 此上下文中有 3 个表达式
B = "B";
ConcatenateString(A, B);
} // 此上下文表达式 '变为' { this="AB"; A="A"; B="B" }

// 在另一个模块中,我们可以:
MyContext = Context();
MyContext; // => "AB"
MyContext.A; // => "A"
MyContext.B; // => "B"

Switch(分支)

Switch 表达式提供了一种灵活的语法,以流畅的模式匹配风格来管理链式和分支脉冲控制流。大多数 switch 表达式可以重写为显式的模块参数,但某些用例需要 switch。此外,switch 有助于使控制流更易于理解。

switch 表达式以 switch 关键字开头,后跟一个节点调用表达式。该表达式需要接收一个或多个脉冲作为输入参数。在表达式之后,使用 | <pattern> [<node>] |> <continuation_expression> 指定一个或多个延续匹配。指定的模式必须是模块的参数名称之一。模式后的节点是可选的,它将 switch 表达式的结果值绑定到延续表达式中的名称(类似于 = 将值绑定到名称,以便在上下文中的后续表达式中使用)。延续表达式是将为此模式执行的操作。

// 等同于 If(true, OnTrue=ImpulsDisplay, OnFalse=ImpulseDisplay)
switch If(Condition=true)
| OnTrue  |> ImpulseDisplay
| OnFalse |> ImpulseDisplay;

switch For(Count=100)
| LoopStart |> ImpulseDisplay
| LoopEnd   |> ImpulseDisplay
| LoopIteration ForNode |> // 需要 switch 才能获取 Iteration
    switch (SomeVariable <- ForNode.Iteration)
    | OnWritten |> ImpulseDisplay
    | OnFail    |> ImpulseDisplay;

List(列表)

多个值可以在编译时使用列表组合在一起。列表的元素用 [ ] 包围,并用逗号分隔。与记录不同,列表元素使用其位置(0 基索引)通过 [ ] 进行索引。整个列表表达式的值是一个记录,其中 this 是元素列表。

List1: int[] = [1, 2, 3];
One: int = List1[0];

列表是固定长度的,并且必须在编译时已知。此外,在索引列表时,索引必须是非负整数文字。

Good2 = List1[1]; // 正确
Two = 1 + 1;
Bad3 = List[Two]; // 无效,不是字面量 

信息:对于返回列表的模块,使用 <FieldName>.Count=<length> 作为参数指定返回列表的大小。此处指定的长度(与索引一样)必须是有效的非负整数文字。

ImpulseDemultiplexer(Operations.Count=5) 

undefined(未定义)

undefined 是一个用作占位符表达式的关键字。undefined 可以用在任何表达式的位置。如果检测到未定义的值,编译器会发出警告。

undefined 在开发过程中如果需要占位符(如代码中的 TODO)可能会很有用,但完整的程序不应留下任何未定义的值。

Operators(运算符)

运算符是作为不同模块简写形式的符号。运算符具有特定的优先级(运算顺序);有关运算符优先级顺序的完整列表,请参阅运算符参考

Pipe(管道)

-> 是一个前向组合运算符。它在其左侧接受一个值,在其右侧接受一个模块。然后将左侧的值作为第一个位置参数应用到右侧的模块。这使您能够以更流畅的样式编写管道。

Uri1 =
    LocalUser
    ->UserRootSlot
    ->GetSlotName
    ->TrimString
    ->ReplaceFirstSubstring(SearchFor=".", ReplaceWith="/")
    ->ConcatenateString("/Data")
    ->StringToAbsoluteURI;

// 等同于以上,但没有使用 ->
Uri2 = 
    StringToAbsoluteURI(
        ConcatenateString(
            ReplaceFirstSubstring(
                TrimString(
                    GetSlotName(UserRootSlot(LocalUser))), 
                SearchFor=".", 
                ReplaceWith="/"), 
            "/Data"));

Write(写入)

写入(或赋值)运算符可用于将变量更新为新值。这是一个脉冲节点。

store MyVar: int;
SomeValue = 25;

// 启动时(当生成时),将 25 写入存储的变量
switch OnStart
| Trigger |> MyVar <- SomeValue;

Arithmetic(算术)

+-*/mod 等运算符允许您使用更常见的数学符号来进行数值运算,而不是使用完整的模块名称。- 也可以用作前缀运算符表示取反。

// 数学运算符遵循标准的运算顺序 
NumberCalculation = 10 / 2 + 5 * 9 + 1 - 2 

基本数值运算符也针对各种常见结构进行了重载。

M1: float3 = pack(1.0, 0.0, 1.0);
M2: float3 = pack(1.9, -2.9, 0.1);
MM: float3x3 = PackRows_Float3x3(M1, M2, M1);
M3: float3 = MM * M2 - M1 + 2.0 / M2;

Comparison(比较)

==!=><<=>= 是二元中缀比较运算符。

Boolean(布尔)

andorxorxnornornand 是常见的二元布尔运算,可用作中缀运算符。not 是一个一元前缀运算符。它们也可用作支持数值类型的按位运算符。

Other(其他)

  • 三元/条件if <condition> then <value_on_true> else <value_on_false>
  • 空合并<possibly_null> ?? <fallback_value>