第一天 – Raku 鬼精灵: 圣诞节实用指南

A Practical Guide to Ruining Christmas

第一天 – Raku 鬼精灵: 圣诞节实用指南

看看他们!同事、朋友和亲近的家人都在开心地笑着。他们都在享受着使用 Raku 的 6.c “圣诞”版编程的乐趣。给力的并发原语, 核心文法, 还有非常棒的对象模型。它让我印象深刻!

但是等一下… 就一秒。我有个想法。一个可怕的想法。我想到了一个鬼主意! 我们可以在他们的"圣诞"上捣乱。需要的只有一点花招。哈哈哈哈哈哈!!

欢迎来到 2017 年的 Raku 圣诞日历!每天,从今天直到圣诞节,我们都会有一篇很赞的关于 Raku 的博客推送到你面前。

今天,我们会展示我们淘气的一面并且故意地做些淘气的事情。确实,这有点用,但是淘气点更快乐。我们开始吧!

But True does False

你听过 but 操作符吗?一个好玩的东西:

say True but False ?? 'Tis true' !! 'Tis false';
# OUTPUT: «Tis false␤»

my  $n = 42 but 'forty two';
say $n;     # OUTPUT: «forty two␤»
say $n + 7; # OUTPUT: «49␤»

它是一个中缀操作符,它首先拷贝它左边的对象,然后把它右边提供的 role 混进这个拷贝中:

my $n = 42 but role Evener {
    method is-even { self %% 2 }
}
say $n.is-even; # OUTPUT: «True␤»
say $n.^name;   # OUTPUT: «Int+{Evener}␤»

上面的前俩个例子中的那些不是 roles。but 操作符有种便捷的写法:如果 but 右边的东西不是 role,它就会给你创建一个!那个 role 只会有一个方法,以右侧对象的 ^name 命名,并且那个方法只会简单地返回那个给定的对象。因此,这…

put True but 'some boolean'; # OUTPUT: «some boolean␤»

等价于:

put True but role {
    method ::(BEGIN 'some boolean'.^name) {
        'some boolean'
    }
} # OUTPUT: «some boolean␤»

.^name 在我们的字符串上返回 Str, 因为它是一个 Str 对象:

say 'some boolean'.^name;  # OUTPUT: «Str␤»

所以那个 role 提供了一个叫做 Str 的方法, 在非 Str 对象上调用该方法以获取字符串值的输出, 使我们的布尔值变成修改过的字符串化的表示。

举个例子,字符串 0 在 Rakudo Raku 中是 True 但是在 Perl 5 中是 False。使用 but 操作符, 我们能修改字符串的行为,让它表现的像 Perl 5 版本那样:

role Perl5Str {
    method Bool {
        nextsame unless self eq '0';
        False
    }
}
sub perlify { $^v but Perl5Str };

say so perlify 'meows'; # OUTPUT: «True␤»
say so perlify '0';     # OUTPUT: «False␤»
say so perlify '';      # OUTPUT: «False␤»

Perl5Str 这个 role 提供了供 so 子例程调用的 .Bool 方法。在这个方法里面,我们使用 nextsame 子例程重新分派了原来的 .Bool 方法,除非那个字符串是 0, 那时我们仅仅返回 False

but 操作符有一个兄弟: does 中缀操作符。它们的行为相似,但是它不拷贝。

my $o = class { method stuff { 'original' } }.new;
say $o.stuff;  # OUTPUT: «original␤»

$o does role { method stuff { 'modded' } };
say $o.stuff; # OUTPUT: «modded␤»

程序中的某些东西是全局可访问的,而在有些实现(例如 Rakudo)中,某些常量被缓存了。这意味着我们可以在程序的不同部分变得很淘气,而那些圣诞节的庆祝者们甚至不知道发生了什么!

假如我们覆写了 prompt 子例程的读方法会怎么样?他们喜欢圣诞节?我们来给他们一些圣诞树:

$*IN does role { method get { "🎄 {callsame} 🎄" } }

