Learn Raku

Raku (formerly Perl 6, renamed in 2019) is a gradually typed, multi-paradigm member of the Perl family, designed by Larry Wall and developed over many years as a clean-slate sister language to Perl. Its reference compiler, Rakudo, runs on the MoarVM virtual machine and implements the Raku 6.d language specification, with new releases roughly monthly. Raku is expressive and unusually feature-rich: a sigil-based variable system, arbitrary-precision and rational numbers, Unicode-correct strings, lazy and infinite lists, multiple dispatch, an introspectable object model with composable roles, first-class grammars for parsing, and high-level concurrency built on promises, supplies, and react/whenever. This track teaches Raku from the ground up - installing Rakudo and zef, variables and gradual typing, subroutines and signatures, collections and lazy pipelines, exceptions and soft failures, concurrency, classes and grammars, and packaging modules for the ecosystem - with runnable examples and links to the official docs at docs.raku.org.

Setup and the Rakudo Toolchain

Install Rakudo, run your first program, and meet the REPL and one-liners.

Setup and the Rakudo Toolchain

Raku is a member of the Perl family of languages, but a distinct language with its own design. It is gradually typed, multi-paradigm (object-oriented, functional, and procedural), and famous for built-in grammars, lazy lists, and first-class concurrency. The reference compiler is Rakudo, which runs on the MoarVM virtual machine and implements the Raku language specification version 6.d.

Installing Rakudo

The easiest way to get a full batteries-included setup is the Rakudo Star bundle, which ships the compiler plus the zef package manager and a set of popular modules. You can also install the bare compiler from your OS package manager, or manage multiple versions with rakubrew.

# Debian/Ubuntu
$ sudo apt-get install rakudo

# macOS (Homebrew)
$ brew install rakudo-star

Confirm the install. The raku command is the entry point (the older perl6 name still works as an alias):

$ raku --version
Welcome to Rakudo(tm) v2024.09.
Implementing the Raku(R) Programming Language v6.d.
Built on MoarVM version 2024.09.

