Perl 6圣诞月历 (2013)

2013

Heredocs, Theredocs, Everywheredocs docs

So let’s say you’ve got a bit of documentation to print out, a help statement perhaps. You could use an ordinary string, but it always looks like something you really shouldn’t be doing.

sub USAGE {
    say "foobar Usage:
./foobar <args> <file>

Options:

...
";
}

Perl 6 has a much better idea for you, fortunately: heredocs! They work a bit differently from Perl 5, and are now invoked using the adverb :heredoc on quoting constructs:

say q:heredoc/END/;
Hello world!
END

When you use :heredoc, the contents of the string are no longer the final contents; they become the string that signifies the end of a heredoc. q”END” results in the string “END”, q:heredoc”END”results in everything before the next END to appear on its own line.
You will have also noticed that heredocs only start on the next possible line for them to start, not immediately after the construct closes. That semicolon after the construct never gets picked up as part of a heredoc, don’t worry :) .
The :heredoc adverb is nice, but it seems a bit long, doesn’t it? Luckily it has a short form, :to, which is much more commonly used. So that’s what we’ll be using through the rest of the post.

say q:to"FIN";
Hello again.
FIN

You can use any sort of string for the delimiter, so long as there’s no leading whitespace in it. A null delimiter (q:to//) is fine too, it just means you end the heredoc with two newlines, effectively a blank line.
And yes, delimiters need to be on their own line. This heredoc never ends:

say q:to"noend";
HELLO WORLD noend

A note about indentation: look at this heredoc

say q:to[finished];
  Hello there
    everybody
finished

Which of those three heredoc lines decides how much whitespace is removed from the beginning of each line (and thus sets the base level of indentation)? It’s the line with the end delimiter, “finished” in the last example. Lines with more indentation than the delimiter will appear indented by however much extra space they use, and lines with less indentation will be as indented as the delimiter, with a warning about the issue.
(Tabs are considered to be 8 spaces long, unless you change $?TABSTOP. This usually doesn’t matter unless you mix spaces and tabs for indentation anyway though.)
It doesn’t matter how much the delimiter indentation is, all that matters is indentation relative to the delimiter. So these are all the same:

say q:to/END/;
HELLO
  WORLD
END
say q:to/END/;
    HELLO
      WORLD
    END
say q:to/END/;
               HELLO
                 WORLD
               END

One other thing to note is that what quoting construct you use will affect how the heredoc contents are parsed, so

say q:to/EOF/;
$dlrs dollars and {$cnts} cents.
EOF

Interpolates nothing,

say q:to:c/EOF/;
$dlrs dollars and {$cnts} cents.
EOF

Interpolates just {$cnts} (the :c adverb allows for interpolation of just closures), and

say qq:to/EOF/;
$dlrs dollars and {$cnts} cents.
EOF

Interpolates both $dlrs and {$cnts}.
Here’s the coolest part of heredocs: using more than one at once! It’s easy too, just use more than one heredoc quoting construct on the line!

say q:to/end1/, qq:to/end2/, Q:to/end3/;
This is q.\\Only some backslashes work though\t.
$sigils don't interpolate either.
end1
This is qq. I can $interpolate-sigils as well as \\ and \t.
Neat, yes?
end2
This is Q. I can do \\ no \t such $things.
end3

Which, assuming you’ve defined $interpolate-sigils to hold the string “INTERPOLATE SIGILS”, prints out

    This is q.\Only some backslashes work though\t.
    $sigils don't interpolate either.
    This is qq. I can INTERPOLATE SIGILS as well as \ and   .
    Neat, yes?
    This is Q. I can do \\ no \t such $things.

After every end delimiter, the next heredoc to look for its contents starts.
Of course, indentation of different heredocs will help whenever you have to stack a bunch of them like this.

say qq:to/ONE/, qq:to/TWO/, qq:to/THREE/, qq:to/ONE/;
The first one.
ONE
    The second one.
    TWO
The third one.
THREE
    The fourth one.
    ONE

Which outputs:

    The first one.
    The second one.
    The third one.
    The fourth one.

(And yes, you don’t have to come up with a unique end delimiter every time. That could have been four q:to/EOF/ statements and it’d still work.)
One final note you should be aware of when it comes to heredocs. Like the rest of Perl 6 (barring a couple of small exceptions), heredocs are read using one-pass parsing (this means your Perl 6 interpreter won’t re-read or skip ahead to better understand the code you wrote). For heredocs this means Perl 6 will just wait for a newline to start reading heredoc data, instead of looking ahead to try and find the heredoc.
As long as the heredoc contents and the statement that introduces the heredoc are part of the same compilation unit, everything’s fine. In addition to what you’ve seen so far, you can even do stuff like this:

sub all-info { return q:to/END/ }

This is a lot of important information,
and it is carefully formatted.
END

(If you didn’t put the brace on the same line, it would be part of the heredoc, and then you’d need another brace on a line afterEND.)
However, things like BEGIN blocks start compiling before normal code, so trying that last one with BEGIN block fails:

BEGIN { say q:to/END/ }
This is only the BEGINning.
END

You have to put the heredoc inside the BEGIN block, with the quoting construct, in order to place them in the same compilation unit.

BEGIN {
    say q:to/END/;
    This is only the BEGINning.
    END
}

That’s it for heredocs! When should you use them? I would say whenever you need to type a literal newline (by hitting Enter) into the string. Help output from the USAGE sub is probably the most common case. The one at the beginning could easily (and more readably) be written as

sub USAGE {
    say q:to"EOHELP";
        foobar Usage:
        ./foobar <args> <file>

        Options:

        ...
        EOHELP
}

Parsing and generating recurring dates

By Moritz
There are a lot of events that are scheduled on particular days of the week each month, for example the regular Windows Patch Day on the second Tuesday of each month, or in Perl 6 land that Rakudo Perl 6 compiler release, which is scheduled for two days after the Parrot release day, which again is scheduled for the third Tuesday of the month.
So let’s write something that calculates those dates.
The specification format I have chosen looks like 3rd tue + 2 for the Rakudo release date, that is, two days after the 3rd Tuesday of each month (note that this isn’t always the same as the 3rd Thursday).
Parsing it isn’t hard with a simple grammar:

grammar DateSpec::Grammar {
    rule TOP {
        [<count><.quant>?]?
        <day-of-week>
        [<sign>? <offset=count>]?
    }
    token count { \d+ }
    token quant { st | nd | rd | th }
    token day-of-week { :i
        [ mon | tue | wed | thu | fri | sat | sun ]
    }
    token sign { '+' | '-' }
}

As you can see, everything except the day of the week is optional, so sun would simply be the first Sunday of the month, and 2 sun – 1 the Saturday before the second Sunday of the month.
Now it’s time to actually turn this specification into a data structure that does something useful. And for that, a class wouldn’t be a bad choice:

my %dow = (mon => 1, tue => 2, wed => 3, thu => 4,
        fri => 5, sat => 6, sun => 7);

class DateSpec {
    has $.day-of-week;
    has $.count;
    has $.offset;

    multi method new(Str $s) {
        my $m = DateSpec::Grammar.parse($s);
        die "Invalid date specification '$s'\n" unless $m;
        self.bless(
            :day-of-week(%dow{lc $m<day-of-week>}),
            :count($m<count> ?? +$m<count>[0] !! 1),
            :offset( ($m<sign> eq '-' ?? -1 !! 1)
                    * ($m<offset> ?? +$m<offset> !! 0)),
        );
    }

We only need three pieces of data from those date specification strings: the day of the week, whether the 1st, 2nd, 3rd. etc is wanted (here named $.count), and the offset. Extracting them is a wee bit fiddly, mostly because so many pieces of the grammar are optional, and because the grammar allows a space between the sign and the offset, which means we can’t use the Perl 6 string-to-number conversion directly.
There is a cleaner but longer method of extracting the relevant data using an actions class.
The closing } is missing, because the class doesn’t do anything useful yet, and that should be added. The most basic operation is to find the specified date in a given month. Since Perl 6 has no built-in type for months, we use a Date object where the .day is one, that is, a Date object for the first day of the month.

   method based-on(Date $d is copy where { .day == 1}) {
        ++$d until $d.day-of-week == $.day-of-week;
        $d += 7 * ($.count - 1) + $.offset;
        return $d;
    }

The algorithm is quite simple: Proceed to the next date (++$d) until the day of week matches, then advance as many weeks as needed, plus as many days as needed for the offset. Date objects support addition and subtraction of integers, and the integers are interpreted as number of days to add or subtract. Handy, and exactly what we need here. (The API is blatantly copied from theDate::Simple Perl 5 module).
Another handy convenience method to implement is next, which returns the next date matching the specification, on or after a reference date.

    method next(Date $d = Date.today) {
        my $month-start = $d.truncated-to(month);
        my $candidate   = $.based-on($month-start);
        if $candidate ge $d {
            return $candidate;
        }
        else {
            return $.based-on($month-start + $month-start.days-in-month);
        }
    }
}