my $name = prompt "Enter your name: ";
say "You entered your name as: $name";

# OUTPUT
# Enter your name: (typed by user:) Zoffix Znet
# You entered your name as: 🎄 Zoffix Znet 🎄

即使我们把代码粘贴到模块中该覆盖也会起作用。 我们也可以把它提升一个档次,弄乱枚举和缓存的常量,虽然这个顽皮的举动可能将无法跨越模块边界和其他特定实现的缓存失效:

True does False;
say 42 ?? "tis true" !! "tis false";
# OUTPUT: «tis true␤»

到目前为止,这还没有达到想要的效果,但是让我们试着把我们的数字强制为 Bool 值:

True does False;
say 42.Bool ?? "tis true" !! "tis false";
# OUTPUT: «tis false␤»

我们做到了! 而现在,对于最后的格林奇 - 值得接触,我们将混淆数字计算的数值结果。 Rakudo 缓存 Int 常量。 当用不同类型的数字计算时,Infix + 运算符也使用 internal-ish-ish .Bridge 方法。 所以,让我们重写常量上的 .Bridge 来返回一些奇怪的东西:

BEGIN 42 does role { method Bridge { 12e0 } }
say 42 + 15;   # OUTPUT: «57␤»
say 42 + 15e0; # OUTPUT: «27␤»

这是善意的邪恶,肯定会毁了圣诞节,但这只是开始…

Wrapping It Up

没有包装的礼物,会是什么样的圣诞节? 哦,对于礼物,我们将有 Raku 的 Routine 类型的 .wrap 方法包装他们,哦,太好了。

use soft;
sub foo { say 'in foo' }
&foo.wrap: -> | {
    say 'in the wrap';
    callsame;
    say 'back in the wrap';
}
foo;

# OUTPUT:
# in the wrap
# in foo
# back in the wrap

我们使用 use soft 编译指令来防止不必要的内联,否则这些内联会干扰我们的包装。然后,我们使用一个我们想要包装成一个名词的例程,通过它和 sigil 来使用它,并调用带有一个Callable.wrap 方法。

给定的 Callable 的签名必须与包装的例程(或其 proto 原型,如果它是一个 multi)兼容;否则我们将无法正确调度程序并使用参数调用包装器。在上面的例子中,我们只是使用匿名的 Capture|)来接受所有可能的参数。

Callable 里面,我们有两个 say 调用,并使用 callsame 例程来调用下一个可用的调度候选者,这正好是我们原来的例程。这很方便,因为我们试图在包装器中按照它的名字来调用 foo ,我们将从头开始调度,导致无限的调度循环。

既然方法是 Routine,我们也可以把它们包装起来。我们可以使用 .^lookup 元方法来获取 Method 对象:

IO::Handle.^lookup('print').wrap: my method (|c) {
    my &wrapee = nextcallee;
    wrapee self, "🎄 Ho-ho-ho! 🎄\n";
    wrapee self, |c
};

print "Hello, World!\n";

# OUTPUT:
# 🎄 Ho-ho-ho! 🎄
# Hello, World!

在这里,我们从 IO::Handle 类型中获取 .print 方法,然后包装它。我们希望在方法内部使用 self,所以我们使用独立的方法(my method …)来代替块或子例程。我们想使用 self 的原因是能够调用我们包装的方法来打印我们的 Christmassy 消息。因为我们的方法是分离的,callwith 和相关的例程将需要与其他参数一起自我馈送,以确保我们继续分派给正确的对象。

在 wrap 中,我们使用 nextcallee 例程来获得原始的方法。如果它是一个 multi,我们将得到 proto,而不是一个与原始参数最匹配的特定候选者,所以相比传统的例程,下一个 candidate ordering 候选排序在 wrap 中略有不同。我们把 nextcallee 放到一个变量中,因为我们想多次调用它,调用它将例程从调度栈中移出。在第一个调用中,我们打印了我们的 Christmass 信息,而在第二个调用中,我们只是 slip 我们的原始参数的 Capture|c),完成了原来想要发生的调用。

