Haskell 和 Raku 是 *非常* 不同的语言。这是显而易见的。但是,这并不意味着它们之间没有相似之处或共同的想法!本页旨在帮助 Haskell 用户快速上手 Raku。Haskell 用户可能会发现,他们在使用 Raku 编写脚本时不必放弃所有 Haskell 式的思维方式。

请注意,这不能被误认为是 Raku 的初学者教程或概述;它旨在作为具有扎实 Haskell 背景的 Raku 学习者的技术参考。

类型§

类型与值§

在 Haskell 中,您有类型级编程,然后是值级编程。

plusTwo :: Integer -> Integer   -- Types
plusTwo x = x + 2               -- Values

在 Haskell 中,你不能像下面这样混合类型和值

plusTwo 2          -- This is valid
plusTwo Integer    -- This is not valid

在 Raku 中,类型(也称为类型对象)与值处于同一级别

sub plus-two(Int $x --> Int{ $x + 2 }
 
plus-two(2);    # This is valid 
plus-two(Int);  # This is valid 

我将通过另一个例子来说明 Raku 的这一独特方面

multi is-string(Str $ --> True{}
multi is-string(Any $ --> False{}
 
is-string('hello');    #True 
is-string(4);          #False 

也许§

在 Haskell 中,你有一个 Maybe 类型,它可以让你不必担心空类型。假设你有一个将字符串解析为整数的假设函数

parseInt :: String -> Maybe Integer

case parseInt myString of
  Just x  -> x
  Nothing -> 0

在 Raku 中,由于类型对象与普通对象共存,我们有了 Defined 和 Undefined 对象的概念。普通类型对象是未定义的,而实例化对象是已定义的。

sub parse-int(Str $s --> Int{ ... }
 
my $string = {...};
given parse-int($string{
  when Int:D { $_ }
  when Int:U { 0 }
}

因此,在 Raku 中,我们有类型约束来指示类型的定义性。它们是

Int:D# This is a defined Int. 
Int:U# This is an undefined Int, AKA a type object 
Int:_# This is either defined or undefined. 

如果我们想在上面的例子中明确说明(可能是个好主意),我们可以添加 :_ 约束到返回类型上。这将让用户知道他们应该考虑已定义和未定义的返回值。我们也可以使用其他方法和结构来专门测试定义性。

sub parse-int(Str $s --> Int:_{ ... }
 
# One way to do it 
my $string = {...};
given parse-int($string{
  when Int:D { $_ }
  when Int:U { 0 }
}
 
# Another way to do it 
my Int $number = parse-int($string);
if $number.defined { $number } else { 0 }
 
 
# A better way 
with parse-int($string{ $_ } else { 0 }
 
# With the defined-or operator 
parse-int($string// 0

你上面看到的 with 运算符类似于 if,不同的是它明确地测试定义性,然后将结果传递给后面的代码块。类似地,without 测试对象是否未定义,并将结果传递给后面的代码块。

为了更自然地控制未定义和已定义类型的流程,Raku 引入了 andthenorelse

sub parse-int(Str $s --> Int:_{ ... }
 
my $string = {...};
my $result = parse-int($stringorelse 0;
 
sub hello() { say 'hi' }
hello() andthen say 'bye';

因此,在实践中,Raku 没有空类型的概念,而是有已定义或未定义类型的概念。

数据定义§

Raku 本质上是一种面向对象的语言。但是,它也让你自由地在任何你想要的范式中编写代码。如果你只想使用接受一个对象并返回一个新对象的纯函数,你当然可以这样做。

这是一个 Haskell 代码示例

data Point = Point x y

moveUp :: Point -> Point
moveUp (Point x y) = Point x (y + 1)

以及等效的 Raku 示例

class Point { has $.xhas $.y}
 
sub move-up(Point $p --> Point{
  Point.new(x => $p.x=> $p.y + 1)
}

上面展示的代码是 Product Type 的一个例子。如果你想编写一个 Sum Type,Raku 中没有完全等效的类型。最接近的是 Enum

data Animal = Dog | Cat | Bird | Horse

testAnimal :: Animal -> String
testAnimal Dog   = "Woof"
testAnimal Horse = "Neigh"

虽然它不适合完全相同的用例,但它可以用于对类型进行约束。

enum Animal < Dog Cat Bird Horse >;
 
proto test-animalAnimal        ) {*}
multi test-animalDog           ) { 'Woof' }
multi test-animalAnimal::Horse ) { 'Neigh'  }   # more explicit 
 
say test-animal Animal::Dog;                          # more explicit 
say test-animal Horse;

类型别名和子集§

在 Haskell 中,你可以为现有类型创建别名,以提高意图的清晰度并重复使用现有类型。

type Name = String

fullName :: Name -> Name -> Name
fullName first last = first ++ last

Raku 中的等效代码如下。

my constant Name = Str;
 
sub full-name ( Name \firstName \last --> Name ) { first ~ last }

需要注意的是,在 Raku 中,还可以创建现有类型的子集。

subset Name of Str where *.chars < 20;
 
sub full-name(Name $firstName $last{
  $first ~ $last
}
 
full-name("12345678901234567890111""Smith"# This does not compile, as the first parameter 
                                              # doesn't fit the Name type 

类型类§

TODO

函数§

定义和签名§

模式匹配§

Haskell 在函数定义中大量使用模式匹配。

greeting :: String -> String
greeting  ""   = "Hello, World!"
greeting "bub" = "Hey bub."
greeting  name = "Hello, " ++ name ++ "!"

Raku 也这样做!你只需要使用 multi 关键字来表示它是一个多重调度函数。

proto greeting ( Str   --> Str ) {*}
multi greeting ( ""    --> "Hello, World!" ) {}
multi greeting ( "bub" --> "Hey bub." ) {}
multi greeting ( \name ) { "Hello, " ~ name ~ "!" }

proto 声明符不是必需的,但有时可以帮助确保所有 multi 都遵循你的业务规则。在 proto 的签名中使用变量名可以提供更多错误信息和自省信息。

proto greeting ( Str \name --> Str ) {*}
 
say &greeting.signature;                  # OUTPUT: «(Str \name --> Str)␤» 

上面 Raku 代码中需要注意的一点是,将 'bub' 之类的值作为函数参数传递只是 where 保护的语法糖。

保护§

使用本页“模式匹配”部分的示例,你可以看到幕后用于约束函数参数的保护。

multi greeting ( ""    --> "Hello, World!" ) {}
multi greeting ( "bub" --> "Hey bub." ) {}
 
# The above is the same as the below 
 
multi greeting(Str \name where ''    ) {'Hello, World!'}
multi greeting(Str \name where 'bub' ) {'Hey bub.'}
 
# The above is the same as the below, again. 
 
multi greeting(Str \name where $_ ~~ ''   ) {'Hello, World!'}
multi greeting(Str \name where $_ ~~ 'bub'{'Hey bub.'}

$_ 被称为主题变量。它会根据需要呈现出适当的形式。智能匹配运算符 ~~ 会找出确定左侧是否匹配右侧的最佳方法,无论是数字范围、字符串等。我们上面的三个示例从最糖化(顶部)到最少糖化(底部)。

上面底部的示例可以包含在花括号中,使其更明显地表明它是一个代码块。请注意,where 子句也可以接受显式的 Callable

multi greeting(Str \name where { $_ ~~ '' } ) {'Hello, World!'}
 
multi greeting(Str \name where -> $thing { $thing ~~ '' } ) {'Hello, World!'}
 
multi greeting ( Str \name where { Bool.pick } --> 'True' ){}
 
multi greeting ( Str \name where &some-subroutine ){}

如果你阅读了本页关于子集的部分,你会注意到“where”在创建子集和这里都被使用。这两个地方“where”的使用方式完全相同。

使用where时,请注意定义的顺序很重要,就像在Haskell中一样。

multi greeting ( Str \name where '' --> 'Hello, World!' ){}
multi greeting ( Str \name where { Bool.pick } --> 'True' ){}
multi greeting ( Str \name where 'bub' --> 'Hey, bub.' ){}
 
say greeting ''   ; # will never say True 
say greeting 'bub'# about 50% of the time it will say True 

参数解构§

TODO

柯里化/部分应用§

Haskell函数默认情况下是柯里化形式,因此它们可以直接进行部分应用。

plus : Int -> Int -> Int
plus a b = a + b

plusTwo = plus 2

在Raku中,部分应用可以通过任何Callable对象的assuming方法实现。

sub plus(Int $iInt $j --> Int{ return $i + $j}
 
my &plus-two = &plus.assuming(2*);

组合§

TODO

展示函数组合运算符。也许可以解释一下更原生的Raku方法来实现这一点。

Case/匹配§

Haskell大量使用case匹配,如下所示

case number of
  2 -> "two"
  4 -> "four"
  8 -> "eight"
  _ -> "don't care"

在Raku中,你可以使用given/when结构实现相同的功能

my $number = {...};
given $number {
  when 2  { "two" }
  when 4  { "four" }
  when 8  { "eight" }
  default { "don't care" }
}

请注意,when的顺序也很重要,就像本页guard部分中的where一样。

列表§

TODO

解释Raku数组、序列和列表之间的区别。解释关于@符号的数据形状。解释如何将数组转换为使用|@的扁平化对象列表。

数据形状变得非常直观,但需要一些练习。

列表推导§

Raku中没有显式的列表推导。但你可以通过几种不同的方式实现列表推导。

以下是一个简单的Haskell示例

evens = [ x | x <- [0..100], even x ]

现在在Raku中

# using `if` and `for` 
my @evens = ($_ if $_ %% 2 for 0..100);
 
# using gather/take to build a Seq 
my $evens = gather for 0..100 { take $_ if $_ %% 2 };
 
# using gather/take to build an Array 
my @evens = gather for 0..100 { take $_ if $_ %% 2 };

由于for总是急切的,因此通常最好使用mapgrep,它们将继承其列表参数的惰性或急切性。

my @evens = map { $_ if $_ %% 2 }0..100;
 
my @evens = grep { $_ %% 2 }0..100;
 
# using a Whatever lambda 
my @evens = grep  * %% 2,  0..100;

以下是Haskell中元组的创建

tuples = [(i,j) | i <- [1,2],
                  j <- [1..4] ]
-- [(1,1),(1,2),(1,3),(1,4),(2,1),(2,2),(2,3),(2,4)]

在Raku中

my @tuples = 1,2  X  1..4;
# [(1,1), (1,2), (1,3), (1,4), (2,1), (2,2), (2,3), (2,4)] 

有关Raku中可能有哪些类型的列表推导的更多信息,请参阅此设计文档:https://design.raku.org/S04.html#The_do-once_loop.

如你所见,当你使用一些更高级的Haskell列表推导时,Raku的翻译并不完全相同,但仍然可以做同样的事情。

Fold§

Haskell中的Fold在Raku中被称为Reduce。

mySum = foldl (+) 0 numList

my @numbers = {...};
reduce { $^a + $^b }0|@numbers;
@numbers.reduce: {$^a + $^b}

但是,在Raku中,如果你想使用一个中缀运算符(+ - / % 等),有一个很好的小助手叫做Reduction元运算符。

my @numbers = {...};
[+@numbers     # This is the same 
[+0|@numbers # as this 

它在列表中的所有值之间插入运算符,并生成一个结果,就像Fold一样。

在Haskell中,你有foldl和foldr。在Raku中,这个区别是由与运算符/子例程相关的结合性决定的。

sub two-elem-list ( \a, \b ) { ( ab ) }
 
# you can use a subroutine as an infix operator 
say 'a' [&two-elem-list'b'# OUTPUT: «(a b)␤» 
 
# as the reduction prefix metaoperator takes an infix operator, it will work there too; 
[[&two-elem-list]] 1..5;           # OUTPUT: «((((1 2) 3) 4) 5)␤» 
say (1..5).reduce: &two-elem-list# OUTPUT: «((((1 2) 3) 4) 5)␤» 
 
# right associative 
sub right-two-elem-list( \a, \b ) is assoc<right> { ( ab ) }
say (1..5).reduce: &right-two-elem-list# OUTPUT: «(1 (2 (3 (4 5))))␤» 
 
# XXX there is possibly a bug here as this currently doesn't look at 
# XXX the associativity of &right-two-elem-list and just always does left assoc 
say [[&right-two-elem-list]] 1..5;
 
# chaining 
say [<1..5;            # OUTPUT: «True␤» 
say (1..5).reduce: &[<]; # OUTPUT: «True␤» 

takeWhile§

Haskell中的takeWhile函数遍历一个列表,返回所有元素,直到满足一个条件为止

fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
takeWhile (<20) fibs -- Returns [0,1,1,2,3,5,8,13]

Raku中没有单个等效函数;在生成此文本的问题中提出了几种替代方案。这将是一个替代方案

[123405060789...^ !(* < 10)

虽然它不是一个单一的函数,但它本质上是使用序列运算符的一种特定方式,使用插入符号来排除最后一个项,并使用条件来结束序列。这个具体的例子将等效于

takeWhile (<10) [1, 2, 3, 40, 50, 60, 7, 8, 9]

Map§

TODO

范围§

Haskell和Raku都允许你指定值的范围。

myRange1 = 10..100
myRange2 = 1..        -- Infinite
myRange3 = 'a'..'h'   -- Letters work too

my $range1 = 10..100;
my $range2 = 1..*;      # Infinite 
my $range3 = 'a'..'h';  # Letters work too 

惰性与急切性§

在上面的例子中,你非常清楚地展示了惰性的概念。Raku只在最合理的地方使用惰性。例如,在范围10..100中,这是急切的,因为它有一个确定的结束。如果一个列表没有确定的结束,那么这个列表显然应该是惰性的。

(1 .. 100).is-lazy# False 
(1 .. Inf).is-lazy# True 

这些是Raku引以为傲的“明智的默认值”。但它们仍然是默认值,可以更改为其中之一。

(1 .. 100).lazy.is-lazy;       # True 
(1 .. 100).lazy.eager.is-lazy# False 

上下文(let-in/where)§

TODO

解释given/whenwith/without以及for循环如何使用参数作为上下文打开词法作用域。

将其与let/in和where结构进行比较,也许可以?

解析器§

解析器组合器与语法§

TODO

尾调用优化或尾调用消除§

Haskell 和许多其他函数式编程语言使用尾调用优化,有时也称为尾调用消除,来移除某些类型递归函数调用的堆栈开销。

Raku 语言规范中没有禁止实现此类优化,但目前没有实现它。

请注意,许多 Haskell 循环结构使用递归函数调用。如果没有尾调用优化,Haskell 程序会更频繁地遇到堆栈溢出错误。标准的 Raku 循环结构不是基于递归函数调用,这使得该功能不那么重要。