Again there’s no rocket science involved: try the date based on the month of $d, and if that’s before $d, try again, but with the next month as base.
Time to close the class :-).
So, when is the next Rakudo release? And the next Rakudo release after Christmas?

my $spec = DateSpec.new('3rd Tue + 2');
say $spec.next;
say $spec.next(Date.new(2013, 12, 25));

Output:
2013-12-19
2014-01-23

The code works fine on Rakudo with both the Parrot and the JVM backend.
Happy recurring hollidates!

Hashes and pairs

Hashes are nice. They can work as a kind of “poor man’s objects” when creating a class seems like just too much ceremony for the occasion.

my $employee = {
    name => 'Fred',
    age => 51,
    skills => <sweeping accounting barking>,
};

花括号可以省略:

my %employee =
    name => 'Fred',
    age => 51,
    skills => <sweeping accounting barking>,
;

散列的最后一项的末尾可以添加一个逗号。
Hashes make great “configuration objects”, too. You want to pass some options into a routine somewhere, but the options (for reasons of future compatibility, perhaps) need to be an open set.

my %options =
    rpm => 440,
    duration => 60,
;
$centrifuge.start(%options);

Actually, we have two options with that last line. Either we pass in the whole hash like that, and the method in the centrifuge class will need to look like this:

method start(%options) {
    # probably need to start by unpacking options here
    # ...
}