感谢 .wrap,我们可以改变甚至完全重新定义子程序和方法的行为,当你的朋友尝试使用它们的时候肯定会很快乐。哈哈哈!

看不见的斗篷

我们到目前为止所玩的技巧是非常可怕的,但它们太明显,太…明显。 由于 Raku 具有极好的 Unicode 支持,所以我认为我们应该搜索大量的 Unicode 字符来获得一些有趣的恶作剧。 特别是,我们正在寻找不是空白的隐形字符。 我们的目的只有一个就足够了,但是这四个在我的电脑上是相当隐蔽的:

[] U+2060 WORD JOINER [Cf]
[] U+2061 FUNCTION APPLICATION [Cf]
[] U+2062 INVISIBLE TIMES [Cf]
[] U+2063 INVISIBLE SEPARATOR [Cf]

Raku 支持可以由任何字符组成的自定义术语和操作符,除了空格之外。 例如,这是我的专利耸肩操作符:

sub infix:<¯\(°_o)/¯> {
    ($^a, $^b).pick
}

say 'Coke' ¯\(°_o)/¯ 'Pepsi';
# OUTPUT: «Pepsi␤»

这是一个由非标识字符组成的术语(我们也可以在定义中使用真实的字符):

sub term:«\c[family: woman woman boy boy]» {
    '♫ We— are— ♪ faaaamillyyy ♬'
}
say 👩👩👦👦;
# OUTPUT: «♫ We— are— ♪ faaaamillyyy ♬»

用我们看不见的非空白字符,我们可以使无形的操作符和术语!

sub infix:«\c[INVISIBLE TIMES]» { $^a × $^b }
my \r = 42;

say "Area of the circle is " ~ πr²;
# OUTPUT: «Area of the circle is 5541.76944093239␤»

让我们来创建一个 Jolly 模块,它将导出一些不可见的术语和操作符。 然后我们把它们撒在我们的 Christmassy朋友的代码中:

unit module Jolly;

sub   term:«\c[INVISIBLE TIMES]» is export { 42 }
sub  infix:«\c[INVISIBLE TIMES]» is export {
    $^a × $^b
}
sub prefix:«\c[INVISIBLE SEPARATOR]» (|)
    is looser(&[,]) is export
{
    say "Ho-ho-ho!";
}

我们对术语和中缀操作符使用了相同的字符。 这很好,因为 Raku 对操作符有相当严格的期望,反之亦然,所以它会知道我们什么时候使用该术语或何时使用中缀操作符。 下面是由此产生的 Grinch 代码,以及它产生的输出:

say 42⁢⁢;

# OUTPUT:
# 1764
# Ho-ho-ho!

这将确保调试的乐趣! 以下是该行代码中的字符列表,供您查看我们使用隐形好东西的位置:

.say for '⁣say 42⁢⁢;'.uninames;

# OUTPUT:
# INVISIBLE SEPARATOR
# LATIN SMALL LETTER S
# LATIN SMALL LETTER A
# LATIN SMALL LETTER Y
# SPACE
# DIGIT FOUR
# DIGIT TWO
# INVISIBLE TIMES
# INVISIBLE TIMES
# SEMICOLON

Ho-Ho-Ho

圣诞节时的生产力下降到停滞状态。 人们心中都有节日和新年。 在所有代码中看到大量的 TODO 注释并不让我感到惊讶。 但是如果我们能够发现并投诉他们呢? 只要有人感到懒惰,没有什么比 Grinch 更像编程了!

Raku 有俚语。 这是一个实验性的功能,目前还没有一个官方支持的接口,但是,对于我们的目的来说,它会做的很好。

使用俚语,可以在词法上改变 Raku 的文法,并引入语言特性和行为,就像 Raku 核心开发者一样:

BEGIN $*LANG.refine_slang: 'MAIN',
    role SomeExtraGrammar {
        token term:sym<meow> {
            'This is not a syntax error'
        }
    },
    role SomeExtraActions {
        method EXPR (Mu $/) {
            say "Parsed expression: " ~ $/;
            nextsame
        }
    }