Rakudo follows a roughly monthly release cadence (release #192 was 2026.04), so the version string you see will be newer; the language itself is stable at 6.d.

Your first program

Raku source files use the .raku extension (modules use .rakumod, tests .rakutest). Create hello.raku:

say "Hello, Raku!";

say prints its argument followed by a newline (it calls .gist, a human-friendly representation). Run it:

$ raku hello.raku
Hello, Raku!

There is no separate compile step to manage and no main boilerplate required for a script: top-level statements run top to bottom. If you want an explicit entry point that receives command-line arguments, declare a sub MAIN:

sub MAIN(Str $name, Int :$times = 1) {
    say "Hi, $name!" for ^$times;
}
$ raku hello.raku --times=2 Ada
Hi, Ada!
Hi, Ada!

Raku turns the signature of MAIN into an argument parser for free, including a generated usage message when the arguments do not match. Named options like --times=2 go before the positional arguments. The ^$times is a shorthand range 0 ..^ $times.

The REPL

Running raku with no file drops you into an interactive REPL, perfect for experimenting:

$ raku
> say 6 * 7;
42
> (1..100).sum
5050

One-liners

Like its Perl heritage, Raku is excellent for shell one-liners. -e runs a program from the command line, and -n/-p wrap your code in a loop over input lines (the current line is in the topic variable $_):

$ raku -e 'say (1..100).sum'
5050

# print the character count of each line of a file
$ raku -ne 'say .chars' lines.txt

A leading dot like .chars calls a method on $_, the implicit topic.

Editor support and docs

Most editors have Raku syntax highlighting, and the Comma/community plugins add deeper support. The single most valuable resource is the official documentation, which is searchable and example-rich, plus the offline p6doc/Rakudoc viewer that ships with many distributions.

With the toolchain working, you are ready to learn the language itself, starting with its variables and types.

Sigils, Variables, and Types

Scalars, arrays, hashes, the sigil system, and gradual typing.

Sigils, Variables, and Types

Raku's most visible feature is its sigils: the leading symbol on a variable name tells you, at a glance, what kind of thing it holds. This lesson covers declarations, the sigil system, the basic types, and Raku's gradual typing.

Declaring variables with my

my declares a lexically scoped variable, visible only within its enclosing block:

my $name = "Ada";        # scalar: a single value
my @nums = 1, 2, 3, 4;   # array: an ordered list
my %ages = Ada => 36, Alan => 41;  # hash: key/value pairs

You can declare several at once: my ($x, $y, %config);.

The sigils

Sigil Name Holds Example
$ scalar one item (any single value, even a whole list as one object) $name
@ positional an ordered, indexable list @nums
% associative a key/value map %ages
& callable a routine you can invoke &greet

Sigils are not just decoration: they drive string interpolation and clarify intent. Inside a double-quoted string, a variable interpolates, and you can interpolate an expression by wrapping it so it produces output:

say "Hello, $name!";              # Hello, Ada!
say "First number is @nums[0]";   # indexing interpolates
say "Total: { @nums.sum }";       # {} interpolates any expression

Indexing: postcircumfix brackets

Use [ ] to index a positional and < > (or { }) for an associative:

say @nums[0];      # 1
say @nums[*-1];    # 4  (the *-1 means "last")
say %ages<Ada>;    # 36  (<> is a quote-words subscript)
say %ages{'Alan'}; # 41

Twigils

A twigil is a second symbol between the sigil and the name that marks a special scope. The most common are $! for a private object attribute, $. for a public accessor attribute, and $* for a dynamic variable (looked up through the call stack):

say $*CWD;     # built-in dynamic variable: current working directory
say $*PID;     # process id

Basic types and gradual typing

Raku is gradually typed: you may add type constraints where you want safety, and omit them where you want flexibility. An untyped variable holds Any.

my Int $count = 5;
my Str $label = "items";
my Num $ratio = 1.5e0;
say $count.^name;   # Int   (.^name introspects the type)
say $label.WHAT;    # (Str)

Numbers are richer than in most languages. Integers are arbitrary precision, and 1/3 is a real rational (Rat), not a lossy float:

say 2 ** 100;       # 1267650600228229401496703205376
say (1/3).WHAT;     # (Rat)
say 0.1 + 0.2;      # 0.3   (exact, because these are Rats)

Strings are Unicode-correct

A Raku Str is a sequence of graphemes (what a human perceives as a character), so length and indexing behave intuitively even with combining marks:

my $s = "héllo";
say $s.chars;       # 5
say $s.uc;          # HÉLLO
say "abc".comb.reverse.join;   # cba

.comb splits into characters; .words, .lines, and .split are the other common splitters.

Mutability: containers vs values

my $x creates a container you can reassign. To declare a true constant, use constant; to bind a name directly to a value (no reassignment), use :=:

constant PI = 3.14159;
my $a := 42;        # bound; $a = 1 would now be an error

Reference

Next we turn these declarations into reusable behavior with subroutines and signatures.

Subroutines, Signatures, and Multiple Dispatch

Parameters, named/optional/slurpy args, multi subs, blocks, and Whatever.

Subroutines, Signatures, and Multiple Dispatch

Raku has one of the most expressive parameter systems of any language. A subroutine's signature can declare positional, named, optional, typed, and slurpy parameters, and you can define several variants of the same routine that the runtime chooses between by type and shape.

Defining a sub

sub defines a named routine; the last expression is the implicit return value:

sub add($a, $b) { $a + $b }
say add(5, 3);   # 8

Use return for an early exit. Subs are lexically scoped by default.

Positional, optional, and default parameters

Parameters are positional by default. Mark one optional with ?, or give it a default with = (which also makes it optional):

sub power($base, $exp = 2) { $base ** $exp }
say power(5);     # 25
say power(5, 3);  # 125

Named parameters

A leading colon makes a parameter named - passed by name in any order. Combine with a default to make it optional:

sub greet(:$name, :$title = 'Mr') {
    "Hello $title $name"
}
say greet(name => 'Alice', title => 'Dr');  # Hello Dr Alice
say greet(:name<Bob>);                        # Hello Mr Bob

The :name<Bob> form is shorthand for name => "Bob", and a bare :debug is shorthand for debug => True.

Slurpy parameters

A *@ parameter slurps all remaining positionals into an array, and *% slurps named arguments into a hash:

sub describe(*@items, *%opts) {
    "items={@items.join(',')} opts={%opts.kv.join(',')}"
}
say describe(1, 2, 3, verbose => True);
# items=1,2,3 opts=verbose,True

The reduction metaoperator [+] sums a list, so a tidy sum-everything sub is just:

sub total(*@n) { [+] @n }
say total(1, 2, 3, 4);   # 10

Type constraints and where

You can constrain a parameter by type and even by an arbitrary predicate with where:

sub fac(Int $n where * >= 0) { [*] 1..$n }
say fac(5);   # 120

Here * >= 0 is a Whatever expression: the * becomes the argument, producing a one-parameter test. Calling fac(-1) fails the constraint and dies with a clear message.

Multiple dispatch with multi

Instead of branching on type inside one function, declare several multi candidates. Raku dispatches to the best match by argument types and arity:

multi greet(Int $x) { "int: $x" }
multi greet(Str $x) { "str: $x" }
say greet(42);     # int: 42
say greet("hi");   # str: hi

This is checked at the signature level, so adding a new case never requires editing the others - a clean alternative to long if/given chains.

Blocks, pointy blocks, and placeholders

A bare { ... } is a block; inside it $_ is the topic. A pointy block -> $x { ... } names its parameters explicitly, and placeholder variables $^a, $^b declare parameters implicitly in alphabetical order:

my $add  = -> $a, $b { $a + $b };
say $add(3, 4);             # 7

my &square = { $^x ** 2 };  # &-sigil binds a callable
say square(6);              # 36

Whatever currying in chains

The * Whatever shines in collection pipelines, where it builds a small closure:

say (1..5).map(* ** 2).join(", ");   # 1, 4, 9, 16, 25
say (1..3).map(* + 10).join(",");    # 11,12,13

Reference

Next: the collection types and the pipeline style that makes Raku data processing so compact.

Lists, Arrays, Hashes, and Lazy Pipelines

map/grep/sort, ranges, junctions, lazy gather/take, and reductions.

Lists, Arrays, Hashes, and Lazy Pipelines

Raku gives you ordered collections (List, Array), key/value collections (Hash), and a rich, chainable set of list operations. Many sequences are lazy, so you can describe infinite data and only compute what you consume.

Arrays

An @-array is mutable and growable. Push, pop, and friends work as you would expect:

my @nums = 1, 2, 3;
@nums.push(4);
@nums.unshift(0);
say @nums;           # [0 1 2 3 4]
say @nums.elems;     # 5
say @nums[*-1];      # 4   (last element)

Ranges

A Range describes a span of values lazily. .. is inclusive, ..^ excludes the endpoint, and the caret prefix ^n means 0 ..^ n:

say (1..5).list;     # (1 2 3 4 5)
say (^5).list;       # (0 1 2 3 4)
say ('a'..'e').join; # abcde

The core trio: map, grep, sort

These transform, filter, and order. They chain naturally because each returns a new sequence:

my @nums = 1..10;
say @nums.grep(* %% 2).join(" ");   # 2 4 6 8 10  (%% is "divisible by")
say @nums.map(* ** 2).join(" ");    # squares
say <pear apple fig>.sort.join(","); # apple,fig,pear
say (3, 1, 2).sort(-*).join(",");    # 3,2,1  (sort by negated value)

A typical pipeline reads left to right:

my @result = (1..10).grep(* %% 2).map(* ** 2);
say @result;   # [4 16 36 64 100]

Hashes

A %-hash maps keys to values. Iterate keys, values, or pairs:

my %ages = Ada => 36, Alan => 41, Grace => 85;
say %ages<Ada>;                       # 36
say %ages.keys.sort.join(",");        # Ada,Alan,Grace
for %ages.sort(*.key) -> $pair {
    say "{$pair.key} is {$pair.value}";
}

%ages.kv yields a flat key, value, key, value list; .pairs yields Pair objects.

Junctions: one value, many possibilities

A junction superimposes several values and tests them all at once. any (written |), all (&), and one collapse to a Boolean in comparison:

say 3 == (1 | 3 | 5);     # True   (any matches)
say 4 < (5 & 6 & 7);      # True   (all are greater)
my $c = 'e';
say $c eq any(<a e i o u>);  # True  -> it is a vowel

Junctions let you write conditions that read like plain English without explicit loops.

Reductions and metaoperators

Wrap an operator in [ ] to fold it across a list - the reduction metaoperator:

say [+] 1..5;     # 15   (sum)
say [*] 1..5;     # 120  (product / factorial of 5)
say [max] 3, 9, 2;  # 9

The hyper metaoperator >>op<< applies an operator element-wise across lists.

Laziness: gather/take and infinite sequences

A gather block produces values lazily; each take emits one. Mark the binding lazy so an infinite generator does not run forever:

my @fib = lazy gather {
    my ($a, $b) = 0, 1;
    loop {
        take $a;
        ($a, $b) = $b, $a + $b;
    }
};
say @fib[^10].join(",");   # 0,1,1,2,3,5,8,13,21,34

Because the list is lazy, asking for the first ten elements computes exactly ten Fibonacci numbers and stops. The sequence operator ... can also build lazy sequences directly: (1, 1, * + * ... *) is the Fibonacci sequence.

Reference

Next we handle the failures real programs produce, with Raku's exception and Failure model.

Exceptions, try, CATCH, and Failures

die/throw, the lexical CATCH block, custom exceptions, and soft failures.

Exceptions, try, CATCH, and Failures

Raku has a full exception system built on the Exception type hierarchy, plus a lighter-weight notion of a Failure - an error you can choose to handle as a value instead of as a thrown exception. Together they let you pick the right level of ceremony for each error.

Throwing: die and throw

die raises an exception. Given a plain string it wraps it in X::AdHoc; given an Exception object it throws that:

die "something went wrong";

You can also build and throw a typed exception explicitly with .throw.

Catching: the lexical CATCH block

Unlike try/catch in many languages, a Raku CATCH block lives inside the block whose exceptions it handles, and it applies to that whole lexical scope no matter where you place it. The thrown exception arrives in the topic $_, and you match it with when/default:

{
    die "boom";
    CATCH {
        when X::AdHoc { say "caught: ", .message }
        default      { say "other: ", .message }
    }
    say "not reached after an uncaught die";
}
say "execution continues here";

When CATCH handles an exception, the enclosing block exits normally (the statements after the die are skipped) and the program continues after that block, unless you explicitly .resume.

try: catch as an expression

try is the quick form. It runs a block, swallows any exception, sets $! to it, and evaluates to the result (or Nil on failure) - which pairs beautifully with the defined-or operator //:

my $n = try { +"99999" } // 0;    # 99999
my $m = try { +"oops"  } // 0;    # 0, conversion failed
say "$n $m";                       # 99999 0

if $! { say "last error: ", $!.message }

Custom exception types

Subclass Exception and provide a message method. Adding typed attributes lets callers inspect structured detail:

class X::Validation is Exception {
    has $.field;
    has $.reason;
    method message { "validation failed on $!field: $!reason" }
}

sub check($name) {
    die X::Validation.new(field => 'name', reason => 'required')
        unless $name;
    $name
}

{
    check("");
    CATCH {
        when X::Validation { say "bad field: ", .field }
    }
}

Because you matched on the type, you get the original object back in $_ and can read .field.

Failures: soft, unthrown errors

fail returns a Failure instead of throwing immediately. A Failure stays dormant as long as you treat it like a value; it only throws if you actually use it in a way that demands a real result. This lets a caller decide whether an error is fatal:

sub parse-port($s) {
    return fail "not a number: $s" unless $s ~~ /^ \d+ $/;
    +$s
}

my $p = parse-port("abc");        # no throw yet; $p holds a Failure
if $p ~~ Failure {                # checking it is safe
    say "using default port";
    $p = 8080;
}
say $p;   # 8080

Many built-ins return Failures (for example, opening a missing file), so you can handle them with // defaults or check them explicitly rather than wrapping everything in try.

When to use which

  • Use try for a one-off conversion or call where any error means "use a fallback."
  • Use a CATCH block when a region of code can fail in several ways you want to distinguish by type.
  • Define custom exceptions when callers need structured information about the failure.
  • Return a Failure from your own routines when the caller, not you, should decide whether the error is fatal.

Reference

Next we reach one of Raku's signature strengths: high-level concurrency.

Concurrency: Promises, Supplies, and react/whenever

start/await promises, reactive supplies, the react block, and hyper/race.

Concurrency: Promises, Supplies, and react/whenever

Concurrency is built into Raku at the language level. Rather than juggling raw threads, you compose Promises (one future value), Supplies (streams of values over time), and Channels (thread-safe queues), and you drive event loops with the declarative react/whenever syntax. A thread pool schedules the work for you.

Promises and start

start { ... } runs a block on the thread pool and returns a Promise that will be kept with the result or broken with an exception. await blocks until it settles and returns the value:

my $p = start { (1..1000).sum };
say await $p;        # 500500

await accepts several promises and returns all their results once every one completes, giving you fan-out parallelism in one line:

my @results = await (start { 6 * 7 }), (start { 2 ** 10 });
say @results;        # [42 1024]

Promise.allof(@promises) and Promise.anyof(@promises) build a promise that is kept when all (or any) of the inputs settle, and .then chains a follow-up computation.

Supplies: asynchronous streams

A Supply is an asynchronous stream of values. A Supplier is the producer side: you emit values, and subscribers tap them. Supplies support the same grep/map pipeline as ordinary lists, but applied to values as they arrive:

my $supplier = Supplier.new;
my $supply   = $supplier.Supply;

$supply.grep(* %% 2).tap(-> $v { say "even: $v" });

$supplier.emit($_) for 1..6;   # prints even: 2, even: 4, even: 6

react and whenever

The react block is the idiomatic way to consume one or more asynchronous sources. Inside it, each whenever subscribes to a stream and runs its body for every value; the block stays alive until you call done:

my $count = 0;
react {
    whenever Supply.interval(0.05) -> $tick {
        $count++;
        done if $tick == 3;   # stop after ticks 0,1,2,3
    }
}
say "ticks=$count";   # ticks=4

A single react can watch many sources at once - a timer, a network socket, and a Channel - each with its own whenever, which makes event-driven code read like a list of rules:

react {
    whenever $orders.Supply { say "order: $_" }
    whenever Supply.interval(60) { say "heartbeat" }
}

Inside a whenever, the phasers LAST { ... } and QUIT { ... } handle stream completion and errors respectively.

Channels: thread-safe queues

A Channel is a producer/consumer queue. Unlike a Supply, each value goes to exactly one receiver, which makes channels ideal for distributing work:

my $ch = Channel.new;
$ch.send("a");
$ch.send("b");
$ch.close;
say $ch.list.join(",");   # a,b

You can also drive a channel from a react block with whenever $ch { ... }.

Data parallelism: hyper and race

For CPU-bound work over a collection you do not need promises at all. Insert .hyper (order preserved) or .race (order not guaranteed) into a pipeline and the map/grep runs across multiple cores automatically:

say (1..6).hyper.map(* ** 2).join(",");   # 1,4,9,16,25,36

The result is the same as a plain .map; only the execution is parallel. Use .hyper when you must keep input order and .race when you do not, for a little more speed.

Guidance

  • Keep whenever bodies short; offload heavy work into a start block so the event loop stays responsive.
  • Prefer these high-level tools (Promise, Supply, Channel, react) over manual Thread/Lock use.
  • Use .hyper/.race for embarrassingly parallel data processing, and Promises for independent tasks.

Reference

Next we cover Raku's object model and grammars before touching the module ecosystem.

Classes, Roles, and Grammars

Define classes and attributes, compose roles, and parse with grammars.

Classes, Roles, and Grammars

Raku has a powerful, introspectable object system. You build types from classes, share behavior with roles (composable units that beat single inheritance), and - uniquely - write parsers as first-class grammars built from regexes.

Classes and attributes

A class groups attributes and methods. The $. twigil declares a public, read-only attribute with an auto-generated accessor; $! declares a private one. Add is rw to make an attribute writable from outside:

class Point {
    has $.x;
    has $.y;
    has Str $.label is rw = "origin";

    method dist-to(Point $o) {
        sqrt(($.x - $o.x)**2 + ($.y - $o.y)**2)
    }
}

my $p = Point.new(x => 0, y => 0);
my $q = Point.new(x => 3, y => 4);
say $p.dist-to($q);   # 5
$p.label = "start";   # allowed because label is rw

new is provided for free and accepts named arguments matching the attributes. Inside methods, $.x calls the accessor while $!x reads the attribute directly; self is the invocant.

Roles: composition over inheritance

A role is a bundle of methods (and attributes) that you mix into classes with does. Unlike inheritance, composing multiple roles is flat and order-independent, and a conflict is a compile-time error rather than a silent override:

role Greeter {
    method greet { "Hi, I am " ~ self.name }
}

class Person does Greeter {
    has Str $.name;
    has Int $.age = 0;
    method birthday { $!age++ }
}

my $person = Person.new(name => "Ada", age => 36);
say $person.greet;     # Hi, I am Ada
$person.birthday;
say $person.age;       # 37

Use a class for the "is-a" identity and roles for reusable "can-do" capabilities.

Dispatch and given/when

Methods can be multi just like subs, dispatching by argument type. For value-based branching inside a method, given/when is the topicalizing switch - when smart-matches against $_:

sub classify($n) {
    given $n {
        when * < 0 { "negative" }
        when 0     { "zero" }
        default    { "positive" }
    }
}
say classify(-5), " ", classify(0), " ", classify(7);
# negative zero positive

Introspection with the meta-object

Every object exposes a meta-object via .^ calls (the meta-method operator). This makes runtime reflection trivial:

say 42.^name;          # Int
say Point.^methods.map(*.name).sort;   # method names of Point
say $person.^attributes.elems;         # number of attributes

Grammars: parsing as a first-class feature

A grammar is like a class whose methods are named regexes (token, rule, regex). The special TOP rule is the entry point, and .parse runs the whole grammar against a string, returning a Match tree:

grammar Calc {
    token TOP { <num> '+' <num> }
    token num { \d+ }
}

my $m = Calc.parse("12+30");
say $m<num>>>.Str.join(" and ");   # 12 and 30

Named tokens become named captures in the match ($m<num>), and >>.Str (a hyper method call) stringifies each capture. For real work you attach an actions class whose methods run as each rule matches, building an abstract syntax tree as you parse. Grammars are how Raku parses Raku itself, and they make writing config formats, DSLs, and data parsers far cleaner than hand-rolled regex soup.

Reference

Finally, we look at how to package and share all of this through the module ecosystem.

Modules, zef, and the Ecosystem

Write a module with export, package it with META6.json, and use zef.

Modules, zef, and the Ecosystem

Raku code is shared as distributions of modules, installed and managed by zef, the standard package manager. This lesson shows how to use other people's modules, write and export your own, and describe a distribution with META6.json.

Using a module

use loads a module and imports whatever it exports:

use JSON::Fast;        # a popular JSON module
my %data = from-json('{"name":"Ada","age":36}');
say %data<name>;       # Ada
say to-json(%data);

Installing with zef

zef installs distributions from the ecosystem and handles dependencies and tests. The common commands:

$ zef install JSON::Fast          # install a distribution
$ zef install .                   # install the distribution in the current dir
$ zef update                      # refresh the package index
$ zef search JSON                 # find distributions by name
$ zef uninstall JSON::Fast
$ zef test .                      # run a distribution's test suite

You can pin an exact identity, for example zef install ROT13:auth<zef:tony-o>:ver<0.0.1>, where auth is the verified author and ver the version. Browse the ecosystem at raku.land, a searchable directory of every published distribution.

Writing a module

A module file uses the .rakumod extension. The unit module declaration names it for the whole file. Mark each routine you want callers to import with the is export trait:

# lib/Greet.rakumod
unit module Greet;

sub greet(Str $name --> Str) is export {
    "Hello, $name!"
}

sub shout($s) is export(:extras) {   # only exported on demand
    $s.uc
}

is export adds the routine to the default export set; tagging it like is export(:extras) puts it in a named set the caller must ask for with use Greet :extras. A file can also be unit class Foo; to make the whole file one class.

Use it from a script by pointing Raku at the lib directory:

# use.raku
use lib 'lib';
use Greet;
say greet("Raku");      # Hello, Raku!
$ raku use.raku
Hello, Raku!

Describing a distribution: META6.json

A distribution is a directory with a META6.json at its root. This metadata tells zef what the distribution provides and depends on, and tells Rakudo which file backs each module name:

{
  "name": "Greet",
  "auth": "zef:yourname",
  "version": "0.0.1",
  "api": "0",
  "provides": {
    "Greet": "lib/Greet.rakumod"
  },
  "depends": [],
  "test-depends": [ "Test" ],
  "description": "A friendly greeting module",
  "license": "Artistic-2.0",
  "tags": [ "greeting", "example" ]
}

Every .rakumod under lib/ should appear in provides, mapping the module's name to its file. Once META6.json exists you can zef test . and zef install . locally.

Standard layout and testing

A conventional distribution looks like this:

Greet/
  META6.json
  lib/
    Greet.rakumod
  t/
    01-basic.rakutest

Tests live in t/ and use the built-in Test module:

use Test;
use Greet;
plan 1;
is greet("Raku"), "Hello, Raku!", "greet works";
done-testing;

Run them with zef test . or raku -Ilib t/01-basic.rakutest.

Publishing

To publish to the ecosystem, authors use fez (the upload client): fez upload packages the current distribution and pushes it to the zef ecosystem, after which it appears on raku.land and is installable by everyone. The helper module App::Mi6 (mi6) scaffolds new distributions, runs tests, and automates releases.

Reference

That completes the Raku track: you can install the toolchain, write typed multi-paradigm code, handle errors and concurrency, define classes, roles, and grammars, and package your work for the ecosystem.