Wait the light to fall

Raku 中的 Empty

焉知非鱼

Empty

如果把一个列表中的偶数过滤掉, 首先想到的应该是使用 grep。但是, map 同样也能做到, 不过, 这要借助 Raku 中的一个特殊值: Empty。使用 map 和 Empty 甚至可以模拟出 Rust 中的 filter_map:

(1..5).map(-> \x { if x % 2 == 0 {       } else { x } }) # (1 Nil 3 Nil 5)
(1..5).map(-> \x { if x % 2 == 0 { Nil   } else { x } }) # (1 Nil 3 Nil 5)
(1..5).map(-> \x { if x % 2 == 0 { Empty } else { x } }) # (1 3 5)

从输出可以知道, Nil 在列表中占用了一个位置(slot), 而 Empty 在列表中不占用位置(slot)。对含有 NilEmpty 的列表分别使用 .elemsfor 循环就可以验证:

(1, Nil, 3).elems.say;         # 3
(1, Empty, 3).elems.say;       # 2

(for Nil { $_ }).raku.say;     # (Nil,)
(for Empty { $_ }).raku.say;   # ()
(for Empty, Nil {$_}).raku.say # (Nil,)
(for Empty, Empty {$_}).elems  # 0

为什么 Empty 如此特殊呢? Raku 官方网站对 Empty 的描述只有一句话(Empty is a Slip of the empty List)。使用 .WHAT 查看到 Empty 的类型是 Slip, 它的元素个数是 0:

say Empty.WHAT;  # (Slip)
say Empty.elems; # 0

say Nil.WHAT;    # Nil
say Nil.elems;   # 1

Nil 的类型就是 Nil, 它的元素个数是 1。我们可以对 Empty 进行智能匹配, 这跟和空列表(List)、空数组(Array)和空 Hash 进行智能匹配一样:

say "".comb ~~ ();        # True
say "".comb ~~ [];        # True
say "".comb ~~ List();    # True
say "".comb ~~ Array();   # True
say "".comb ~~ List.new;  # True
say "".comb ~~ Array.new; # True
say "".comb ~~ Empty;     # True
say "".comb ~~ Nil;       # False

那么, Empty 和空列表(List)、空数组(Array)和空 Hash 是同一个东西吗? 答案是: 不是。后者会占用位置(slot):

(1..5).map(-> \x { if x % 2 == 0 {  () } else { x } })     # (1 () 3 () 5)
(1..5).map(-> \x { if x % 2 == 0 {  [] } else { x } })     # (1 [] 3 [] 5)
(1..5).map(-> \x { if x % 2 == 0 {  {} } else { x } })     # (1 {} 3 {} 5)


(1..5).map(-> \x { if x % 2 == 0 { |() } else { x } })     # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 { |[] } else { x } })     # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 { |{} } else { x } })     # (1 3 5)

根据官方网站的描述, Empty is a Slip of the empty List。虽然 ()[]{} 都是空的, 并且 ().elems[].elems{}.elems 的元素个数都是 0, 但是它们不是 Slip。而 |()|[]|{} 才是 Slip

那么使用构造函数创建的空数据结构可以代替 Empty 吗?

(1..5).map(-> \x { if x % 2 == 0 {   List() } else { x } }) # (1 (List(Any)) 3 (List(Any)) 5)
(1..5).map(-> \x { if x % 2 == 0 {  |List() } else { x } }) # (1 (List(Any)) 3 (List(Any)) 5)
(1..5).map(-> \x { if x % 2 == 0 {  Array() } else { x } }) # (1 (Array(Any)) 3 (Array(Any)) 5)
(1..5).map(-> \x { if x % 2 == 0 { |Array() } else { x } }) # (1 (Array(Any)) 3 (Array(Any)) 5)


(1..5).map(-> \x { if x % 2 == 0 { List.new  } else { x } }) # (1 () 3 () 5)
(1..5).map(-> \x { if x % 2 == 0 { Array.new } else { x } }) # (1 [] 3 [] 5)
(1..5).map(-> \x { if x % 2 == 0 { Hash.new  } else { x } }) # (1 {} 3 {} 5)


(1..5).map(-> \x { if x % 2 == 0 { |List.new  } else { x } }) # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 { |Array.new } else { x } }) # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 { |Hash.new  } else { x } }) # (1 3 5)