Or we decide to “gut” the hash as we pass it in, effectively turning it into a bunch of named arguments:

$centrifuge.start( |%options );  # means :rpm(440), :duration(60)

强制参数

method start(:$rpm!, :$duration!) {
    # ...
}

(In this case, we probably want to put in those exclamation marks, to make those named parameters obligatory. Unless we’re fine with providing some of them with a default, such as :$duration = 120.)

前缀操作符 prefix:<|> 其实叫做“展开” 或 “插值”。 在 Perl 6 中, 数组被展开为位置参数,散列被展开为命名参数。

my @args = "Would you like fries with that?", 15, 5;
say substr(|@args);    # fries

my %details = :year(1969), :month(7), :day(16),
              :hour(20),     :minute(17);
my $moonlanding = DateTime.new( |%details );

Perl 6 散列的项真的很像命名参数。当然它们不是, 它们只是散列中的键和值。但确实太像了。我们有 2 种语法来写一个散列的项。 一个是 胖箭头 语法:

my %opts = blackberries => 42;

一个是命名参数语法:

my %opts = :blackberries(42);

他们俩各有千秋。 后者比较 nice 的是它能够混合变量:

my $blackberries = 42;
my %opts = :$blackberries;   # 等价于  :blackberries($blackberries)

如果不重复单词 blackberries,使用 胖箭头语法就做不到了。

