What It Set Out to Change — A Language Where 0.1 + 0.2 == 0.3 Is True
In Raku, 0.1 + 0.2 == 0.3 returns True, because decimal literals are rationals by default. That one line captures what Perl 6 prioritised: not surprising you when you write the obvious thing. Sigil invariance, grammars, multiple dispatch, meta-operators, junctions, lazy lists. Lay them out and it becomes clear — each is a language's worth of work. Half the reason it took fifteen years is right here.
Start With One Line
say 0.1 + 0.2 == 0.3; # True
An expression that returns False in most languages returns True in Raku.
The reason is simple: Raku’s decimal literals are rationals (Rat) by default.
0.1 is not “the double closest to one tenth” — it is held as the fraction 1/10 itself.
If you want floating point, you ask for Num.
There is a cost. Rational arithmetic is slower than floating point, and growing denominators need managing. They made it the default anyway, because not surprising you when you write the obvious thing was the higher priority.
That one line is a scale model of Perl 6’s entire design. Correctness bought with implementation cost. This instalment is the shopping list.
1. Sigil Invariance — the Famous Incompatibility
In Perl 5, a variable’s sigil changes with how you access it.
my @array = (1, 2, 3);
my $first = $array[0]; # $ , not @
my @slice = @array[0, 1]; # @ here
The rule is consistent: the sigil marks how many values come out. It is also the single biggest thing beginners trip over.
Perl 6 chose the opposite rule.
my @array = 1, 2, 3;
my $first = @array[0]; # stays @
“A sigil is the variable’s type, not its context.”
One rule change — and with it, Perl 5 code stops working at the syntax level. The decision to break compatibility is concentrated right here.
2. Twigils — One More Character After the Sigil
Perl 6 lets you put another character after the sigil, spelling out the kind of variable.
| Twigil | Meaning | Example |
|---|---|---|
! |
Class attribute (private) | $!name |
. |
Attribute via its accessor | $.name |
* |
Dynamic variable (looked up through callers) | $*IN, $*CWD |
? |
Compile-time constant | $?FILE, $?LINE |
^ |
Auto-declared positional parameter | { $^a + $^b } |
You can see the kind of scope just by looking.
The clearest case is Perl 5’s local (dynamic scope) being tidied up into $*.
3. Putting OO Into the Language
As part 2 covered, Perl 5’s OO was assembled from existing parts.
bless is a function; new is a convention.
Perl 6 has class / has / method / role as language features.
class Point {
has $.x = 0;
has $.y = 0;
method to-string { "($!x, $!y)" }
}
The one to note is role. Introduced as an alternative to multiple inheritance, it descends from the Smalltalk Traits research: state and behaviour bundled into composable units, with collisions detected at compile time.
And this object model flowed back into Perl 5 before Perl 6 ever shipped. That is Moose. As mentioned in part 2, Moose was a port of Perl 6’s object model. Perl 6’s design was changing Perl 5 before it was itself released.
4. Making Types Writable (but Optional)
sub add(Int $a, Int $b --> Int) { $a + $b }
You may omit them; if you write them, they are checked at runtime. Gradual typing.
And subset gives you types with predicates.
subset Even of Int where * %% 2;
subset Positive of Numeric where * > 0;
sub half(Even $n) { $n div 2 }
half(4); # fine
half(3); # type constraint failure
Any predicate can go in the where clause.
Validation lives in the type definition instead of at the top of every function.
This is the typing version of “easy things easy, hard things possible” from part 1.
5. Multiple Dispatch
One name, selected among by argument type, count, and constraint.
multi greet(Str $name) { "Hello, $name" }
multi greet(Int $times) { "Hi " x $times }
multi greet(Str $name, Int $n) { "Hello, $name" x $n }
multi greet($x where * < 0) { "Negative!" }
Where Java and C++ overloading resolves on static types, Raku resolves on the runtime types of the values. Closer to Common Lisp’s CLOS.
When several candidates match, the more specific one wins,
and a candidate with a where constraint counts as more specific than one with only a type.
6. Treating Operators Meta-ly
Perl 6’s distinctive invention is the meta-operator: an operator that takes an operator and produces an operator.
| Meta-operator | Example | Meaning |
|---|---|---|
reduce [ ] |
[+] 1..10 |
Fold. 55 |
hyper »« |
@a »+» 1 |
Apply elementwise |
cross X |
@a X @b |
Cartesian product |
zip Z |
@a Z @b |
Pair up |
negate ! |
!== |
Auto-generate the negated form |
assign = |
min= |
Auto-generate the assigning form |
Instead of adding N operators, add the rule that makes operators.
There is a lesson from Perl 5 in this. Perl 5 grew by adding operators. It is also the design-side answer to the bias described in part 3 — that RFCs only ever ask to add.
Here is this series’ fourth pattern.
Raise what gets added from features to rules. Features multiply as you add them; rules do not.
7. Junctions — Superposed Values
my $x = 2;
my @list = 1, 2, 3;
if $x == any(@list) { say 'equal to one of them' } # runs
if $x > all(@list) { say 'greater than all' } # does not run
say $x == any(@list); # any(False, True, False) — the distributed result, as is
say so $x == any(@list); # True — collapsed in boolean context
any / all / one / none are values, and comparisons distribute over them automatically.
say shows the distributed result as it is; it collapses only in boolean context — if, or so.
The feature is usually explained by analogy to quantum superposition.
It reaches into the type system too: junctions do not sit under Any.
So passing one to a function that takes an ordinary type triggers the distribution.
The type hierarchy has a reserved place for this feature.
8. Lazy Lists and Infinite Sequences
my @fib = 1, 1, * + * ... *; # infinite Fibonacci
say @fib[^10]; # (1 1 2 3 5 8 13 21 34 55)
... is the sequence operator; the trailing * (Whatever) means “forever.”
Lists are lazy by default, so you can put an infinite sequence in a variable
and take as much as you need later.
9. Changing the Grammar Itself
Raku’s parser is written as a Raku grammar. And users can extend the grammar at compile time.
sub infix:<∈>($x, @set) { $x (elem) @set }
say 2 ∈ (1, 2, 3); # True
Operators are defined under names like infix:<...> / prefix:<...> / postfix:<...>,
with associativity and precedence specified. The moment you define one, it is really syntax.
Grammars themselves get part 11. It is the thing Raku has that other languages don’t.
What You See When You Lay Them Out
Nine items above. Look at them again.
- Rationals by default — designing the numeric tower
- Sigil invariance — changing the entire look of the language
- OO in the language — designing an object model and a MOP
- Gradual typing — designing a type system
- Multiple dispatch — designing a dispatch mechanism
- Meta-operators — designing an operator system
- Junctions — adding a new inhabitant to the type hierarchy
- Lazy lists — changing the evaluation strategy
- Grammar extension — opening the parser to users
Any single one of these is a language’s worth of work.
And they are not independent. Junctions touch the type hierarchy, laziness touches dispatch resolution, grammar extension touches everything. There is a decision to make for every combination.
At the end of part 3 I said there were four reasons it took fifteen years. This is the first.
The design was too ambitious.
This is not a story about sloth. It is a story about how much they set out to do. And they largely did it. It took fifteen years.
From next time, we move to the people who tried to make it actually run. Starting with the fact that for nearly five years, there was nothing that ran.
Next (part 5): Four and a half years without an implementation, and one year of Pugs. In February 2005, a Perl 6 written in Haskell appears. It did not merely run — it rewrote the relationship between specification and implementation.