Raku, which started its life as Perl 6, was meant as successor to Perl language.
The main problem with that is that Perl 6 was officially announced in 2000, and only properly appeared in 2015, and in that time frame Perl lost most of its popularity, with most people moving to Ruby, PHP, or Python.
Raku isn't trying to achieve compatibility with Perl 5, but it follows very similar style. This probably seemed like a good idea back in 2000 when Perl was very popular, considerably less so nowadays.
But in addition to just being cleaned up Perl 5, Raku also has some really cool new concepts that are definitely worth taking a look at.
Hello, World!
#!/usr/bin/env raku
say "Hello, World!";
Two things to notice here.
The good thing - say
is a really nice name. Of all the words languages use for this concept, I think say
is much better than print
or puts
or console.log
or println
or prn
or writeln
and so on. Raku uses a lot of new names for various functions, some quite good, some not so much.
The bad thing - damn mandatory semicolons. Somehow Raku didn't drop this nonsense, even though they've been dropped by nearly every modern language by now, as well as by all good JavaScript coding styles (if you're coding JavaScript with semicolons, you're doing it wrong).
Every useless token increases mental load, and every pointless "missed semicolon" error costs both programmer time and concentration - extremely limited resources. There's no excuse to force semicolons in any new language.
Semicolons in Perl and Raku are statement separators not terminators, so unlike C/Java/etc. you can skip them for last statement of a block.
FizzBuzz
#!/usr/bin/env raku
sub fizzbuzz($n) {
given $n {
when $_ % 3 == 0 && $_ % 5 == 0 { "FizzBuzz" }
when $_ % 3 == 0 { "Fizz" }
when $_ % 5 == 0 { "Buzz" }
default { $_ }
}
}
for 1..100 {
say fizzbuzz($_)
}
Here's one way to do FizzBuzz. The loop is very nice, and 1..100
for range from 1
to 100
makes much more sense than range(1, 101)
convention.
Raku just like Perl uses $_
for default variable in a lot of contexts, such as for
loops and given
blocks. Just to make clear - these two $_
s are not overwriting each other, each $_
is specific to its block.
Fibonacci
#!/usr/bin/env raku
multi sub fib(1) { 1 }
multi sub fib(2) { 1 }
multi sub fib($n) { fib($n-1) + fib($n-2) }
for 1..30 {
say fib($_)
}
Raku allows defining multiple functions with the same name, matched depending on number of parameters, their types, or values. This looks quite Haskell-like.
Equality
Time to get to the most shitty part of Perl design, that 100% got copied over to Raku. There are no "numbers" or "strings". It's just scalars, and they convert automatically.
But it's not so simple. 2
and 2.0
are the same thing (as numbers), but "2"
and "2.0"
are completely different (as strings). So Perl and Raku have full parallel sets of operators. For some like concatenation (2 ~ 2
is 22
, +
is always numerical) that's fine, but the absolute worst is that there are two equalities: ==
for numbers and eq
for strings.
What's the problem with that? Well, in a normal language with normal ==
operator, it can be used not just for numbers and strings, but also for arrays, hashes, sets, or whatever else we throw at it.
In Raku we have this abomination:
#!/usr/bin/env raku
# Numerical
say 2 + 2 == 4;
say 2 == "2.0";
# String equality
say "hello" eq "hello";
# Looks like it works...
say [1, 2] eq [1, 2];
# Raku sorts the key order (unlike Ruby) so they match
say {b=>2, a=>1} eq {a=>1, b=>2};
# WTF? How is any of these True?
say ["one", "two"] eq "one two";
say [1, 2] eq ["1 2"];
say [1, 2] eq "1 2";
say {a=>1} eq "a\t1";
say {a=>1, b=>2} eq "a\t1\nb\t2";
It's True
for every one of them.
Also notice completely insane default stringification rules (Str()
) - instead of Str([1, 2])
being something sensible like [1, 2]
, it's "1 2"
. And for hashes, what the hell with those tabs and newlines?
JSON
There's another popular language with broken inequality - JavaScript. And the traditional workaround would be to JSON.stringify(a) === JSON.stringify(b)
.
Let's see how this goes. First Rakudo comes without JSON in standard library, and without package manager.
I needed to brew uninstall rakudo moarvm nqp
and brew install rakudo-star
instead to get its zef
package manager. That is really weird, but a lot of languages have packaging quirks, so let's roll with it. I didn't actually need to install any packages, as rakudo-star
package (but not rakudo
package) already included JSON::Tiny
.
But wait a minute, if Raku doesn't distinguish numbers from strings, how is it even doing JSON at all!
#!/usr/bin/env raku
use JSON::Tiny;
say to-json([1, 2]);
say to-json(["1", "2"]);
It turns out this works just fine:
$ ./json.raku
[ 1, 2 ]
[ "1", "2" ]
Raku acts as if there was no distinction between numbers and strings, but it's there behind the scenes internally. Number 2
and string "2"
are 99% same, except when they aren't.
I still remember this bug from my Perl days, where printing a variable would change its type:
#!/usr/bin/perl -l
use JSON;
my $x = 2;
print encode_json($x);
print "$x";
print encode_json($x);
Which led to this WTF:
$ ./json_bug.pl
2
2
"2"
So you'd print something for debugging, and suddenly your program would work completely differently. That was a fun evening, I still remember it 15 years later, and that issue was buried much deeper than this.
Anyway, I cannot reproduce it in Raku:
#!/usr/bin/env raku
use JSON::Tiny;
my $x = 2;
say to-json($x);
say "$x";
say to-json($x);
Where it definitely works:
$ ./json_bug.raku
2
2
2
But we'll probably still run into the issue where numbers and strings are 99% same except where they aren't. Maybe more experienced Raku programmers can tell me how to trigger this issue in Raku.
Oh and notice kebab-case identifiers are supported.
JSON Equality
Let's try our equality-by-JSON:
#!/usr/bin/env raku
use JSON::Tiny;
sub json-equal($a, $b) {
to-json($a) eq to-json($b)
}
# True as expected
say json-equal([1, 2], [1, 2]);
# False as expected
say json-equal([1, 2], "[1, 2]");
say json-equal("2", "2.0");
# Also True...
say json-equal(5, 5.0);
say json-equal(0, -0);
# Also False...
say json-equal(1, "1");
# Totally random if True or False
say json-equal({b=>2, a=>1}, {a=>1, b=>2});
First three tests are fine. Then 5
and 5.0
are same, as are 0
and -0
- because these are actually big rationals not floats, let's not get into Raku number system just yet.
1
and "1"
being not JSON-equal is something we already knew about.
But what about {b=>2, a=>1}
and {a=>1, b=>2}
? Str()
always sorted their keys so they were always eq
, but for JSON conversion their order is not guaranteed, so this program will print True
or False
at random.
Overall, total disaster. This can be worked around, but from my Perl and JavaScript experience, not having working universal ==
is massive pain that comes up all the time.
EQV
Oh wait, we're not done yet. There's eqv
! Let's see how it does:
#!/usr/bin/env raku
# True as expected
say [1, 2] eqv [1, 2];
say {b=>2, a=>1} eqv {a=>1, b=>2};
# This is still True
say 0 eqv -0;
# False as expected
say [1, 2] eqv "[1, 2]";
say "2" eqv "2.0";
# Now this is False too?
say 5 eqv 5.0;
say 1 eqv "1";
It's the closest to what we got so far. 1
and "1"
being different is what we already knew about. But now 5
and 5.0
are treated by eqv
as different (as integer and rational), even though they were the everywhere else up to this point. It's still the best we got so far, but it's a total embarrassment to design a language without a working universal ==
in this century.
Unicode
At least Perl never had problems with Unicode, and neither does Raku:
#!/usr/bin/env raku
say uc "Żółw";
say chars "Żółw";
say chars "🍰";
Which outputs completely correct:
$ ./unicode.raku
ŻÓŁW
4
1
State and Hashes
Let's do Fibonacci again, except with memoization:
#!/usr/bin/env raku
sub fib($n) {
state %cache = (0 => 0, 1 => 1);
%cache{$n} //= fib($n - 1) + fib($n - 2);
}
say fib($_) for 1..1000
Raku has a lot of scopes. my
are the usual local variables. state
are scoped to their function, but they're only initialized once.
Variable types can be inferred by the sigil - $
are scalar (strings, integers, and so on). @
are arrays. %
are hashes (or dictionaries). Because Raku knows their type, it can initialize them to correct empty values - but i this case we initialize it ourselves.
A //= B
sets A to B if it's missing (||=
checks truthiness of A, and that's not what we want, as 0, empty string etc. are false in Raku). It also returns the return value.
So this is extremely concise way to do memoization for a language that doesn't have builtin support for memoization specifically.
Oh and you can use if
, for
and such as a suffix. That definitely improves readability for "guard clauses" (return if ...
or return unless
or break if ...
), not necessarily here with a for
loop.
Junctions
Did you ever want to write if x == 1 | 2 | 3
, but your language wouldn't support it, so you needed to do if x == 1 || x == 2 || x == 3
instead?
I've got some good news!
#!/usr/bin/env raku
say "What's your name?";
my $name = get;
if ($name eq "admin" | "root") {
say "Don't try to hack me"
} else {
say "Hello, $name";
}
Let's give it a go:
$ ./junctions.raku
What's your name?
Alice
Hello, Alice
$ ./junctions.raku
What's your name?
admin
Don't try to hack me
get
reads one line, and |
creates a "junction". It's a collection of values in one of the modes (and, or, one, none) that performs all its operations on each element for as long as it can.
#!/usr/bin/env raku
say "Some of them match!" if (1 | 2 | 3) * 2 == (4 | 5);
say "All of them match!" if (1 & 2) * 2 == (4 | 5 | 2);
$ ./junctions2.raku
Some of them match!
All of them match!
Only when evaluated in boolean context junctions collapse to a single value. The code means:
- one of (
1 * 2
,2 * 2
,3 * 2
) equals one of (4
,5
) - all of (
1 * 2
,2 * 2
) equals one of (4
,5
,2
)
Junctions are pretty much unique to Raku, and I have no idea if they actually make coding easier, or they're just unnecessary complexity.
Regular expressions
Perl introduced its own extended regular expressions, and now every single language uses Perl style regular expressions, so it was a huge success. The pre-Perl version is basically forgotten. Perl's regular expressions were at least largely compatible with pre-Perl kind, so they were limited to using small number of symbols in some twisted combinations to achieve extra functionality.
Raku obviously introduced its own regular expression system, which tries to be more expressive and is intentionally not backwards compatible.
#!/usr/bin/env raku
say "What's your name?";
my $name = get;
if ($name ~~ /^<[a..z]>+$/ | /^<[A..Z]>+$/) {
say "Hello, $name"
} else {
say "Please use consistent case"
}
What is going on here...
$ ./regexp.raku
What's your name?
alice
Hello, alice
$ ./regexp.raku
What's your name?
BOB
Hello, BOB
$ ./regexp.raku
What's your name?
Charlie
Please use consistent case
First, the pattern matching with ~~
(Perl and Ruby use =~
). Any junction for |
. Neither is specific to regular expressions.
Interesting things start with /^<[a..z]>+$/
. Raku regular expressions has a lot of new features, and so they had to change some existing syntax to make space for all the new features. Character range [a-z]
became <[a..z]>
. The []
got repurposed to be non-capturing group ((?:)
in Perl regexp), which is arguably much more important functionality.
The subject of Raku Regular Expressions is big enough, that I'll need to dedicate a whole episode to it. It's basically a language within a language.
Unicode operators
It really should come at no surprise that Raku has Unicode operators in its syntax. After Julia, this is the second language we're investigating which does it, and I expect this to become mainstream feature of new programming languages going forward.
Porting Julia's example:
#!/usr/bin/env raku
say [1,2] ⊆ [2,3,1];
say 3 ∈ [2,3,1];
say (π/3).sin * 3.sqrt;
It outputs:
$ ./unicode_operators.raku
True
True
1.4999999999999998
Just like in Julia, set operations like ∈
and ⊆
work the same. π
is the usual constant. Unlike Julia, .sin
is a method not a standalone function, and .sqrt
is a method and there's no √
.
Neither Raku nor Julia go overboard with Unicode operators, but they can definitely make math code more readable.
Kitchen Sink Language Design
Raku is a kitchen-sink language like Perl or Scala. They tend to try out all the ideas, courageously break with pass assumptions, and some of these turns out to be amazing. But all the not-so-good ideas have high mental cost, so kitchen-sink languages tend to top the charts of the most disliked as well.
Sometimes it works wonders. Perl might have fallen far from its peak popularity, but it will live forever as the ancestor of pretty much every modern programming language. Ruby, JavaScript, PHP (and to lesser degree Python) are all basically Perl's children, and these languages in turn influenced everything else. It revolutionized programming language design. Just compare the last major pre-Perl language Java, which is a total usability disaster, with any of the post-Perl designs.
On a smaller scale CoffeeScript tried to throw a kitchen sink at making JavaScript bearable, and while it's largely dead now, ES6+ wouldn't exist without CoffeeScript paving the way.
But just as often, it doesn't work at all. Kitchen sink languages like Raku or Scala didn't get far, and so far didn't have much impact on other languages either.
Should you use Raku?
If you're interested in programming language design, or thinking about designing your own programming language, Raku is definitely worth checking out, even if you don't plan to write any real code in it. It tries out so many things, I barely scratched the surface here.
If you're a Perl programmer who for some reason doesn't want to switch to Ruby or Python like most, Raku might be worth a try. For me it hold on to too many bad design ideas from Perl, but if you're already accepting the costs, maybe it's not a big deal.
On the other hand, if you think Ruby is already Perl 6, or can't stand Perl style languages, then Raku will be highly unappealing.
I haven't really explored how well Raku works in pragmatic terms, such as its ecosystem, performance, and so on. From a quick look it doesn't look too great.
I'd definitely recommend Raku as language to give a go for a fun weekend, even if few would want to use it for anything more serious.
Code
All code examples for the series will be in this repository.
Top comments (9)
Thank you for looking into the Raku Programming Language. I feel however it needs some notes and corrections. Hope you won't mind too much.
The writer seems to have missed the fact that that is just THE part of Perl design that was NOT copied from Perl. In Raku, everything is an object (or can act as one). Numbers are either Int (bigints actually), Rat (rational number, consisting of 2 Ints), Num (floating point) or Complex (a real and a imaginary component). They are NOT just scalars.
The numeric operators, such as
==
,<
, etc, will attempt to coerce their operands to numeric values if they are not already. The string operators , such as 'eq', 'lt', etc, will coerce their operands to strings. That's a simple rule. No need to think much about that. Contrary to Javascript e.g., you don't have to guess whethera + b
will be a string concatenation or a numeric addition!That is completely incorrect: they do NOT convert automatically. It's the operators that coerce their operands. If you do a:
The
$a
is still a string. It only gets coerced to a numeric value for the addition with+
. Just as the constant666
is coerced to a string for the concatenation with~
.As soon as you grok that the operators coerce their operands, you also understand the behaviour of
eq
on lists and hashes:eq
in that case, works on the stringification of the lists and hashes. Which may produce unexpected results if you don't understand what the operators do.So generally, you don't use string comparison operators on lists or hashes. There are more useful operators, such as
eqv
which tests equivalence of both operands:or the set equality operator
(==)
or≡
which considers its operands asSet
s:They aren't. That's the big departure from Perl. A lesson learnt!
If you take the effort of not applying older experiences without more research, you will find that not having a universal
==
is actually a good thing.A lot of thought has gone into why operators work like they do in the Raku Programming Language. It would serve the author well by trying to move ahead from their misconceptions and hurt from the past before making a judgement.
These statements are factually incorrect:
That is correct, but easily fixed:
Note that (almost) all operators in Raku are just subroutines with special names.
That is a very true statement.
One note of caution. Use of the Raku Programming Language may be addictive. Even if you don't like the whole package, you may want to implement parts of it in the programming language of your choice! :-)
And if you want to seem some cool uses of Raku: there's this years Raku Advent Calendar (and the one from last year as well) with all sorts of interesting smaller and bigger blog posts about Raku.
And if you want to keep up-to-date on Rakudo developments (the most prominent implementation of Raku), you can check out the Rakudo Weekly News.
Due to my ex-Perl background, this episode is very much "how much Raku fixes Perl issues" story, that's why I'm focusing on this.
It's sort of true that
eqv
and lack of behind-the-scenes variable conversions are a huge improvement over Perl already. But are the fixes going far enough?For example these are three
eqv
-different objects, even though they print the same,==
each other,eq
each other, JSON-equal each other, and in a typical language with universal == they'd all be equal to each other (and are also pretty much indistinguishable in Perl):So using eqv as "universal ==" would require a lot of care to be paid to such invisible differences - something you don't need to do if you code in Ruby or Python or such, where == is completely predictable.
Another issue, and that comes up a lot. Now that we have eqv, can we have a Hash (or similar structure) that keys by eqv, just like Hashes in Ruby or dicts in Python work?
This doesn't work, and I'm not seeing anything that would work instead:
I know of Perl workarounds like stringifying keys or (for this simple case not in general) double-nesting Hashes, but they're drastically inferior to just being able to use arbitrary keys in Ruby (or at least arbitrary immutable keys in Python, which is close enough). Raku seems to be still suffering from Perl issues here.
JavaScript has same Perl legacy, and it used to suffer from the same issue, with only string-keyed hashes, and introduced ES6 Maps as a fix, so now we can use any objects as keys. I couldn't find any Raku equivalent.
As for sin vs .sin, I'm not sure why I missed that. You're right that both forms work.
Thanks for the writeup! It's always great to hear another prospective and this post gives some fairly strong indications of where our docs could be clearer.
One distinction I'd like to draw re:
==
andeqv
is between the "the behaviors of equality operators" and "the richness of the type system". As you point out, in JavaScript,1
,Math.cos(0)
and1.0
are all===
– and this makes perfect sense, because thetypeof
each one isnumber
and the value of each one is 1; thus, they really are equal, both in type and value.In contrast, in Raku,
1
is anInt
,cos(0)
is aNum
(i.e., a floating point number) and1.0
is aRat
(a rational number). And that distinction (sometimes!) matters, either for performance or correctness reasons, and I'm glad to have access toeqv
. (For example, with floating point rounding errors:.1 × 3 == .3
multiplies aRat
and thus returnsTrue
;.1e0 × 3 == .3
multiplies aNum
/float and returnsFalse
– the way most other languages behave.) In other cases, the distinction between1
doesn't matter – I just care how many of something there is – and I'm glad to have==
.Maybe it's because of the time I've spent with Rust and other languages that care more about their types, but I have a strong preference for equality operators that are explicit about their type conversions. Ruby's decision to have
1
and1.0
be==
even though they're different classes strikes me as a misfeature: in every other case==
tests for object equality, but in this one, special case it does something different. Though I totally understand that it's an area where people disagree :)Every language does 1 == 1.0 numerically, so Ruby isn't unusual here, Raku's eqv is the weird one out.
But mainly if you have a bunch of numbers of different types in Ruby, it is immediately obvious which number is what type, as they all print as different:
Same with Python:
And I think pretty much all other languages where there are ints and floats and other types. If there are multiple number types, they appear as different.
In Raku they appear identical as 1, while eqv treats them as different. I don't think this is a good design.
Well, "print as" is also a complex topic :) In Raku, the
.gist
method (which is used bysay
etc) prints both1.0
and1
as1
, but the.raku
method (which prints the debug representation) prints1.0
and1
. This feels correct to me – again, maybe because of time spent with Rust, which makes exactly the same choice.Oh, and re:
Again with Rust, not only does
1 == 1.0
not return true, it doesn't even compile (without an explicit cast). So, from a certain point of view, Raku's behavior represents something of a compromise between the dynamic behavior of a language like Ruby and the type-system-enforced guarantees of a more static language.And, really, Raku falls somewhere between those two extremes quite frequently. You noted Raku's clear Perl legacy, and that's definitely a big part of the linage. But Raku's DNA also owes a surprisingly large amount to Haskell due to a large number of Haskellers involved in the early design process (or so I've heard/read – I wasn't involved that early).
Haskell makes the same choices as Ruby and Python (and pretty much every other language):
Understood. But Raku borrowed a lot of ideas from many programming languages. And hopefully only the good things :-)
Well, I'd argue that
==
is completely predictable: it does a numerical comparison. I'd argue that a "universal ==" actually does more harm than good, as it may sweep potentially important differences "under the carpet" as it were.You seemed to have missed object hashes. In short, you can specify a constraint to be applied on the keys of a hash. The default is
Str()
, which is short forStr(Any)
, which means: acceptAny
object (all classes exceptJunction
andMu
inherit fromAny
) object and coerce it toStr
.An example of using just integer keys, would be:
If you want it to coerce to an
Int
, you can useInt()
as the type:Now, when it comes to using lists as a key, the situation is a little more complex. You can use a value type as a key in an object hash. An immutable
Set
is an example of a value type:Now, Lists and Arrays are not value types: Arrays because they are mutable, and Lists because they can contain mutable elements:
One approach to fix this for now, is the Tuple class. Discussions have been had about making Lists value types, but the above idiom is pretty pervasive in the Raku ecosystem and "darkpan", so not easy to fix. Perhaps a future version of Raku will allow for this to work:
to indicate a list with mutable elements, thereby freeing up
()
for immutable value type lists. The future will tell. Fortunately, Raku will allow such a change in syntax in a new language level: each compilation unit can indicate at which language level they wish to be compiled. This allows different semantics to be applied, while keeping interoperability between code of different language versions.In short: the Raku Programming Language has "change" built into it, to make it a language of choice for long-living code bases. And which code base doesn't live longer than intended :-)
Thanks for this exploration Tomasz. I found the write-up and all the comments helpful.