所以散列的项(一个键+一个键值)在 Perl 6 中变的更像一个东西。
在 Perl 6 中, 通过使用 :blackberries(42) 语法 或 :$blackberries 语法,让散列的项更突出。不仅如此, 把散列传递到子例程中时也是一项一项传递烦人,这让项更加突出。
最后,我们妥协了,意识到这样一串散列的项可以作为一个单位, 所以我们给它一个名字叫 Pair。散列是由一串串 Pair 对象(无序的)组成的。
所以,

say %employee.elems;

打印出 “3″… 这就是散列 %employee 中 Pair 对象的数量。

But in the end, Pair objects even turn out to have a sort of independent existence, graduating from their role as hash constituents. For, example, you can treat them as cons pairs and simulate Lisp lists with them:

my $lisp-list = 1 => 2 => 3 => Nil;  # it's nice that infix:<< => >> is right-associative

And then, as a final trick, let’s dynamically extend the Pair class to recognize arbitrary cadr-like method calls. (Note that.^add_fallback is not in the spec and currently Rakudo-only.)

Pair.^add_fallback(
    -> $, $name { $name ~~ /^c<[ad]>+r$/ },  # should we handle this? yes, if /^c<[ad]>+r$/
    -> $, $name {                            # if it turned out to be our job, this is what we do
        -> $p {
            $name ~~ /^c(<[ad]>*)(<[ad]>)r$/;        # split out last 'a' or 'd'
            my $r = $1 eq 'a' ?? $p.key !! $p.value;    # choose key or value
            $0 ?? $r."c{$0}r"() !! $r;                            # maybe recurse
        }
    }
);

$lisp-list.caddr.say;    # 3

Whee!

Adverbly Adverby Adverbs

By Lueinc
两种创建Pair对象的方法:

my %h = debug => True;

还有一种是冒号记法

my %h = :debug(True);

今天,我会向你展示冒号记法是如何有用,Perl 6将它们用作主要的语言特性
什么是副词?
在自然语言中,副词没有动词与形容词的意思变化的明显。例如
The dog fetched the stick. # 狗叼回了棒子
仅仅是狗所做的表现。通过加上副词,例如:
The dog quickly fetched the stick. # 狗很快地叼回了棒子
声明狗能在很短的时间完成这件事。副词能让变化很激烈,就像看到的:
This batch of cookies was chewy. # 饼干很难嚼
This batch of cookies was oddly chewy. # 饼干极其难嚼
第二个句子,使用副词 “oddly”,让你知道那饼干不是面包师的目标。Perl6中的副词表现的跟上面的任务很像,告诉函数和其它语言特性做它们想做的
副词基础
副词是使用冒号+副词的语法来表达的。通常,你将它们用作开关。
开启副词的方式就像这样:

:adverb

它和这一样:

:adverb(True)

关闭副词长这样:

:!adverb

它就像这样:

:adverb(False)

如果你传递的是字符串直接量,例如

:greet('Hello')
:person("$user")

你可以用下面的代替:

:greet<Hello>
:person«$user» or :person<<$user>>

只要字符串中没有空格(尖括号形式实际上创建一列项,用空格分隔)
你也可以缩写变量如果变量的名字和键的名字相同。

:foo($foo)
:$foo

如果你提供一个十进制数,有两种写法:

:th(4)
:4th

