标量结构§
有些类没有内部结构,要访问它们的各个部分,必须使用特定的方法。数字、字符串和其他一些单片类都包含在该类中。它们使用 $
符号,尽管复杂数据结构也可以使用它。
my = 7;my = "8";
有一个 Scalar
类,它用于在内部为使用 $
符号声明的变量分配默认值。
my = 333;say .VAR.^name; # OUTPUT: «Scalar»
任何复杂数据结构都可以通过使用 项目上下文化器 $
来标量化
(1, 2, 3, $(4, 5))[3].VAR.^name.say; # OUTPUT: «Scalar»
但是,这意味着它们将在其所在的上下文中被视为标量。您仍然可以访问其内部结构。
(1, 2, 3, $(4, 5))[3][0].say; # OUTPUT: «4»
一个有趣的副作用,或者可能是预期的功能,是标量化保留了复杂结构的标识。
for ^2# OUTPUT: «Array|93947995146096Array|93947995700032»
每次分配 (1, 1)
时,创建的变量在 ===
的意义上将是不同的;正如所示,内部指针表示的不同值被打印出来。然而
for ^2# OUTPUT: «List|94674814008432List|94674814008432»
在本例中,$list
使用了标量符号,因此将是一个 Scalar
。任何具有相同值的标量将完全相同,如打印指针时所示。
复杂数据结构§
复杂数据结构分为两大类:Positional
,或列表式,以及 Associative
,或键值对式,具体取决于您如何访问其一级元素。一般来说,复杂数据结构,包括对象,将是两者的结合,对象属性被归类为键值对。虽然所有对象都是 Mu
的子类,但一般来说,复杂对象是 Any
的子类的实例。虽然理论上可以混合使用 Positional
或 Associative
,但大多数适用于复杂数据结构的方法是在 Any
中实现的。
导航这些复杂数据结构是一个挑战,但 Raku 提供了几个可以在其上使用的函数:deepmap
和 duckmap
。虽然前者将按顺序访问每个元素,并执行传递的块所需的任何操作,
say [[1, 2, [3, 4]],[[5, 6, [7, 8]]]].deepmap( *.elems );# OUTPUT: «[[1 1 [1 1]] [1 1 [1 1]]]»
它返回 1
,因为它进入更深层级并对它们应用 elems
,duckmap
可以执行更复杂的操作
say [[1, 2, [3, 4]], [[5, 6, [7, 8]]]].duckmap:-> where .elems == 2 ;# OUTPUT: «[[1 2 2] [5 6 2]]»
在本例中,它深入结构,但如果元素不满足块中的条件(1, 2
),则返回元素本身,如果满足条件(每个子数组末尾的两个 2
),则返回数组的元素数量。
由于 deepmap
和 duckmap
是 Any
方法,因此它们也适用于关联数组
say %( first => [1, 2], second => [3,4] ).deepmap( *.elems );# OUTPUT: «{first => [1 1], second => [1 1]}»
只是在这种情况下,它们将应用于每个作为值的列表或数组,而保留键不变。
Positional
和 Associative
可以相互转换。
say %( first => [1, 2], second => [3,4] ).list[0];# OUTPUT: «second => [3 4]»
但是,在这种情况下,对于 Rakudo >= 2018.05,它每次运行都会返回不同的值。哈希将被转换为键值对的列表,但保证是无序的。您也可以执行相反的操作,只要列表的元素数量为偶数(奇数将导致错误)
say <a b c d>.Hash # OUTPUT: «{a => b, c => d}»
但是
say <a b c d>.Hash.kv # OUTPUT: «(c d a b)»
每次运行都会获得不同的值;kv
将每个 Pair
转换为列表。
复杂数据结构通常也是 Iterable
。从它们中生成一个 迭代器 将允许程序逐个访问结构的第一级
.say for 'א'..'ס'; # OUTPUT: «אבגדהוזחטיךכלםמןנס»
'א'..'ס'
是一个 Range
,一个复杂的数据结构,在前面加上 for
将迭代直到列表耗尽。您可以通过覆盖 迭代器 方法(来自角色 Iterable
)在您的复杂数据结构上使用 for
is Array;my := SortedArray.new([3,2,1,4]);.say for ; # OUTPUT: «1234»
for
直接在 @thing
上调用 iterator
方法,使其按顺序返回数组的元素。有关 迭代的更多信息,请参阅专门介绍它的页面。
函数式结构§
Raku 是一种函数式语言,因此函数是一流的数据结构。函数遵循 Callable
角色,它是四个基本角色中的第四个。Callable
与 &
符号一起使用,尽管在大多数情况下为了简单起见省略了它;在 Callable
的情况下,始终允许省略此符号。
my = ;say a-func(3), a-func(7); # OUTPUT: «(0 1 2)(0 1 2 3 4 5 6)»
Block
是最简单的可调用结构,因为 Callable
不能实例化。在本例中,我们实现了一个记录事件并可以检索它们的块
my = -> , = Nil( "Stuff" );( "More stuff" );say ( Nil, "2018-05-28" ); # OUTPUT: «(Stuff More stuff)»
一个 Block
具有一个 Signature
,在本例中是两个参数,第一个是将要记录的事件,第二个是检索事件的键。它们将以独立的方式使用,但其目的是展示 状态变量 的使用,该变量从每次调用到下一次调用都保留下来。此状态变量封装在块中,无法从外部访问,除非使用块提供的简单 API:使用第二个参数调用块。可以克隆 Block
my = .clone;( "Clone stuff" );( "More clone stuff" );say ( Nil, "2018-05-28" );# OUTPUT: «(Clone stuff More clone stuff)»
克隆将重置状态变量;而不是克隆,我们可以创建外观来更改 API。例如,消除使用 Nil
作为第一个参数来检索特定日期的日志的需要
my = .assuming( Nil, * );( %(changing => "Logs") );say ( "2018-05-28" );# OUTPUT: «({changing => Logs} Stuff More stuff)»
assuming
围绕一个块调用进行包装,为我们需要的参数提供一个值(在本例中为 Nil
),并将参数传递给我们使用 *
表示的其他参数。实际上,这对应于自然语言语句“我们正在调用 $logger
,假设第一个参数是 Nil
”。我们可以稍微改变这两个块的外观,以明确它们实际上是在对同一个块进行操作。
my = .clone;my ::logs = .assuming( *, Nil );my ::get = .assuming( Nil, * );::logs( <an array> );::logs( %(key => 42) );say ::get( "2018-05-28" );
虽然 ::
通常用于调用类方法,但它实际上是变量名称的有效部分。在本例中,我们按照惯例使用它们来简单地指示 $Logger::logs
和 $Logger::get
实际上是在调用 $Logger
,我们将其大写以使用类似类的外观。本教程的重点是,将函数用作一等公民,以及使用状态变量,允许使用某些有趣的模式,例如这种模式。
作为一等数据结构,可调用对象可以在任何其他类型的数据可以使用的任何地方使用。
my = ( //, //, // );say .map: "33af" ~~ *;# OUTPUT: «(「3」 alnum => 「3」 「a」 alpha => 「a」 Nil)»
正则表达式实际上是一种可调用对象。
say /regex/.does( Callable ); # OUTPUT: «True»
在上面的示例中,我们正在调用存储在数组中的正则表达式,并将它们应用于字符串文字。
可调用对象是通过使用 函数组合运算符 ∘ 组合的。
my = -> ;my ::withtype = ::logs ∘ ;::withtype( Pair.new( 'left', 'right' ) );::withtype( ¾ );say ::get( "2018-05-28" );# OUTPUT: «(Pair → left right Rat → 0.75)»
我们正在将 $typer
与上面定义的 $Logger::logs
函数组合,得到一个记录对象类型前缀的函数,这对于过滤等操作很有用。实际上,$Logger::withtype
是一个复杂的数据结构,它由两个以串行方式应用的函数组成,但每个组合的可调用对象都可以保持状态,从而创建复杂的变换可调用对象,这是一种类似于面向对象领域中对象组合的设计模式。您必须在每个特定情况下选择最适合您问题的编程风格。
定义和约束数据结构§
Raku 有不同的方法来定义数据结构,但也有很多方法来约束它们,以便您可以为每个问题域创建最合适的数据结构。例如,but
将角色或值混合到值或变量中。
my := %(2 => 3) but Associative[Int, Int];say .^name; # OUTPUT: «Hash+{Associative[Int, Int]}»say .of; # OUTPUT: «Associative[Int, Int]»= 4;<thing> = 3;say ; # OUTPUT: «{2 => 3, 3 => 4, thing => 3}»
在本例中,but
正在混合 Associative[Int, Int]
角色;请注意,我们正在使用绑定,以便变量的类型是定义的类型,而不是由 %
符号强加的类型;这个混合的角色显示在用大括号括起来的 name
中。这到底意味着什么?该角色包含两个方法,of
和 keyof
;通过混合角色,将调用新的 of
(旧的 of
将返回 Mu
,它是哈希的默认值类型)。但是,这就是它所做的全部。它并没有真正改变变量的类型,正如您所见,因为我们在接下来的几个语句中使用了任何类型的键和值。
但是,我们可以使用这种类型的混合为变量提供新的功能。
my := %( 3 => 33, 4 => 44) but Lastable;say .sort[0]; # OUTPUT: «3 => 33»say .last; # OUTPUT: «4 => 44»
在 Lastable
中,我们使用通用 self
变量来引用混合了该特定角色的任何对象;在本例中,它将包含混合了它的哈希;在其他情况下,它将包含其他内容(并且可能以其他方式工作)。这个角色将为它混合的任何变量提供 last
方法,为普通变量提供新的、可附加的功能。角色甚至可以使用 does
关键字添加到现有变量中。
子集 也可以用来约束变量可能持有的可能值;它们是 Raku 对 逐步类型化 的尝试;它不是一个完整的尝试,因为子集在严格意义上不是真正的类型,但它们允许运行时类型检查。它为常规类型添加了类型检查功能,因此它有助于创建一个更丰富的类型系统,允许像此代码中显示的那样的事情。
where (1/).Int == 1/;my OneOver = ⅓;say ; # OUTPUT: «0.333333»
另一方面,my OneOver $ = ⅔;
将导致类型检查错误。子集可以使用 Whatever
,即 *
,来引用参数;但这将在每次使用它时实例化为不同的参数,因此如果我们在定义中使用它两次,我们将得到一个错误。在本例中,我们正在使用主题单变量 $_
来检查实例化。子集可以在 签名 中直接完成,而无需声明它。
无限结构和惰性§
人们可能会认为数据结构中包含的所有数据实际上都“存在”。但情况并非总是如此:在许多情况下,出于效率原因或仅仅因为不可能,数据结构中包含的元素只有在实际需要时才会出现。这种按需计算项目的做法被称为具象化或活化。
# A list containing infinite number of un-reified Fibonacci numbers:my = 1, 1, * + * … ∞;# We reify 10 of them, looking up the first 10 of them with array index:say [^10]; # OUTPUT: «(1 1 2 3 5 8 13 21 34 55)»# We reify 5 more: 10 we already reified on previous line, and we need to# reify 5 more to get the 15th element at index 14. Even though we need only# the 15th element, the original Seq still has to reify all previous elements:say [14]; # OUTPUT: «987»
上面我们对使用序列运算符创建的Seq
进行了具象化,但其他数据结构也使用这个概念。例如,一个未具象化的Range
仅仅是两个端点。在某些语言中,计算一个巨大范围的总和是一个漫长且占用内存的过程,但 Raku 可以立即计算出来。
say sum 1 .. 9_999_999_999_999; # OUTPUT: «49999999999995000000000000»
为什么?因为总和可以在不具象化范围的情况下计算出来;也就是说,无需找出它包含的所有元素。这就是这个特性存在的原因。你甚至可以使用gather
和 take
来创建你自己的按需具象化对象。
my = gathersay "Let's reify an element!";say [0];say "Let's reify more!";say [1];say "Both are reified now!";say [^2];# OUTPUT:# Let's reify an element!# About to make 1st element# 1# Let's reify more!# About to make 2nd element# 2# Both are reified now!# (1 2)
根据上面的输出,你可以看到gather
内部的打印语句只在我们具象化单个元素并查找元素时才执行。还要注意,元素只具象化了一次。当我们在示例的最后一行再次打印相同的元素时,gather
内部的消息不再打印。这是因为构造使用了Seq
缓存中已经具象化的元素。
请注意,上面我们将gather
分配给了一个Scalar
容器($
符号),而不是Positional
容器(@
符号)。原因是@
符号的变量大多是急切的。这意味着它们会立即具象化分配给它们的东西,大多数情况下。它们唯一不这样做的时候是当这些项目被认为是is-lazy
的时候,比如我们使用无穷大作为结束点的序列。如果我们将gather
分配给一个@
变量,它内部的say
语句就会立即打印出来。
另一种完全具象化列表的方法是调用它的.elems
。这就是为什么检查列表是否包含任何项目最好使用.Bool
方法(或者直接使用if @array { … }
),因为你不需要具象化所有元素来找出它们中是否存在任何元素。
有时你确实希望在做某事之前完全具象化一个列表。例如,IO::Handle.lines
返回一个Seq
。以下代码包含一个错误;记住具象化,试着找出它。
my = "/tmp/bar".IO.open;my = .lines;close ;say [0];
我们打开一个文件句柄,然后将.lines
的返回值分配给一个Scalar
变量,因此返回的Seq
不会立即具象化。然后我们close
文件句柄,并尝试从$lines
打印一个元素。
代码中的错误是,当我们在最后一行具象化$lines
Seq
时,我们已经关闭了文件句柄。当Seq
的迭代器试图生成我们请求的项目时,它会导致尝试从已关闭的句柄中读取的错误。因此,为了修复错误,我们可以将它分配给一个@
符号的变量,或者在关闭句柄之前调用$lines
的.elems
。
my = "/tmp/bar".IO.open;my = .lines;close ;say [0]; # no problem!
我们也可以使用任何副作用是具象化的函数,比如上面提到的.elems
。
my = "/tmp/bar".IO.open;my = .lines;say "Read $lines.elems() lines"; # reifying before closing handleclose ;say [0]; # no problem!
使用eager也会具象化整个序列。
my = "/tmp/bar".IO.open;my = eager .lines; # Uses eager for reification.close ;say [0];
自省§
像 Raku 这样的允许自省的语言,在类型系统中附带了一些功能,让开发者可以访问容器和值的元数据。这些元数据可以在程序中使用,根据它们的值执行不同的操作。顾名思义,元数据是从值或容器中通过元类提取的。
my = "random object";my = .HOW;say .^mro; # OUTPUT: «((ClassHOW) (Any) (Mu))»say .can( , "uc" ); # OUTPUT: «(uc uc)»
第一个 say
语句展示了元模型类的类层次结构,在本例中是 Metamodel::ClassHOW
。它直接继承自 Any
,这意味着可以调用该类中的任何方法;它还混合了多个角色,可以提供有关类结构和函数的信息。但是,该特定类的一个方法是 can
,我们可以用它来查找对象是否可以使用 uc
(大写)方法,显然是可以的。然而,在某些情况下,当角色直接混合到变量中时,这可能并不那么明显。例如,在 上面定义的 %hash-plus
的情况下
say .^can("last"); # OUTPUT: «(last)»
在本例中,我们使用 HOW.method
的语法糖 ^method
来检查你的数据结构是否响应该方法;输出显示了匹配的方法名称,证明我们可以使用它。
另请参阅 这篇关于类内省的文章,了解如何访问类属性和方法,并使用它为类生成测试数据;这篇 Advent Calendar 文章详细介绍了元对象协议。