第八天 — 让你的 Raku grammar 紧凑一点

Make your Raku grammar compact

欢迎来到今年的 Raku Advent Calendar 的第8天!

Grammars 是使 Raku 成为一种优秀编程语言的众多因素之一。 我甚至不会尝试预测轮询的结果,以便在 grammars,Unicode 支持,并发功能,超运算符或集合语法之间进行选择,或者选择 Whatever star。 谷歌发现了自己在互联网上发布的最好的 Raku 功能列表。

img

无论如何,今天我们将讨论 Raku grammars,我将分享一些技巧,用于使 grammars 更紧凑。

1.拆分 actions

假设您正在编写 grammar 来解析 Perl 的变量声明。 您希望它与以下语句匹配:

my $s; my @a;

它们都声明了一个变量,因此我们可以制定一个通用规则来解析这两种情况。 下面是完整的程序:

grammar G {
    rule TOP {
        <variable-declaration>* %% ';'
    }

    rule variable-declaration {
        | <scalar-declaration>
        | <array-declaration>
    }

    rule scalar-declaration {
        'my' '$' <variable-name>
    }

    rule array-declaration {
        'my' '@' <variable-name>
    }

    token variable-name {
        \w+
    }
}

class A {
    has %!var;

    method TOP($/) {
        dd %!var;
    }

    method variable-declaration($/) {
        if $<scalar-declaration> {
            %!var{$<scalar-declaration><variable-name>} = 0;
        }
        elsif $<array-declaration> {
            %!var{$<array-declaration><variable-name>} = [];
        }
    }
}

G.parse('my $s; my @a;', :actions(A.new));

我不解释这个程序的每一点; 如果您有兴趣,可以在最近的 Amsterdam.pm 会议上观看80分钟的视频

现在感兴趣的对象是规则 variable-declaration 及其相应的 action。

该规则包含两个选项:是否声明了标量或数组。 该 action 还在选项之间进行选择,并使用 if-else 块执行该 action 操作。 Raku 允许你省略布尔条件周围的括号,但是,整个结构仍然很大。 例如,想想如果添加哈希声明,则需要添加另一个 elsif 分支。

为每个子分支分别采取 action 操作会更清楚:

method scalar-declaration($/) {
    %!var{$<variable-name>} = 0;
}

method array-declaration($/) {
    %!var{$<variable-name>} = [];
}

现在,每个方法的主体包含单行代码,你可以立即看到它正在做什么。 更不用说它变得不那么容易出错了。

在我们继续讨论下一个技巧之前,你可能需要实现另一个优化:my 关键字出现在任一声明中,因此请使用非捕获括号并将公用字符串从它们之中移出:

rule variable-declaration {
    'my' [
        | <scalar-declaration>
        | <array-declaration>
    ]
}

rule scalar-declaration {
    '$' <variable-name>
}

rule array-declaration {
    '@' <variable-name>
}

使用 multi 方法

让我们改进 grammar 以允许使用目标语言进行赋值:

my $s; my @a; $s = 3; $a[1] = 4;

请注意,赋值是以 Perl 5 样式完成的,数组元素为 sigil。 有了这个,可以使用以美元开头的单个规则来完成赋值:

grammar G {
    rule TOP {
        [
            | <variable-declaration>
            | <assignment>
        ]
        * %% ';'
    }

    # . . .

    rule assignment {
        '$' <variable-name> <index>? '=' <value>
    }

    rule index {
        '[' <value> ']'
    }

    token value {
        \d+
    }
}

因此,assignment action 操作必须推断出它目前正在处理的赋值类型。

同样,您可以使用我们的老朋友,action 操作中的 if-else 块。 根据索引的存在,您可以确定这是一个简单的标量还是数组的元素:

method assignment($/) {
    if $<index> {
        %!var{$<variable-name>}[$<index><value>] = +$<value>;
    }
    else {
        %!var{$<variable-name>} = +$<value>;
    }
}

此代码也可以轻松简化,但这次使用 multi 方法:

multi method assignment($/ where !$<index>) {
    %!var{$<variable-name>} = +$<value>;
}

multi method assignment($/ where $<index>) {
    %!var{$<variable-name>}[$<index><value>] = +$<value>;
}

where 子句允许 Raku 决定哪个候选方法在给定情况下更适合。