(The :4th form only works on quoting construct adverbs, like m//and q[], in Rakudo at the moment.)
注意,副词的反义形式 (:!adv) 和 符号形式 (:$foo, :@baz) 不能给予值, 因为你已经给了它一个值了。

> my $foo = 'Fooo'; my $bar = 'Barrr';
Fooo
Barrr
> my %h = :$foo, :$bar;
bar => Barrr, foo => Fooo
> say %h<foo>;
Fooo

函数调用中的副词
函数调用中的副词用法更像具名参数,但仍计为副词。
下面是例子:

foo($z, :adverbly);
foo($z, :bar, :baz);
foo($z, :bar :baz);

每个副词都是一个具名参数,所以使用多个逗号分隔每个副词,就像分隔其它参数一样。注意你也可以像最后一个例子中一样,允许你叠加副词。
作用在操作符上的副词

副词能作用于操作符上,就像它们在函数中做的那样。它们优先级比项的赋值高,比条件的优先级低。
例子:

foo($z) :bar :baz  # 等价于 foo($z, :bar, :baz)
1 / 3 :round       # applies to /
$z & $y :adverb    # applies to &

When it comes to more complex cases, it’s helpful to remember that adverbs work similar to how an infix operator at that precedence level would (if it helps, think of the colon as a double bond in chemistry, binding both “sides” of the infix to the left-hand side). It operates on the loosest precedence operator no looser than adverbs.当情况复杂的时候, 记住副词与中缀操作符在那个优先级上的效果相似(如果有用,把冒号看作化学里面的双键(如H2C=CH2(乙烯)。碳原子与碳原子C=C以双键结合。)把中缀操作符的两侧绑定到左边)它作用于优先级最低(比副词优先级高)的操作符

1 || 2 && 3 :adv   # applies to ||
1 || (2 && 3 :adv) # applies to &&
!$foo.bar() :adv   # applies to !
!($foo.bar() :adv) # applies to .bar()
@a[0..2] :kv       # applies to []
1 + 2 - 3 :adv     # applies to -
1 ** 2 ** 3 :adv   # applies to the leftmost **

Notice that the behavior of adverbs on operators looser than adverbs is currently undefined.

1 || 2 and 3 :adv  # error ('and' too loose, applies to 3)
1 and 2 || 3 :adv  # applies to ||

作用在引号结构上的副词

各种引号那样的结构也通过副词改变行为。
(注意:这儿没有提供副词的详尽信息。 S02 和 S05 里面有更详细的介绍)
例如,让一个引号结构表现为单引号并插值闭包, 则你需要写成这样:

q:c 'Hello, $name. You have { +@msgs } messages.'
#  是的,字符 c 和 字符 ' 之间需要空格

这会输出:Hello, $name. You have 12 messages.
(这表明@msgs 数组有12个元素)
如果你想让双引号结构不插值标量,你会使用副词 :s 的反义形式 :!s

qq:!s ' ... etc ...'

正则 Regexes 允许你在 regex 外部使用副词之外, 还允许你在 regex 内部使用副词。在某些不能使用副词的情况下,内部副词允许你使用那些副词带来的功能。

$a ~~ m:i/HELLO/; # matches HELLO, hello, Hello ...
$a ~~ /:i HELLO/; # same
regex Greeting {
    :i HELLO
}                 # same

要记住的是作用在引号结构上的副词必须使用圆括号来传递值。这是因为,通常出现在副词后面的括号会被作为值传递给副词,这与你可以选择自己的引号括号的权利冲突了。

m:nth(5)// # OK
m:nth[5]// # Not OK
q:to(EOF)  # passing a value to :to, no delimiters found
q:to (EOF) # string delimited by ()

使用你自己的副词
所以你决定给你的函数添加你自己定义的副词。如果你记得的话,副词和具名参数基本上是一样的东西。所以,为了给你的函数创建副词,你仅仅只需要声明具名参数就好了:

sub root3($number, :$adverb1, :$adverb2) {
    # ... snip ...
}

给副词一个默认值就和位置参数一样,并且让某个副词必须出现,只需在副词名后面添加一个感叹号就好了:

sub root4($num, :$adv1 = 42, :$adv2, :$adv3!) {
    # default value of $adv1 is 42,
    # $adv2 is undefined (boolifies to False)
    # $adv3 must be supplied by the user
}

如果你想捕捉别人扔给你的所有副词,你可以使用 slurpy 散列:

sub root3($num, *%advs) {
    # %advs 包含所有传递给该函数的副词 :adverbs
    # that were passed to the function.
}

如果你在MAIN子例程定义了具名参数,它们会变成命令行选项!
操作符也是一样,因为操作符就是特殊语法的函数!
既然你已经学会了怎样把简陋的 Pair 应用到更多不止 Hashes 上面, 我希望你能在你的代码中快速使用它们, 并愉快地阅读剩下的 advent!

Slicing with adverbs, the only way!

By Liztormato
在散列切片和数组切片中你能使用哪些副词呢?

名称              描述
:exists 元素是否存在
:delete 移除元素,返回真,如果有元素被移除的话
:kv             将键和值作为Parcel返回
:p              return key(s) and value(s) as Parcel of Pairs
:k            只返回键
:v            只返回值

:exists
这个副词代替 .exists方法。 副词为散列和数组提供了统一的接口,可以一次检查多个元素。 .exists方法只允许一次检查单个键。
例子更有说服力。检查单个键是否存在:

$ perl6 -e 'my %h = a=>1, b=>2; say %h<a>:exists’
True

如果我们将这扩展到切片上,我们会得到一堆布尔值

$ perl6 -e 'my %h = a=>1, b=>2; say %h<a b c>:exists'
True True False

返回结果是 (Parcel)
注意,如果我们仅仅请求一个键,我们取回的是一个布尔值,不是一个只含一个布尔值的Parcel.

$ perl6 -e 'my %h = a=>1, b=>2; say (%h<a>:exists).WHAT’
(Bool)

如果很清楚地知道我们是在处理多个键,或者在编译时不清楚我们仅仅处理单个键,我们得到 一个 Parcel:

$ perl6 -e 'my %h = a=>1, b=>2; say (%h<a b c>:exists).WHAT’
(Parcel)
$ perl6 -e 'my @a="a"; my %h = a=>1, b=>2; say (%h{@a}:exists).WHAT'
(Parcel)

有时,知道某些东西不存在更方便。你可以很方便的在副词前面前置一个 叹号 ! 来反转副词 ,无论如何,它们其实真的很像具名参数

$ perl6 -e 'my %h = a=>1, b=>2; say %h<c>:!exists'
True

:delete
只有这个副词能改变散列或数组,它代替的是 .delete方法

$ perl6 -e 'my %h = a=>1, b=>2; say %h<a>:delete; say %h.perl'
("b" => 2).hash

当然,你也可以删除切片

$ perl6 -e 'my %h = a=>1, b=>2; say %h<a b c>:delete; say %h.perl'
1 2 (Any)
().hash

注意对于一个不存在的值会返回 (Any),如果你碰巧给定散列一个默认的值,它会长这样:

$ perl6 -e 'my %h is default(42) = a=>1, b=>2; say %h<a b c>:delete; say %h.perl'
1 2 42
().hash

:exists 一样,你可以反转 :delete 副词,但是没有太多意义。因为副词本质上是具名参数,你可以让:delete属性带条件参数。

$ perl6 -e 'my $really = True; my %h = a=>1, b=>2; say %h<a b c>:delete($really); say %h.perl'
1 2 (Any)
().hash

因为传递给副词的值是真的,删除才真正发生。然而,如果你传递一个假值:

$ perl6 -e ‘my $really; my %h = a=>1, b=>2; say %h<a b c>:delete($really); say %h.perl'
1 2 (Any)
("a" => 1, "b" => 2).hash

它没有删除。注意返回值没有变化。删除操作就没有执行。如果你使用子例程或方法处理一些常规的切片,这会很方便,并且,你想用一个可选参数表明切片是否也被删除:仅仅将参数传递为副词的参数!

:kv, :p, :k, :v
kv 属性返回键值对, :p属性返回一对Parcel, :k 和 :v属性只返回键和值

$ perl6
> my %h = a => 1, b => 2;
("a” => 1, "b” => 2).hash
> %h<a>:kv
a 1
> %h<a>:p  # 注意:p 返回的是 Parcel
"a" => 1
> %h<a>:k
a
> %h<a>:v
1

注意下面返回值的不同

> %h<a b c>
1 2 (Any)
> %h<a b c>:v
1 2

因为 :v 属性起着过滤的作用,过滤掉 Any. 但是,有时候你不需要这种行为。反转那个属性就可以达到目的:

> %h<a b c>:k
a b
> %h<a b c>:!k
a b c

将副词组合在一块
你也可以将几个副词结合在一块作用到 散列或切片上。最有用的组合是用 :exist 和:delete中的一个或两个,结合 :kv, :p, :k, :v中的其中之一。一些例子,例如将散列中的切片放到另外一个散列中:

$ perl6 -e 'my %h = a=>1, b=>2; my %i = (%h<a c>:delete:p).list; say %h.perl; say %i.perl'  # delete返回删除的东西
("b” => 2).hash
("a” => 1).hash

下面返回的是删除掉的键:

$ perl6 -e 'my %h = a=>1, b=>2; say %h<a b c>:delete:k’
a b

数组不是散列
在数组中,元素的键是数组的索引,所以,显示数组中定义有值的元素的索引,我们可以使用 :k属性

$ perl6 -e 'my @a; @a[3] = 1; say @a[]:k'
3

或使用数组中的所有元素创建一个 Parcel:

$ perl6 -e 'my @a; @a[3] = 1; say @a[]:!k’
0 1 2 3

然而,从数组中删除一个元素,和把 Nil 赋值给它类似,所以它会返回它默认的值(通常是 (Any))

> my @a=^10;
0 1 2 3 4 5 6 7 8 9
$ perl6 -e 'my @a = ^10; @a[3]:delete; say @a[2,3,4]; say @a[2,3,4]:exists'
2 (Any) 4
True False True

如果我们给数组指定了默认值,结果会稍有不同:

$ perl6 -e 'my @a is default(42) = ^10; @a[3]:delete; say @a[2,3,4]; say @a[2,3,4]:exists'
2 42 4
True False True

所以,即使元素不存在了,它也能返回一个定义好的值

A Grammar with duplicate checking

By Dwarring
今天的例子构建了一个 grammar 用于追踪打牌。一个或多个玩家, 每个玩家手上只有 5 张牌。每次发牌不允许有重复纸牌:

A simple Card Game Grammar
To start with, here’s the basic grammar (no duplicate checks yet):

grammar CardGame {

    rule TOP { ^ <deal> $ }

    rule deal {
        <hand>+ % ';'
    }

    rule hand  { [ <card> ]**5 }
    token card { <face><suit>  }

    proto token suit {*}
    token suit:sym<♥>  {<sym>}
    token suit:sym<♦>  {<sym>}
    token suit:sym<♣>  {<sym>}
    token suit:sym<♠>  {<sym>}

    token face {:i <[2..9]> | 10 | j | q | k | a }
}

say CardGame.parse("2♥ 5♥ 7♦ 8♣ 9♠");
say CardGame.parse("2♥ a♥ 7♦ 8♣ j♥");

最高阶层的 rule 包含一个 deal (发牌)。 deal 由一个或多个使用 ; 隔开的 hands(一手牌)组成。每手牌 hand 有 5 张纸牌。
每张纸牌由一个 face 和一个 suite 代表。face 有 A、J、Q、K 或 2-10. 后面跟着花色 suite:♥ (红心) ♦ (方块) ♣ (梅花) or ♠ (黑桃)。
[我们可以使用纸牌字符, Unicode 6.0新引入的,但是还未被广泛支持]
不出所料,第一茬 grammar 能就解析任意手牌:

say CardGame.parse("a♥ a♥ 7♦ 8♣ j♥");
# 一手, duplicate a♥
say CardGame.parse("a♥ 7♥ 7♦ 8♣ j♥; 10♥ j♥ q♥ k♥ a♥");
# 两手, duplicate j♥

检测重复

我们开始给这个 grammar 添加一个 Perl 6变量申明。这将用于追踪纸牌:

rule deal {
    :my %*PLAYED = ();
    <hand>+ % ';'
}

这申明了 %*PLAYED。 ‘%‘ twigil 表明那是一个散列, ‘‘ 表明它是动态作用域的。
动态作用域不仅仅用于子例程和方法调用。它也能无缝地和 grammar rules、tokens和 actions 用在一起。
因为是动态作用域, %*PLAYED 对于 deal rule 的调用者是可见的; hand token 和它的调用者, card token。
%*PLAYED 对于任何随后被调用的 actions 也是可见的。所以通过为 card token 创建一个 action 我们能够追踪和报告重复:

class CardGame::Actions {
    method card($/) {
       my $card = $/.lc;
       say "Hey, there's an extra $card"
           if %*PLAYED{$card}++;
   }
}

my $a = CardGame::Actions.new;
say CardGame.parse("a♥ a♥ 7♦ 8♣ j♥", :actions($a));
# "Hey there's an extra a♥"
say CardGame.parse("a♥ 7♥ 7♦ 8♣ j♥; 10♥ j♥ q♥ k♥ a♦",
                   :actions($a));
# "Hey there's an extra j♥"

这可能就是所有用于追踪和报告重复的代码了。grammar 申明和 action 是分开的, 还有一个动态作用域的散列。

不接收重复
我们要求当出现重复时,让解析失败。把重复检查的 grammar 语法移动到里面就好了:

token card {<face><suit>
    <?{
        # only allow each card to appear once
        my $card = $/.lc;
        say "Hey, there's an extra $card"
            if %*PLAYED{$card};

        ! %*PLAYED{$card}++;
     }>
}

这在<?{ 和 }> 之间引入了一个断言. 当这段代码值为 True 时, rule 就成功, 当单次发牌同一张纸牌出现多于一次时card token 失败:

say CardGame.parse("2♥ 7♥ 2♦ 3♣ 3♦");
# legitimate, parses

say CardGame.parse("a♥ a♥ 7♦ 8♣ j♥");
# fails with message: Hey, there's an extra a♥

say CardGame.parse("a♥ 7♥ 7♦ 8♣ j♥; 10♥ j♥ q♥ k♥ a♦");
# fails with message: Hey, there's an extra j♥

Unary Sort

By Moritz
在Perl5中按数值大小排序:

use v5;
my @sorted = sort { $a <=> $b } @values;

Perl6 提供类似的选择:

use v6;
my @sorted = sort { $^a <=> $^b }, @values;

主要区别在于,参数不是通过全局变量 $a$b 来传递,而是作为 comparator的参数传递。 comparator 可以是任何能掉调用的东西,即具名或匿名的子例程或代码块。{ $^a <=> $^b}语法对于sort也不特殊,我仅仅用了占位变量来展示和Perl5 的相似之处。 下面的写法一样:

my @sorted = sort -> $a, $b { $a <=> $b }, @values;
my @sorted = sort * <=> *, @values;
my @sorted = sort &infix:«<=>», @values;

The first one is just another syntax for writing blocks, * <=> * use* to automatically curry an argument, and the final one directly refers to the routine that implements the <=> “space ship” operator (which does numeric comparison).

# 按照散列中定义的顺序排序单词:
my %rank = a => 5, b => 2, c => 10, d => 3;
say sort { %rank{$^a} <=> %rank{$^b} }, 'a'..'d';  # b d a c ,升序排列
 #          ^^^^^^^^^^     ^^^^^^^^^^  code duplication
# 不区分大小写排序
say sort { $^a.lc cmp $^b.lc }, @words;
 #          ^^^^^^     ^^^^^^  代码重复

因为我们酷爱便捷憎恨重复,Perl 6 提供了更短的方案:

# sort words by a sort order defined in a hash:
say sort { %rank{$_} }, 'a'..'d';

# sort case-insensitively
say sort { .lc }, @words;

sort足够聪明地知道代码块现在只有一个参数,并使用它将输入列表中的每个元素映射为新值。这与Schwartzian Transform很相似,但是很方便,因为它是内置的。所以,现在代码块起着转换者的角色,而非比较器。
如果你想按数字顺序比较,你可以强制元素在数字上下文中进行比较,使用 + 号:

my @sorted-numerically = sort +*, @list;

如果你想按相反的顺序比较数字,就使用 -* 代替好了。

    原文作者:焉知非鱼
    原文地址: https://www.jianshu.com/p/886bda8070ae
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