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 --> Int)plus-two(2); # This is validplus-two(Int); # This is valid
我将通过另一个例子来说明 Raku 的这一独特方面
multi is-string(Str $ --> True)multi is-string(Any $ --> False)is-string('hello'); #Trueis-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 --> Int)my = ;given parse-int()
因此,在 Raku 中,我们有类型约束来指示类型的定义性。它们是
Int; # This is a defined Int.Int; # This is an undefined Int, AKA a type objectInt; # This is either defined or undefined.
如果我们想在上面的例子中明确说明(可能是个好主意),我们可以添加 :_
约束到返回类型上。这将让用户知道他们应该考虑已定义和未定义的返回值。我们也可以使用其他方法和结构来专门测试定义性。
sub parse-int(Str --> Int)# One way to do itmy = ;given parse-int()# Another way to do itmy Int = parse-int();if .defined else# A better waywith parse-int() else# With the defined-or operatorparse-int() // 0
你上面看到的 with
运算符类似于 if
,不同的是它明确地测试定义性,然后将结果传递给后面的代码块。类似地,without
测试对象是否未定义,并将结果传递给后面的代码块。
为了更自然地控制未定义和已定义类型的流程,Raku 引入了 andthen
和 orelse
。
sub parse-int(Str --> Int)my = ;my = parse-int() orelse 0;sub hello()hello() andthen say 'bye';
因此,在实践中,Raku 没有空类型的概念,而是有已定义或未定义类型的概念。
数据定义§
Raku 本质上是一种面向对象的语言。但是,它也让你自由地在任何你想要的范式中编写代码。如果你只想使用接受一个对象并返回一个新对象的纯函数,你当然可以这样做。
这是一个 Haskell 代码示例
data Point = Point x y
moveUp :: Point -> Point
moveUp (Point x y) = Point x (y + 1)
以及等效的 Raku 示例
sub move-up(Point --> Point)
上面展示的代码是 Product Type 的一个例子。如果你想编写一个 Sum Type,Raku 中没有完全等效的类型。最接近的是 Enum。
data Animal = Dog | Cat | Bird | Horse
testAnimal :: Animal -> String
testAnimal Dog = "Woof"
testAnimal Horse = "Neigh"
虽然它不适合完全相同的用例,但它可以用于对类型进行约束。
< Dog Cat Bird Horse >;proto test-animal( Animal )multi test-animal( Dog )multi test-animal( Animal::Horse ) # more explicitsay test-animal Animal::Dog; # more explicitsay 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 \first, Name \last --> Name )
需要注意的是,在 Raku 中,还可以创建现有类型的子集。
of Str where *.chars < 20;sub full-name(Name , Name )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 )
proto
声明符不是必需的,但有时可以帮助确保所有 multi 都遵循你的业务规则。在 proto 的签名中使用变量名可以提供更多错误信息和自省信息。
proto greeting ( Str \name --> Str )say .signature; # OUTPUT: «(Str \name --> Str)»
上面 Raku 代码中需要注意的一点是,将 'bub'
之类的值作为函数参数传递只是 where
保护的语法糖。
保护§
使用本页“模式匹配”部分的示例,你可以看到幕后用于约束函数参数的保护。
multi greeting ( "" --> "Hello, World!" )multi greeting ( "bub" --> "Hey bub." )# The above is the same as the belowmulti greeting(Str \name where '' )multi greeting(Str \name where 'bub' )# The above is the same as the below, again.multi greeting(Str \name where ~~ '' )multi greeting(Str \name where ~~ 'bub')
$_
被称为主题变量。它会根据需要呈现出适当的形式。智能匹配运算符 ~~
会找出确定左侧是否匹配右侧的最佳方法,无论是数字范围、字符串等。我们上面的三个示例从最糖化(顶部)到最少糖化(底部)。
上面底部的示例可以包含在花括号中,使其更明显地表明它是一个代码块。请注意,where 子句也可以接受显式的 Callable
。
multi greeting(Str \name where )multi greeting(Str \name where -> )multi greeting ( Str \name where --> 'True' )multi greeting ( Str \name where )
如果你阅读了本页关于子集的部分,你会注意到“where”在创建子集和这里都被使用。这两个地方“where”的使用方式完全相同。
使用where
时,请注意定义的顺序很重要,就像在Haskell中一样。
multi greeting ( Str \name where '' --> 'Hello, World!' )multi greeting ( Str \name where --> 'True' )multi greeting ( Str \name where 'bub' --> 'Hey, bub.' )say greeting '' ; # will never say Truesay 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 , Int --> Int)my = .assuming(2, *);
组合§
TODO
展示函数组合运算符。也许可以解释一下更原生的Raku方法来实现这一点。
Case/匹配§
Haskell大量使用case匹配,如下所示
case number of
2 -> "two"
4 -> "four"
8 -> "eight"
_ -> "don't care"
在Raku中,你可以使用given/when结构实现相同的功能
my = ;given
请注意,when
的顺序也很重要,就像本页guard部分中的where
一样。
列表§
TODO
解释Raku数组、序列和列表之间的区别。解释关于@
符号的数据形状。解释如何将数组转换为使用|@
的扁平化对象列表。
数据形状变得非常直观,但需要一些练习。
列表推导§
Raku中没有显式的列表推导。但你可以通过几种不同的方式实现列表推导。
以下是一个简单的Haskell示例
evens = [ x | x <- [0..100], even x ]
现在在Raku中
# using `if` and `for`my = ( if %% 2 for 0..100);# using gather/take to build a Seqmy = gather for 0..100 ;# using gather/take to build an Arraymy = gather for 0..100 ;
由于for
总是急切的,因此通常最好使用map
或grep
,它们将继承其列表参数的惰性或急切性。
my = map , 0..100;my = grep , 0..100;# using a Whatever lambdamy = 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 = 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 = ;reduce , 0, |;.reduce:
但是,在Raku中,如果你想使用一个中缀运算符(+ - / % 等),有一个很好的小助手叫做Reduction元运算符。
my = ;[+] # This is the same[+] 0, | # as this
它在列表中的所有值之间插入运算符,并生成一个结果,就像Fold一样。
在Haskell中,你有foldl和foldr。在Raku中,这个区别是由与运算符/子例程相关的结合性决定的。
sub two-elem-list ( \a, \b )# you can use a subroutine as an infix operatorsay 'a' [] 'b'; # OUTPUT: «(a b)»# as the reduction prefix metaoperator takes an infix operator, it will work there too;[[]] 1..5; # OUTPUT: «((((1 2) 3) 4) 5)»say (1..5).reduce: ; # OUTPUT: «((((1 2) 3) 4) 5)»# right associativesub right-two-elem-list( \a, \b ) is assoc<right>say (1..5).reduce: ; # 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 assocsay [[]] 1..5;# chainingsay [<] 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中没有单个等效函数;在生成此文本的问题中提出了几种替代方案。这将是一个替代方案
[1, 2, 3, 40, 50, 60, 7, 8, 9] ...^ !(* < 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 = 10..100;my = 1..*; # Infinitemy = '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/when
和with/without
以及for循环
如何使用参数作为上下文打开词法作用域。
将其与let/in和where结构进行比较,也许可以?
解析器§
解析器组合器与语法§
TODO
尾调用优化或尾调用消除§
Haskell 和许多其他函数式编程语言使用尾调用优化,有时也称为尾调用消除,来移除某些类型递归函数调用的堆栈开销。
Raku 语言规范中没有禁止实现此类优化,但目前没有实现它。
请注意,许多 Haskell 循环结构使用递归函数调用。如果没有尾调用优化,Haskell 程序会更频繁地遇到堆栈溢出错误。标准的 Raku 循环结构不是基于递归函数调用,这使得该功能不那么重要。