另请注意在第二个 multi 方法中如何使用 <value> 键两次。 <value> 的每个条目指的是目标代码的不同部分:一个用于索引值,另一个用于右侧值。

3. 让 Perl 完成这项工作

有时,Perl 可以为我们完成工作,特别是如果你想实现 Perl 熟悉的东西。 例如,让我们在赋值中允许不同类型的数字:

my $a; my $b; $a = 3; $b = -3.14;

在 grammar 中引入浮点数比较容易:

token value {
    | '-'? \d+
    | '-'? \d+ '.' \d+
}

您想添加其他类型的数字,请参阅 perl.com 上的文章。 现在,我们可以用上面两个选项限制 grammar,因为这足以阐明这个技巧。

如果您使用更改运行代码,您可能会对获得所需结果感到惊讶。 两个变量都接收值:

Hash %!var = {:a(3), :b(-3.14)}

在这两种情况下,都触发了相同的 action 操作:

multi method assignment($/ where !$<index>) {
    %!var{$<variable-name>} = +$<value>;
}

在赋值的右侧,我们看到 +$<value>,这是从 Match 对象转换为数字的类型。 grammar 将 3-3.14 放在 $<value> 中,两者都作为字符串。 + 这个一元运算符尝试将字符串转换为数字。 两个字符串都是有效数字,因此 Raku 不会抱怨。

自己编写代码将字符串转换为数字会更加困难,因为需要考虑数值的所有不同形式。 要了解 Raku 知道的其他格式,请查看 Raku grammarnumish 标记的定义:

token numish {
    [
    | 'NaN' >>
    | <integer>
    | <dec_number>
    | <rad_number>
    | <rat_number>
    | <complex_number>
    | 'Inf' >>
    | $<uinf>='∞'
    | <unum=:No+:Nl>
    ]
}

如果您在自己的 grammar 中允许任何上述类型,Perl 将能够为您转换它们。

4. 使用 multi-rules 和 multi-tokens

它不仅是方法,也可以是 multi-things。 grammar 的规则和标记也是方法,您也可以创建它们的多个变体。

让我们更新我们的 grammar,以允许在赋值的右侧使用数学表达式:

my $a; $a = 6 + 5 * (4 - 3);

这里的新问题是解析表达式并处理运算符优先级和括号。 您可以通过以下方式描述任何表达式:

1、表达式是由 +- 分隔的项的序列。
2、上一个规则中的任何项都是由 */ 分隔的项的序列。
3、括号内的任何内容都是另一个表达式,因此请转到规则1。

话虽如此,您最终会得到以下 grammar 变更:

grammar G {
    # . . .

    rule assignment {
        '$' <variable-name> <index>? '=' <expression>
    }

    multi token op(1) {
        '+' | '-'
    }

    multi token op(2) {
        '*' | '/'
    }

    rule expression {
        <expr(1)>
    }

    multi rule expr($n) {
        <expr($n + 1)>+ %% <op($n)>
    }

    multi rule expr(3) {
        | <value>
        | '(' <expression> ')'
    }

    # . . .
}

这里,rules 和 tokes 都是 multi 方法,它采用反映表达式深度的单个整数值。 操作符也是如此:在第一级,你期望 +- ,在第二级 - */

不要忘记 Raku 中的 multi 方法(以及 multi-subs)可以基于常量进行调度,这就是为什么你可以, 例如, 使用你在 multi token op(2) 中看到的签名。

expr($n) 规则通过 expr($n + 1) 递归定义。 $n 达到3时递归停止,Raku 选择最后一个候选 multi rule expr(3)

让我懒惰,并使用以前的建议让 Perl 计算表达式:

multi method assignment($/ where !$<index>) {
    use MONKEY-SEE-NO-EVAL;
    %!var{$<variable-name>} = EVAL($<expression>);
}

一般来说,我建议只在神奇的圣诞节期间使用 EVAL。 在今年余下的时间里,请自己计算表达式并使用抽象语法树和 makemade 方法对儿保存部分结果。 例如,请参阅此处的示例

我还建议一些额外的阅读,以便更好地了解如何使用 multiproto 关键字:

1、Raku 中的 proto 关键字
2、有关 Raku 中 proto 关键字的更多信息

此时此刻,令人惊叹的 Raku grammar 之旅就要结束了。 你可以在 GitHub 上找到今天帖子的完整例子。 祝你读完其余的 Perl Advent Calendars,祝你愉快!

comments powered by Disqus