List()List.new 不同, Array()Array.new 不同。通过 new 创建出来的数据结构, 其元素的个数是 0:

List().WHAT;        # (List(Any))
List().elems;       # 1

Array().WHAT;       # (Array(Any))
Array().elems;      # 1

List.new.WHAT;      # (List)
List.new.elems;     # 0

Array.new.WHAT;     # (Array)
Array.new.elems;    # 0

Hash.new.WHAT;      # (Hash)
Hash.new.elems;     # 0

(|List.new).WHAT;   # (Slip)
(|List.new).elems;  # 0

(|Array.new).WHAT;  # (Slip)
(|Array.new).elems; # 0

(|Hash.new).WHAT;   # (Slip)
(|Hash.new).elems;  # 0

所以以下这几种写法是等价的:

(1..5).map(-> \x { if x % 2 == 0 { Empty } else { x } }) # (1 3 5)

(1..5).map(-> \x { if x % 2 == 0 {   |() } else { x } }) # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 {   |[] } else { x } }) # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 {   |{} } else { x } }) # (1 3 5)

(1..5).map(-> \x { if x % 2 == 0 { |List.new  } else { x } }) # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 { |Array.new } else { x } }) # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 { |Hash.new  } else { x } }) # (1 3 5)

你甚至还可以使用多个 Slip 运算符:

(1..5).map(-> \x { if x % 2 == 0 { ||Empty } else { x } })   # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 { |||Empty } else { x } })  # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 { ||||Empty } else { x } }) # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 { ||||||||||||||||||||||||||Empty } else { x } }) # (1 3 5)

我们竟然还能在 map 中使用 Raku 中的一些特殊符号:

(1..5).map(-> \x { if x % 2 == 0 { @ } else { x } }) # (1 [] 3 [] 5)
(1..5).map(-> \x { if x % 2 == 0 { % } else { x } }) # (1 {} 3 {} 5)
(1..5).map(-> \x { if x % 2 == 0 { $ } else { x } }) # (1 (Any) 3 (Any) 5)
(1..5).map(-> \x { if x % 2 == 0 { & } else { x } }) # (1 (Callable) 3 (Callable) 5)

因为 @%$& 的前面没有 Slip 运算符, 所以 map 后, 列表中的 slot 没有变化, 依然是一对一的关系。但是, 在 map 中使用 |@|% 会发生什么呢?

(1..5).map(-> \x { if x % 2 == 0 { |@ } else { x } }) # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 { |% } else { x } }) # (1 3 5)

(1..5).map(-> \x { if x % 2 == 0 { |$ } else { x } }) # (1 (Any) 3 (Any) 5)
(1..5).map(-> \x { if x % 2 == 0 { |& } else { x } }) # (1 (Callable) 3 (Callable) 5)

使用 dd 分别查看 @%$& 的结构:

dd @ # Array @ = []
dd % # Hash % = {}
dd $ # Any $ = Any
dd & # Callable & = Callable

因为 @ 是一个匿名数组, % 是一个匿名 Hash。所在 |@|% 在 map 中的作用和 Empty 一样。而 $& 不行。

所以我们可以分别使用 @%$& 符号初始化一个空数组、空 Hash、空标量和空的 Callable:

my @a = @; dd @a; # Array @a = []
my %a = %; dd %a; # Hash %a = {}
my $a = $; dd $a; # Any $a = Any
my &a = &; dd &a; # Callable &a = Callable

甚至这些符号后面可以再跟着括号:

(1..5).map(-> \x { if x % 2 == 0 { |@() } else { x } }) # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 { |%() } else { x } }) # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 { |$() } else { x } }) # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 { |&() } else { x } }) # (1 3 5)

(1..5).map(-> \x { if x % 2 == 0 { |@[] } else { x } }) # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 { |%[] } else { x } }) # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 { |$[] } else { x } }) # (1 3 5)
(1..5).map(-> \x { if x % 2 == 0 { |&[] } else { x } }) # (1 3 5)

(1..5).map(-> \x { if x % 2 == 0 { |&{} } else { x } }) # (1 3 5)

使用 dd 打印它们的值:

dd @(); # ()
dd %(); # Hash % = {}
dd $(); # $( )
dd &(); # $( )
dd %{}; # Hash % = {}

