美文网首页Raku Programming Language
第二天 - Perl 6: 符号, 变量和容器

第二天 - Perl 6: 符号, 变量和容器

作者: 焉知非鱼 | 来源:发表于2017-12-03 23:01 被阅读17次

    第二天-Perl 6: 符号, 变量和容器

    对容器的基本理解对于在 Perl 6 中进行愉快的编程是至关重要的。它们无处不在,不仅影响你获得的变量类型,还决定了 ListMap 在迭代时的行为方式。

    今天,我们将学习什么是容器,以及如何使用它们,但是首先,我希望你暂时忘记你对 Perl 6 的符号和变量的所有知识或怀疑,特别是如果你来自 Perl 5 的背景。 忘记一切。

    把钱拿出来

    在 Perl 6 中,变量以 $ 符号为前缀,用绑定运算符(:=)赋值。 像这样:

    my $foo := 42;
    say "The value is $foo"; # OUTPUT: «The value is 42
    »
    

    如果你已经按照我的建议来忘记你所知道的一切,那么学习 ListHash 类型也是一样:

    my $ordered-things := <foo bar ber>;
    my $named-things   := %(:42foo, :bar<ber>);
    
    say "$named-things<foo> bottles of $ordered-things[2] on the wall";
    # OUTPUT: «42 bottles of ber on the wall
    »
    
    .say for $ordered-things;  # OUTPUT: «foo
    bar
    ber
    »
    .say for $named-things;    # OUTPUT: «bar => ber
    foo => 42
    »
    

    了解这一点,你可以写出各种各样的程序,所以如果你开始觉得有太多的东西需要学习,记住你不需要一次学习所有东西。

    我们祝你有一个愉快的列表圣诞

    让我们试着用我们的变量做更多的事情。 想要更改列表中的值并不罕见。 到目前为止我们的表现如何呢?

    my $list := (1, 2, 3);
    $list[0] := 100;
    # OUTPUT: «Cannot use bind operator with this left-hand side […] »
    

    尽管我们可以绑定到变量,但是如果我们试图绑定到某个值,我们会得到一个错误,无论这个值是来自 List 还是只是一个字面值:

    1 := 100;
    # OUTPUT: «Cannot use bind operator with this left-hand side […] »
    

    这就是为什么列表是不可变的。 然而,这是一个实现愿望的季节,所以我们希望有一个可变的List

    我们需要掌握的是一个 Scalar 对象,因为绑定操作符可以使用它。 顾名思义,一个 Scalar 存储一个东西。 你不能通过 .new 方法实例化一个 Scalar,但是我们可以通过声明一些词法变量来得到它们。 不需要费心给他们的名字:

    my $list := (my $, my $, my $);
    $list[0] := 100;
    say $list; # OUTPUT: «(100 (Any) (Any))
    »
    

    输出中的 (Any) 是容器的默认值(稍后一点)。 上面,似乎我们设法在 List 创建后将一个值绑定到列表的元素上,我们不是吗? 确实我们做了,但是...

    my $list := (my $, my $, my $);
    $list[0] := 100;
    $list[0] := 200;
    # OUTPUT: «Cannot use bind operator with this left-hand side […] »
    

    绑定操作用一个新的值(100)代替 Scalar 容器,所以如果我们试图再次绑定,我们又回到了原来的方括号那个,试图绑定到一个值,而不是一个容器。

    我们需要一个更好的工具。

    That's Your Assignment

    绑定运算符有一个表亲:赋值运算符(=)。 我们不用一个绑定操作符替换我们的 Scalar 容器,而是使用赋值操作符来赋值或者“存储”我们在容器中的值:

    my $list := (my $ = 1, my $ = 2, my $ = 3);
    $list[0] = 100;
    $list[0] = 200;
    say $list;
    # OUTPUT: «(200 2 3)
    »
    

    现在,我们可以从一开始就指定我们的原始值,并且可以随时用其他值替换它们。 我们甚至可以变得时髦,并在每个容器上放置不同的类型约束:

    my $list := (my Int $ = 1, my Str $ = '2', my Rat $ = 3.0);
    $list[0] = 100; # OK!
    $list[1] = 42;  # Typecheck failure!
    
    # OUTPUT: «Type check failed in assignment;
    #    expected Str but got Int (42) […] »
    

    这有些放纵,但有一件事可以使用类型约束:$list 变量。 我们将其限制为 Positional 角色,以确保它只能保持 Positional 类型,就像 ListArray

    my Positional $list := (my $ = 1, my $ = '2', my $ = 3.0);
    

    不知你咋想的,但是这对我来说看起来非常冗长。 幸运的是,Perl 6 有语法来简化它!

    Position@lly

    首先,让我们摆脱变量的显式类型约束。 在 Perl 6 中,您可以使用 @ 而不是 $ 作为符号来表示您希望变量受到角色 Positional 的类型约束:

    my @list := 42;
    # OUTPUT: «Type check failed in binding;
    #   expected Positional but got Int (42) […] »
    

    其次,我们将使用方括号来代替圆括号来存储我们的 List。 这告诉编译器创建一个 Array 而不是一个 ListArrays 是可变的,它们将每个元素自动粘贴到 Scalar 容器中,就像我们在前一节中手动操作一样:

    my @list := [1, '2', 3.0];
    @list[0] = 100;
    @list[0] = 200;
    say @list;
    # OUTPUT: «[200 2 3]
    »
    

    我们的代码变得更短了,但我们可以折腾更多的字符。 就像赋值给$-sigiled 变量而不是绑定一样,你可以赋值给@ -sigiled 变量来获得一个自由的 Array。 如果我们切换到赋值,我们可以完全摆脱方括号:

    my @list = 1, '2', 3.0;
    

    好,简洁。

    类似的想法背后是 - 和 & 符号化的变量。 sigil 意味着 Associative 角色的类型约束,并为赋值提供相同的快捷方式(给你一个 Hash),并为这些值创建 Scalar 容器。 对于角色 Callable 和赋值的 -sigiled变量类型 - 行为类似于 $ sigils,给出一个可以修改其值的自由 Scalar 容器:

    my  %hash = :42foo, :bar<ber>;
    say %hash;  # OUTPUT: «{bar => ber, foo => 42}
    »
    
    my &reversay = sub { $^text.flip.say }
    reversay '6 lreP ♥ I'; # OUTPUT: «I ♥ Perl 6
    »
    
    # store a different Callable in the same variable
    &reversay = *.uc.say;  # a WhateverCode object
    reversay 'I ♥ Perl 6'; # OUTPUT: «I ♥ PERL 6
    »
    

    The One and Only

    之前我们知道赋值给 $ -sigiled 变量会给你一个免费的 Scalar 容器。 由于标量,顾名思义,只包含一个东西......如果你把一个 List 放到 Scalar 中会发生什么? 毕竟,当你试图这样做的时候,宇宙仍然没有被扼杀:

    my  $listish = (1, 2, 3);
    say $listish; # OUTPUT: «(1 2 3)
    »
    

    这样的行为可能使 Scalar 看起来似乎是一个用词不当,但它确实把整个列表视为一个东西。 我们可以通过几种方式观察其差异。 我们来比较绑定到 $ -sigiled 变量的 List(所以不包含 Scalar)和赋值给 $ -sigiled 变量(自动 Scalar 容器)的 List

    # Binding:
    my  $list := (1, 2, 3);
    say $list.perl;
    say "Item: $_" for $list;
    
    # OUTPUT:
    # (1, 2, 3)
    # Item: 1
    # Item: 2
    # Item: 3
    
    
    # Assignment:
    my $listish = (1, 2, 3);
    say $listish.perl;
    say "Item: $_" for $listish;
    
    # OUTPUT:
    # $(1, 2, 3)
    # Item: 1 2 3
    

    .perl 方法给了我们一个额外的见解,并在第二个 List 之前显示了一个 $,以表明它在 Scalar 中是集装箱化的。 更重要的是,当我们用 for 循环迭代我们的 Lists 时,第二个 List 结果只有一个迭代:整个 List 作为一个项目! Scalar 没有辜负它的名字。

    这种行为不仅仅是学术上的兴趣。 回想一下,Arrays(和Hashes)为它们的值创建Scalar 容器。 这意味着如果我们嵌套东西,即使我们选择一个单独的列表或散列在里面存储着 Array(或 Hash),并试图迭代它,它将只被视为一个单一的项目:

    my @stuff = (1, 2, 3), %(:42foo, :70bar);
    say "List Item: $_" for @stuff[0];
    say "Hash Item: $_" for @stuff[1];
    
    # OUTPUT:
    # List Item: 1 2 3
    # Hash Item: bar  70
    # foo 42
    

    同样的推理(即 Scalar 容器中的列表和散列是单个项目)适用于当您试图压扁 Array 的元素或将它们作为参数传递给 slurpy 参数时:

    my @stuff = (1, 2, 3), %(:42foo, :70bar);
    say flat @stuff;
    # OUTPUT: «((1 2 3) {bar => 70, foo => 42})
    »
    
    -> *@args { @args.say }(@stuff)
    # OUTPUT: «[(1 2 3) {bar => 70, foo => 42}]
    »
    

    正是这种行为可以将 Perl 6 初学者推上墙,特别是那些来自 Perl 5 自动展平语言的人。然而,现在我们知道为什么会出现这种行为,我们可以改变它!

    Decont

    如果 Scalar 容器是罪魁祸首,我们所要做的就是删除它。 我们需要将我们的列表和哈希值去容器化,或者简称为 “decont”。 在你的 Perl 6 之旅中,你可以找到几种方法来完成这个工作,但是为此设计的一个方法就是 decont methodop(<>):

    my @stuff = (1, 2, 3), %(:42foo, :70bar);
    say "Item: $_" for @stuff[0]<>;
    say "Item: $_" for @stuff[1]<>;
    
    # OUTPUT:
    # Item: 1
    # Item: 2
    # Item: 3
    # Item: bar   70
    # Item: foo   42
    

    它很容易记住:它看起来像一个被挤压的盒子(一个被踩踏的容器)。 在通过索引到 Array 中检索我们的容器化项目之后,我们附加了 decont 并从 Scalar 容器中移除了内容,导致我们的循环遍历它们中的每个项目。

    如果您希望一次去除 Array 中的每个元素,只需使用超运算符(»,或 >>,如果您更喜欢使用 ASCII)就可以使用 decont:

    my @stuff = (1, 2, 3), %(:42foo, :70bar);
    say flat @stuff»<>;
    # OUTPUT: «(1 2 3 bar => 70 foo => 42)
    »
    
    -> *@args { @args.say }(@stuff»<>)
    # OUTPUT: «[1 2 3 bar => 70 foo => 42]
    »
    

    随着容器被删除,我们的列表和散列就像我们想要的那样变平。 当然,我们可以避免使用 Array,而将原始 List 绑定到变量上。 由于 List 没有把它们的元素放入容器,所以没有任何东西可以去除:

    my @stuff := (1, 2, 3), %(:42foo, :70bar);
    say flat @stuff;
    # OUTPUT: «(1 2 3 bar => 70 foo => 42)
    »
    
    -> *@args { @args.say }(@stuff)
    # OUTPUT: «[1 2 3 bar => 70 foo => 42]
    »
    

    不要让它溜走

    当我们在这里的时候,值得注意的是,当他们想要执行decont(我们不是在传递参数给 Callable 的时候使用它)时,许多人使用 slip运算符(|):

    my @stuff = (1, 2, 3), (4, 5);
    say "Item: $_" for |@stuff[0];
    
    # OUTPUT:
    # Item: 1
    # Item: 2
    # Item: 3
    

    虽然它可以完成工作,但可能会引入微妙的 bugs,这些 bug 可能很难追查到。 尝试在这里找到一个,在一个程序中迭代了一个无限的非负整数列表,并打印那些素数:

    my $primes = ^∞ .grep: *.is-prime;
    say "$_ is a prime number" for |$primes;
    

    放弃? 这个程序会导致内存泄漏... 非常缓慢。 尽管我们遍历了无限的项目列表,但这不是问题,因为 .grep 方法返回的 Seq 对象不会保留已经迭代的项目,因此内存使用永远不会增长。

    有问题的部分是我们的 | slip 操作符。 它将我们的 Seq 转换成一个 Slip ,这是一个 List 类型,并且保存我们已经消耗的所有的值。 如果您希望在 htop 中看到增长,那么这个程序的修改版本会更快地增长:

    # CAREFUL! Don't consume all of your resources!
    my $primes = ^∞ .map: *.self;
    Nil for |$primes;
    

    让我们再试一次,但是这次使用 decont 方法 op:

    my $primes = ^∞ .map: *.self;
    Nil for $primes<>;
    

    内存使用现在是稳定的,程序可以坐在那里迭代直到时间结束。 当然,因为我们知道这是 Scalar 容器导致的容器化,我们希望在这里避免它,所以我们可以简单地将 Seq 绑定到变量上:

    my $primes := ^∞ .map: *.self;
    Nil for $primes;
    

    I Want Less

    如果你讨厌符号,Perl 6 会得到一些你可以微笑的东西:无符号的变量。 只要在声明中加一个反斜杠的前缀,表示你不想要讨厌的符号:

    my \Δ = 42;
    say Δ²; # OUTPUT: «1764
    »
    

    你不会得到任何这样的变量的自由 Scalar,因此,在声明期间,绑定或赋值给他们没有任何区别。 它们的行为类似于将值绑定到 $ -sigiled 变量的行为,包括绑定 Scalars 并使变量可变:

    my \Δ = my $ = 42;
    Δ = 11;
    say Δ²; # OUTPUT: «121
    »
    

    一个更常见的地方,你可能会看到这样的变量是作为例程的参数,在这里,这意味着你想把 is raw trait 应用到参数上。 这在 + positional slurpy 参数的含义也是存在的(不需要反斜杠),如果它是 is raw 的,意味着你将不会得到不需要的 Scalar 容器,因为它是一个 Array,因为它具有 @ sigil:

    sub sigiled ($x is raw, +@y) {
        $x = 100;
        say flat @y
    }
    
    sub sigil-less (\x, +y) {
        x = 200;
        say flat y
    }
    
    my $x = 42;
    sigiled    $x, (1, 2), (3, 4); # OUTPUT: «((1 2) (3 4))
    »
    say $x;                        # OUTPUT: «100
    »
    
    sigil-less $x, (1, 2), (3, 4); # OUTPUT: «(1 2 3 4)
    »
    say $x;                        # OUTPUT: «200
    »
    

    Defaulting on Default Defaults

    容器提供的一个很棒的功能是默认值。 你可能听说过在 Perl 6 中,Nil表示缺少一个值,而不是一个值。 容器默认值就是它的作用:

    my $x is default(42);
    say $x;   # OUTPUT: «42
    »
    
    $x = 10;
    say $x;   # OUTPUT: «10
    »
    
    $x = Nil;
    say $x;   # OUTPUT: «42
    »
    

    一个容器的默认值是使用 is default trait 给它的。 它的参数是在编译时计算的,每当容器缺少一个值时,就使用结果值。 由于 Nil 的工作是表明这一点,因此将 Nil 分配到容器中将导致容器包含其默认值,而不是 Nil

    可以给 ArrayHash 容器赋予默认值,如果你希望你的容器在字面上包含 Nil,当没有值时,只需要指定 Nil 作为默认值:

    my @a is default<meow> = 1, 2, 3;
    say @a[0, 2, 42]; # OUTPUT: «(1 3 meow)
    »
    
    @a[0]:delete;
    say @a[0];        # OUTPUT: «meow
    »
    
    my %h is default(Nil) = :bar<ber>;
    say %h<bar foos>; # OUTPUT: «(ber Nil)
    »
    
    %h<bar>:delete;
    say %h<bar>       # OUTPUT: «Nil
    »
    

    容器的默认值有一个默认的默认值:容器上的显式类型约束:

    say my Int $y; # OUTPUT: «(Int)
    »
    say my Mu  $z; # OUTPUT: «(Mu)
    »
    
    say my Int $i where *.is-prime; # OUTPUT: «(<anon>)
    »
    $i.new; # OUTPUT: (exception) «You cannot create […]»
    

    如果没有明确的类型约束,默认的默认值是一个 Any 类型的对象:

    say my $x;    # OUTPUT: «(Any)
    »
    say $x = Nil; # OUTPUT: «(Any)
    »
    

    请注意,您可能在可选参数的例程签名中使用的默认值不是容器默认值,将 Nil 分配给子例程参数或分配给参数不会使用签名中的默认值。

    自定义

    如果容器的标准行为不适合您的需求,您可以使用 Proxy 类型创建自己的容器:

    my $collector := do {
        my @stuff;
        Proxy.new: :STORE{ @stuff.push: @_[1] },
                   :FETCH{ @stuff.join: "|"   }
    }
    
    $collector = 42;
    $collector = 'meows';
    say $collector; # OUTPUT: «42|meows
    »
    
    $collector = 'foos';
    say $collector; # OUTPUT: «42|meows|foos
    »
    

    接口有点笨重,但它完成了工作。我们使用 .new 方法创建 Proxy 对象,该方法需要两个必需的命名参数:STOREFETCH,每个都带一个 Callable

    每当从容器中读取一个值时,FETCHCallable 被调用,这可能比直接看到的次数多出现一次:在上面的代码中,当容器通过调度和例程这两个调用渗透时,FETCHCallable 被调用10次。 Callable 被调用一个单一的位置参数:Proxy 对象本身。

    无论何时将值存储到我们的容器中(例如,使用赋值运算符(=)),STORE Callable 都会被调用。 Callable 的第一个位置参数是 Proxy 对象本身,第二个参数是存储的值。

    我们希望 STOREFETCH Callable 共享 @stuff 变量,所以我们使用 do statement prefix 和一个代码块来很好地包含它。

    我们将我们的 Proxy 绑定到一个变量,其余的只是正常的变量用法。输出显示我们的自定义容器提供的改变过的行为。

    Proxies 也可以方便地作为返回值来提供具有可变属性的额外行为。例如,这里有一个属性,从外部看来只是一个正常的可变属性,但实际上强制它的值从 Any 任何类型变为 Int 类型:

    class Foo {
        has $!foo;
        method foo {
            Proxy.new: :STORE(-> $, Int() $!foo { $!foo }),
                       :FETCH{ $!foo }
        }
    }
    
    my $o = Foo.new;
    $o.foo = ' 42.1e0 ';
    say $o.foo; # OUTPUT: «42
    »
    

    很甜蜜! 如果你想要一个更好的接口的 Proxy 与一些更多的功能,请检查 Proxee 模块。

    这就是全部,伙计

    那关于这一切。 在 Perl 6 中你将会看到的剩下的动物是 “twigils”:名称前带有两个符号的变量,但是就容器而言,它们的行为与我们所介绍的变量相同。 第二个符号只是表示附加信息,如变量是隐含的位置参数还是命名参数...

    sub test { say "$^implied @:parameters[]" }
    test 'meow', :parameters<says the cat>;
    # OUTPUT: «meow says the cat
    »
    

    ...或者该变量是私有属性还是公共属性:

    with class Foo {
        has $!foo = 42;
        has @.bar = 100;
        method what's-foo { $!foo }
    }.new {
        say .bar;       # OUTPUT: «[100]
    »
        say .what's-foo # OUTPUT: «42
    »
    }
    

    然而,这是另一天的旅程。

    结论

    Perl 6 有一个丰富的变量和容器系统,与 Perl 5 有很大的不同。理解它的工作方式是非常重要的,因为它会影响列表和哈希行为的迭代和展开方式。

    赋值给变量提供了有价值的快捷方式,例如提供ScalarArrayHash 容器,具体取决于符号。 如果您需要,绑定到变量允许您绕过这样的快捷方式。

    在 Perl 6 中存在无符号变量,它们与具有绑定功能的 $ -sigiled 变量具有相似的行为。 当用作参数时,这些变量的行为就像应用了 is raw trait一样。

    最后,容器可以有默认值,可以创建自己的自定义容器,可以绑定到变量或从例程返回。

    节日快乐!

    相关文章

      网友评论

      本文标题:第二天 - Perl 6: 符号, 变量和容器

      本文链接:https://www.haomeiwen.com/subject/piazbxtx.html