This is not a syntax error;
say 'hehe'

# OUTPUT:
# Parsed expression: This is not a syntax error
# Parsed expression: 'hehe'
# Parsed expression: say 'hehe'
# hehe

俚语功能的“实验性”部分主要在于不得不依靠 core Grammarcore Actions 的结构;目前没有官方保证这些将保持不变,这使得俚语变得脆弱。

对于我们调皮的 Grinchy 技巧,我们将修改注释的行为,如果我们读取代码来追踪调用 the comment token 的代码,我们会发现它实际上是重新定义的 ws token 的一部分,正如您可能从每天都知道的 Raku 文法除其他外,负责语法规则中的空白匹配。

这个问题稍微复杂一些,因为 ws 是一个基石标记,与 comp_unitstatementliststatement 一起,它不能在 mainline(例程和块之外的代码)中修改。原因是在使用这些令牌的股票版本解析主线之后,俚语被加载。statement token 内的标记甚至可以在 mainline 中更改,因为 statement 标记会 reblesses 文法,但是 ws 不会获得如此的奢侈。

既然我们已经开始深入到底了……足够的话了!我们来写代码吧:

BEGIN $*LANG.refine_slang: 'MAIN', role {
    token comment:sym<todo> {
        '#' \s* 'TODO' ':'? \s+ <( \N*
        { die "Ho-ho-ho! I think you were"
            ~ " meant to finish " ~ $/ }
    }
}

sub business-stuff {
    # TODO: business stuff
}

# OUTPUT:
# ===SORRY!===
# Ho-ho-ho! I think you were meant to finish business stuff

我们使用 BEGIN phaser 在编译时进行俚语修改,因为我们试图影响如何进一步编译。

我们添加了一个新的 proto 标记: comment:sym<todo> 到核心 Raku 文法,匹配类似于常规注释匹配的内容,除了它还寻找我们的 Christmassy 朋友决定离开的 TODO\N* 原子捕获用户在 TODO 之后键入的字符串,匹配捕获标记指示编译器将存储在 $/ 变量中的匹配对象内的捕获文本中的以前匹配的东西排除在外。

在 token 的末尾,我们简单地使用一个代码块来告诉用户完成他们的 TODO 的消息。 很狡猾!

由于我们宁愿用户不注意我们的诡计,让我们将俚语粘贴到目标代码将要加载的模块中。 我们只是稍微调整一下原来的代码:

# File: ./Jolly.pm6
sub EXPORT {
    $*LANG.refine_slang: 'MAIN', role {
        token comment:sym<todo> {
            '#' \s* 'TODO' ':'? \s+ <( \N*
            { die "Ho-ho-ho! I think you were"
                ~ " meant to finish " ~ $/ }
        }
    }

    Map.new
}

# File: ./script.p6
use lib <.>;
use Jolly;

sub business-stuff {
    # TODO: business stuff
}

# OUTPUT:
# ===SORRY!===
# Ho-ho-ho! I think you were meant to finish business stuff

我们希望俚语在脚本的编译时运行,而不是在模块中,所以我们删除了 BEGIN phaser,而是将代码固定在 sub EXPORT 中,在脚本编译过程中使用该模块时运行。 Map.new 就是我喜欢在 EXPORT sub 中写 {},以表示我们不希望导出任何符号。 在我们的脚本中,我们现在只需要使用模块,俚语被激活。真棒!

结论

今天,我们开始淘气的 Grinches 2017 年的 Raku 的降临日历和搞乱用户的程序。 我们使用 butdoes 操作符来改变对象。 包装的方法和子程序与我们的自定义例程,实现额外的功能。 做出隐形术语和操作符。 甚至突变语言本身来做我们的竞标。

在接下来的 23 天里,我们会看到更多的 Raku Advent 文章,所以一定要回头看看。 也许,到这一切的尽头,我们的 Grinchy 心将长大三个尺寸…

-Ofun

Raku 

comments powered by Disqus