dd @[]; # Array element = []
dd %[]; # Hash % = {}
dd $[]; # $[]
dd &[]; # $[]
dd &{}; # ${}

使用 WHAT 查看元素的类型:

%().WHAT # Hash
@().WHAT # List
$().WHAT # List
&().WHAT # List
%{}.WHAT # Hash

@[].WHAT; # Array
%[].WHAT; # Hash
$[].WHAT; # Array
&[].WHAT; # Array
&{}.WHAT; # Hash

甚至还有一个 |$_:

(1..5).map(-> \x { if x % 2 == 0 {  $_  } else { x } }) # (1 () 3 () 5)
(1..5).map(-> \x { if x % 2 == 0 { |$_  } else { x } }) # (1 3 5)

因为 $_ 也匹配 Empty:

$_.WHAT;     # (Seq)
dd $_;       # ().Seq
$.elems;     # 0
$_ ~~ Empty; # True

Empty 的应用场景 #

  • 使用 Empty 过滤元素
my @a = <AB BC MB NB NL NS ON PE QC SK>; @a.elems; # 10
@a[2]:delete;
@a.elems; # 10
dd @prov_cd
# Array @prov_cd = ["AB", "BC", Any, "NB", "NL", "NS", "ON", "PE", "QC", "SK"]

虽然删除了一个元素, 但是这个被删除的元素还占用一个位置(slot), 使用 Empty 模仿 filter_map 可以把空的 slot 过滤掉:

@prov_cd.map(-> \x { if x.defined { x } else { Empty } })
  • 使用 if False 返回 Empty

使用 if False 返回 Empty 的特性, 可以把 map 当作 grep 来使用:

(1..5).map: { $_ if $_ % 2 != 0 } # (1 3 5)

类似的还有 ... with NilNil andthen ... 也返回 Empty:

(1 with Nil)    ~~ Empty # True
(Nil andthen 1) ~~ Empty # True
(1 with Nil).WHAT  # Slip
(1 with Nil).^name # Slip

(Nil andthen 1).WHAT  # Slip
(Nil andthen 1).^name # Slip
  • 使用 Empty 清空 Hash

因为 Empty 本质上是一个空列表, 所以可以使用 Empty 清空 Hash:

my %h = 'a'..'b' Z=> 1..*;
dd %h;      # Hash %h = {:a(1), :b(2)}
%h = Empty; # 清空 Hash, 等价于 %h = ();
dd %h;      # Hash %h = {}

而把 Nil 赋值给 Hash 会报错:

%h = Nil;

Hash 初始化构造器说它找到奇数个元素:

Odd number of elements found where hash initializer expected:
Only saw 1 element
  • 匹配签名中的 Empty
multi sub foo ($_){.say}
multi sub foo (Empty){ say 'Hello, World' }

foo Empty;        # Hello, World
foo (1 if 0);     # Hello, World
foo (|());        # Hello, World
foo (|[]);        # Hello, World
foo (|{});        # Hello, World
foo (|%);         # Hello, World
foo (|@);         # Hello, World
foo (Slip.new)    # Hello, World
foo (if False {}) # Hello, World

因为:

|() ~~ Empty # True
|[] ~~ Empty # True
|{} ~~ Empty # True
|%  ~~ Empty # True
|@  ~~ Empty # True

Slip.new ~~ Empty      # True
(if False {}) ~~ Empty # True

if False {} 语句返回的是 Slip 类型。

dd (if False {}).^name # "Slip"
  • 在 Grammar 的 Action 中使用 Empty
my $excerpt = q:to/END/;
Here's some unimportant text.
=begin code
I want this line.
and this line as well.
=end code
More unimport text.
=begin code
Let's to go home.
=end code
END

grammar ExtractSection {
    rule TOP      { ^ <section>+ %% <.comment> $      }
    token section { <line>+ % <.ws>                   }
    token line    { <?!before <comment>> \N+ \n       }  
    token comment { ['=begin code' | '=end code' ] \n }
}

class ExtractSectionAction {
    method TOP($/)      { make $/.values».ast }
    method section($/)  { make ~$/.trim       }
    method line($/)     { make ~$/.trim       }
    method comment($/)  { make Empty          }
}

my $em = ExtractSection.parse($excerpt, :actions(ExtractSectionAction)).ast;

for @$em -> $line {
    say $line;
    say '-' x 35;
}

参考链接 #