JavaScript (Node.js) to Raku - nutshell
This page attempts to provide a way for users experienced in Node.js to learn Raku. Features shared between the two languages will be explained here, as well as major differences in syntax and features.
This is not a tutorial for learning Raku; this is a reference for users who are already at an intermediate to advanced skill level with Node.js.
Basic syntax
"Hello, world!"
Let's start with the typical first program when learning new languages. In Node.js, a hello world program would be written like this:
console.log('Hello, world!');
Here are a couple ways to write this in the same way in Raku:
say('Hello, world!');
say 'Hello, world!';
Parentheses are optional for function calls in Raku. While semicolons are, for the most part, optional in Node.js, they are mandatory for expressions in Raku.
Now that we've greeted the world, let's greet our good friend, Joe. We'll start with Node.js again:
let name = 'Joe';
console.log('What\'s up,' + name + '?');
console.log(`What's up, ${name}?`);
console.log("What's up, ", name, "?");
Since he didn't hear us, let's greet him again, this time in Raku:
my $name = 'Joe';
say 'What\'s up, ' ~ $name ~ '?';
say "What's up, $name?";
say "What's up, ", $name, "?";
Here, there are only a couple differences: most variables in Raku have what
are called sigils, which are what the $
in front of its name is, and string
concatenation uses the ~
operator instead of +
. What the two languages
share in common here is support for string interpolation.
Now that the basic examples are out of the way, let's explain the similarities between the two languages in greater detail.
Variables
Variables in Node.js can be defined like this;
var foo = 1; // Lexically scoped with functions and modules
let foo = 1; // Lexically scoped with blocks
const foo = 1; // Lexically scoped with blocks; constant
// No equivalent to Raku dynamic variables exists.
global.foo = 1; // Globally scoped
foo = 1; // Ditto, but implicit; forbidden in strict mode
In Raku there is no equivalent to var
. An important note to make is that
there is no variable hoisting in Raku; variables are defined and assigned
at the line they're on, not defined at the top of its scope and later assigned
at that line.
In addition to regular variables, in Raku there are what is known as dynamic variables. Dynamic variables are looked up using the caller's scope, rather than the outer scope. This is what the equivalent variable declarations look like in Raku:
my $foo = 1; # Lexically scoped
our $foo = 1; # Package scoped
my constant foo = 1; # Lexically scoped; constant
constant foo = 1; # Package scoped; constant
my $*foo = 1; # Dynamic variable; lexically scoped
our $*foo = 1; # Dynamic variable; package scoped
GLOBAL::<$foo> := 1; # Globally scoped
Use my
where you'd use let
, our
for variables you'd define in the
outermost scope needed, and constant
where you'd use const
.
You may have noticed the $
and $*
symbols placed before variable names.
These are known as sigils and twigils respectively, and define what container
the variable has. Refer to the documentation on
variables for more information on sigils, twigils, and
containers.
Variables in Node.js can have the same name as others from outer scopes without conflicting (though linters will usually complain about it depending on how they're configured):
let foo = 1;
function logDupe() {
let foo = 2;
console.log(foo);
}
logDupe(2); // OUTPUT: 2
console.log(foo); // OUTPUT: 1
Raku also allows this:
my $foo = 1;
sub log-dupe {
my $foo = 2;
say $foo;
}
log-dupe; # OUTPUT: 2
say $foo; # OUTPUT: 1
Operators
Assignment
The =
operator works the same across both languages.
The :=
operator in Raku binds a value to a variable. Binding a variable
to another variable gives them the same value and container, meaning mutating
attributes of one will mutate the other's as well. Bound variables cannot be
reassigned with =
or mutated with ++
, --
, etc. but they can be bound
to another value again:
my %map; # This is a hash, roughly equivalent to a JS object or map
my %unbound = %map;
my %bound := %map;
%map<foo> = 'bar';
say %unbound; # OUTPUT: {}
say %bound; # OUTPUT: {foo => bar}
%bound := %unbound;
say %bound; # OUTPUT: {}
Equality
Node.js has two equality operators: ==
and ===
.
==
is the loose equality operator. When comparing operands with the same
type, it will return true if both operands are equal. However, if the
operands are different types, they are both cast to their primitives before
being compared, meaning these will return true:
console.log(1 == 1); // OUTPUT: true
console.log('1' == 1); // OUTPUT: true
console.log([] == 0); // OUTPUT: true
Similarly, in Raku, both operands are cast to Numeric before comparison if they don't share the same type:
say 1 == 1; # OUTPUT: True
say '1' == 1; # OUTPUT: True
say [1,2,3] == 3; # OUTPUT: True, since the array has three elements
The inverse of ==
is !=
.
Raku has another operator similar to ==
: eq
. Instead of casting operands
to Numeric if they're different types, eq
will cast them to strings:
say '1' eq '1'; # OUTPUT: True
say 1 eq '1'; # OUTPUT: True
The inverse of eq
is ne
or !eq
.
===
is the strict equality operator. This returns true if both operands are
the same value. When comparing objects, this will only return true if they
are the exact same object:
console.log(1 === 1); // OUTPUT: true
console.log('1' === 1); // OUTPUT: false
console.log({} === {}); // OUTPUT: false
let obj = {};
let obj2 = obj;
console.log(obj === obj2); // OUTPUT: true;
In Raku, the operator behaves the same, with one exception: two objects that have the same value, but different containers, will return false:
say 1 === 1; # OUTPUT: «True»
say '1' === 1; # OUTPUT: «False»
say 'ayy lmao' === 'ayy lmao'; # OUTPUT: «True»
say {} === {}; # OUTPUT: «False»
my \hash = {};
my %hash = hash;
say hash === %hash; # OUTPUT: False
In the last case it's the same object, but containers are different, which is why it returns False.
The inverse of ===
is !==
.
This is where Raku's other equality operators are useful. If the values have
different containers, the eqv
operator can be used. This operator can be also
be used to check for deep equality, which you would normally need to use a
library for in Node.js:
say {a => 1} eqv {a => 1}; # OUTPUT: True
my \hash = {};
my %hash := hash;
say hash eqv %hash; # OUTPUT: True
In the case you need to check if two variables have the same container and
value, use the =:=
operator.
my @arr = [1,2,3];
my @arr2 := @arr; # Bound variables keep the container of the other variable
say @arr =:= @arr2; # OUTPUT: True
Smartmatching
Raku has one last operator for comparing values, but it is not exactly an
equality operator. This is ~~
, the smartmatch operator. This has several
uses: it can be used like instanceof
in Node.js, to match a regex, and to
check if a value is a key in a hash, bag, set, or map:
say 'ayy lmao' ~~ Str; # OUTPUT: True
my %hash = a => 1;
say 'a' ~~ %hash; # OUTPUT: True
my $str = 'abc';
$str ~~ s/abc/def/; # Mutates $str, like foo.replace('abc', 'def')
say $str; # OUTPUT: def
While we are talking about instanceof
, the equivalent to the constructor
property on Node.js objects in Raku is the WHAT
attribute:
console.log('foo'.constructor); // OUTPUT: String
say 'foo'.WHAT; # OUTPUT: Str
Numeric
Node.js has +
, -
, /
, *
, %
, and (in ES6) **
as numeric
operators. When the operands are different types, similarly to the equality
operators, are cast to their primitives before following through with the
operation, making this possible:
console.log(1 + 2); // OUTPUT: 3
console.log([] + {}); // OUTPUT: [object Object]
console.log({} + []); // OUTPUT: 0
In Raku, again, they are converted to a Numeric type, as before:
say 1 + 2; # OUTPUT: 3
say [] + {}; # OUTPUT: 0
say {} + [1,2,3]; # OUTPUT: 3
In addition, Raku has div
and %%
. div
behaves like int
division in
C, while %%
checks if one number is cleanly divisible by another or not:
say 4 div 3; # OUTPUT: 1
say 4 %% 3; # OUTPUT: False
say 6 %% 3; # OUTPUT: True
Bitwise
Node.js has &
, |
, ^
, ~
, <<
, >>
, >>>
, and ~
for bitwise
operators:
console.log(1 << 1); // OUTPUT: 2
console.log(1 >> 1); // OUTPUT: 0
console.log(1 >>> 1); // OUTPUT: 0
console.log(1 & 1); // OUTPUT: 1
console.log(0 | 1); // OUTPUT: 1
console.log(1 ^ 1); // OUTPUT: 0
console.log(~1); // OUTPUT: -2
In Raku, there is no equivalent to >>>
. All bitwise operators are
prefixed with +
, however bitwise negation uses +^
instead of ~
:
say 1 +< 1; # OUTPUT: 2
say 1 +> 1; # OUTPUT: 0
# No equivalent for >>>
say 1 +& 1; # OUTPUT: 1
say 0 +| 1; # OUTPUT: 1
say 1 +^ 1; # OUTPUT: 0
say +^1; # OUTPUT: -2
Checking for definedness
Javascript includes a nullish coalescing operator, ??
, which progresses
only if null or undefined:
undefined || null || 0 || 1 ; // => 1
undefined ?? null ?? 0 ?? 1 ; // => 0
This is very similar to Raku // operator:
Any || Nil || 0 || 1 ; # => 1
Any // Nil // 0 // 1 ; # => 0
Custom operators and operator overloading
Node.js does not allow operator overloading without having to use a Makefile or build Node.js with a custom version of V8. Raku allows custom operators and operator overloading natively! Since all operators are subroutines, you can define your own like so:
# "distance operator": the distance of two numbers is the absolute value
# of their difference
multi infix:<|-|>($a, $b) is equiv(&infix:<->) { abs $a - $b }
say -1 |-| 3; # OUTPUT: 4
Operators can be defined as prefix
, infix
, or postfix
. The
is tighter
, is equiv
, and is looser
traits optionally define the
operator's precedence. In this case, |-|
has the same precedence as -
.
Note how multi
is used when declaring the operator subroutines. This allows
multiple subroutines with the same name to be declared while also having
different signatures. This will be explained in greater detail in the
Functions section. For now, all we need to know is that it allows
us to override any native operator we want:
# Using the `is default` trait here forces this subroutine to be chosen first,
# so long as the signature of the subroutine matches.
multi prefix:<++>($a) is default { $a - 1 }
my $foo = 1;
say ++$foo; # OUTPUT: 0
Control flow
if/else
You should be familiar with how if
/else
looks in JavaScript:
let diceRoll = Math.ceil(Math.random() * 6) + Math.ceil(Math.random() * 6);
if (diceRoll === 2) {
console.log('Snake eyes!');
} else if (diceRoll === 16) {
console.log('Boxcars!');
} else {
console.log(`Rolled ${diceRoll}.`);
}
In Raku, if
/else
works largely the same, with a few key differences.
One, parentheses are not required. Two, else if
is written as elsif
.
Three, the if clause may be written after a statement:
my Int $dice-roll = ceiling rand * 12 + ceiling rand * 12;
if $dice-roll == 2 {
say 'Snake eyes!';
} elsif $dice-roll == 16 {
say 'Boxcars!';
} else {
say "Rolled $dice-roll.";
}
Alternatively, though less efficient, this could be written to use if
after
statements:
my Int $dice-roll = ceiling rand * 12 + ceiling rand * 12;
say 'Snake eyes!' if $dice-roll == 2;
say 'Boxcars!' if $dice-roll == 16;
say "Rolled $dice-roll." if $dice-roll != 2 && $dice-roll != 16;
Raku also has when
, which is like if
, but if the condition given is
true, no code past the when
block within the block it's in is executed:
{
when True {
say 'In when block!'; # OUTPUT: In when block!
}
say 'This will never be output!';
}
Additionally, Raku has with
, orwith
, and without
, which are like
if
, else if
, and else
respectively, but instead of checking whether
their condition is true, they check if it's defined.
switch
Switch statements are a way of checking for equality between a given value and
a list of values and run some code if one matches. case
statements define
each value to compare to. default
, if included, acts as a fallback for when
the given value matches no cases. After matching a case, break
is typically
used to prevent the code from the cases that follow the one matched from being
executed, though rarely this is intentionally omitted.
const ranklist = [2, 3, 4, 5, 6, 7, 8, 9, 'Jack', 'Queen', 'King', 'Ace'];
const ranks = Array.from(Array(3), () => ranklist[Math.floor(Math.random() * ranks.length)]);
let score = 0;
for (let rank of ranks) {
switch (rank) {
case 'Jack':
case 'Queen':
case 'King':
score += 10;
break;
case 'Ace';
score += (score <= 11) ? 10 : 1;
break;
default:
score += rank;
break;
}
}
In Raku, given
can be used like switch statements. There is no equivalent
to break
since when
blocks are most commonly used like case
statements. One major difference between switch
and given
is that a value
passed to a switch
statement will only match cases that are exactly equal to
the value; given
values are smartmatched (~~
) against the when
values.
my @ranklist = [2, 3, 4, 5, 6, 7, 8, 9, 'Jack', 'Queen', 'King', 'Ace'];
my @ranks = @ranklist.pick: 3;
my Int $score = 0;
for @ranks -> $rank {
# The when blocks implicitly return the last statement they contain.
$score += do given $rank {
when 'Jack' | 'Queen' | 'King' { 10 }
when 'Ace' { $score <= 11 ?? 10 !! 1 }
default { $_ }
};
}
If there are multiple when
blocks that match the value passed to given
and you wish to run more than one of them, use proceed
. succeed
may be
used to exit both the when
block it's in and the given block, preventing any
following statements from being executed:
given Int {
when Int { say 'Int is Int'; proceed }
when Numeric { say 'Int is Numeric'; proceed }
when Any { say 'Int is Any'; succeed }
when Mu { say 'Int is Mu' } # Won't output
}
# OUTPUT:
# Int is Int
# Int is Numeric
# Int is Any
for, while, and do/while
There are three different types of for loops in JavaScript:
// C-style for loops
const letters = {};
for (let ord = 0x61; ord <= 0x7A; ord++) {
let letter = String.fromCharCode(ord);
letters[letter] = letter.toUpperCase();
}
// for..in loops (typically used on objects)
for (let letter in letters) {
console.log(letters[letter]);
}
# OUTPUT:
# A
# B
# C
# etc.
// for..of loops (typically used on arrays, maps, and sets)
for (let letter of Object.values(letters)) {
console.log(letter);
}
# OUTPUT:
# A
# B
# C
# etc.
Raku for
loops most closely resemble for..of
loops, since they work on
anything as long as it's iterable. C-style loops are possible to write using
loop
, but this is discouraged since they're better written as for
loops
using ranges. Like if
statements, for
may follow a statement, with the
current iteration being accessible using the $_
variable (known as "it").
Methods on $_
may be called without specifying the variable:
my Str %letters{Str};
%letters{$_} = .uc for 'a'..'z';
.say for %letters.values;
# OUTPUT:
# A
# B
# C
# etc.
while
loops work identically between JavaScript and Raku. Raku also has
until
loops, where instead of iterating until the given condition is false,
they iterate until the condition is true.
do/while
loops are known as repeat/while
loops in Raku. Likewise with
while
, repeat/until
loops also exist and loop until the given condition
is false.
To write infinite loops in Raku, use loop
rather than for
or while
.
In JavaScript, continue
is used to skip to the next iteration in a loop, and
break
is used to exit a loop early:
let primes = new Set();
let i = 2;
do {
let isPrime = true;
for (let prime of primes) {
if (i % prime == 0) {
isPrime = false;
break;
}
}
if (!isPrime) continue;
primes.add(i);
} while (++i < 20);
console.log(primes); # OUTPUT: Set { 2, 3, 5, 7, 11, 13, 17, 19 }
In Raku, these are known as next
and last
respectively. There is also
redo
, which repeats the current iteration without evaluating the loop's
condition again.
next
/redo
/last
statements may be followed by a label defined before an
outer loop to make the statement work on the loop the label refers to, rather
than the loop the statement is in:
my %primes is SetHash;
my Int $i = 2;
OUTSIDE:
repeat {
next OUTSIDE if $i %% $_ for %primes.keys;
%primes{$i}++;
} while ++$i < 20;
say %primes; # OUTPUT: SetHash(11 13 17 19 2 3 5 7)
do
do
is not currently a feature in JavaScript, however a proposal has been made
to add it to ECMAScript.
do
expressions evaluate a block and return the result:
constant VERSION = v2.0.0;
constant VERSION_NUMBER = do {
my @digits = VERSION.Str.comb(/\d+/);
:16(sprintf "%02x%02x%04x", |@digits)
};
say VERSION_NUMBER; # OUTPUT: 33554432
Types
Creating types
In JavaScript, types are created by making a class (or a constructor in ES5 and earlier). If you've used TypeScript, you can define a type as a subset of other types like so:
type ID = string | number;
In Raku, classes, roles, subsets, and enums are considered types. Creating classes and roles will be discussed in the OOP section of this article. Creating an ID subset can be done like so:
subset ID where Str | Int;
See the documentation on subset and Junction for more information.
TypeScript enums may have numbers or strings as their values. Defining the values is optional; by default, the value of the first key is 0, the next key, 1, the next, 2, etc. For example, here is an enum that defines directions for extended ASCII arrow symbols (perhaps for a TUI game):
enum Direction (
UP = '↑',
DOWN = '↓',
LEFT = '←',
RIGHT = '→'
);
Enums in Raku may have any type as their keys' values. Enum keys (and
optionally, values) can be defined by writing enum
, followed by the name
of the enum, then the list of keys (and optionally, values), which can be done
using < >|/language/quoting#Word_quoting:_<_>,
« »|/language/quoting#Word_quoting_with_interpolation_and_quote_protection:_«_»,
or ( ). ( )
must be used if you want to
define values for the enum's keys. Here is the Direction enum as written in
Raku:
enum Direction (
UP => '↑',
DOWN => '↓',
LEFT => '←',
RIGHT => '→'
);
See the documentation on enum for more information.
Using types
In TypeScript, you can define the type of variables. Attempting to assign a value that doesn't match the type of the variable will make the transpiler error out. This is done like so:
enum Name (Phoebe, Daniel, Joe);
let name: string = 'Phoebe';
name = Phoebe; # Causes tsc to error out
let hobbies: [string] = ['origami', 'playing instruments', 'programming'];
let todo: Map<string, boolean> = new Map([
['clean the bathroom', false],
['walk the dog', true],
['wash the dishes', true]
]);
let doJob: (job: string) => boolean = function (job: string): boolean {
todo.set(job, true);
return true;
};
In Raku, variables can be typed by placing the type between the declarator
(my
, our
, etc.) and the variable name. Assigning a value that doesn't
match the variable's type will throw either a compile-time or runtime error,
depending on how the value is evaluated:
enum Name <Phoebe Daniel Joe>;
my Str $name = 'Phoebe';
$name = Phoebe; # Throws a compile-time error
# The type here defines the type of the elements of the array.
my Str @hobbies = ['origami', 'playing instruments', 'programming'];
# The type between the declarator and variable defines the type of the values
# of the hash.
# The type in the curly braces defines the type of the keys of the hash.
my Bool %todo{Str} = (
'clean the bathroom' => False,
'walk the dog' => True,
'wash the dishes' => True
);
# The type here defines the return value of the routine.
my Bool &do-job = sub (Str $job --> Bool) {
%todo{$job} = True;
};
Comparing JavaScript and Raku types
Here is a table of some JavaScript types and their equivalents in Raku:
JavaScript | Raku |
---|---|
Object | Mu, Any, Hash |
Array | List, Array, Seq |
String | Str |
Number | Int, Num, Rat |
Boolean | Bool |
Map | Map, Hash |
Set | Set, SetHash |
Object
is both a superclass of all types in JavaScript and a way to create
a hash. In Raku, Mu is a superclass of all types, though usually
you want to use Any instead, which is a subclass of Mu but also
a superclass of nearly every type, with Junction being an
exception. When using Object
as a hash, Hash is what you want
to use. One key difference between Object
and Hash is that Object
preserves the order of its keys; Hash does not by default.
There are three types equivalent to Array. Array is most similar to Array, since it acts as a mutable array. List is similar to Array, but is immutable. Seq is used to create lazy arrays.
String
and Str are for the most part used identically.
There are several different types in Raku equivalent to Number
, but the
three you'll most commonly see are Int, Num, and
Rat. Int represents an integer. Num represents a
floating-point number, making it the most similar to Number
. Rat
represents a fraction of two numbers, and is used when Num cannot provide
precise enough values.
Boolean
and Bool are for the most part used identically.
Map
has both a mutable and an immutable equivalent in Raku.
Map is the immutable one, and Hash is the mutable
one. Don't get them mixed up! Like Map
in JavaScript, Map and Hash can
have any type of key or value, not just strings for keys.
Like Map
, Set
also has both a mutable and an immutable equivalent in Raku.
Set is the immutable one, and SetHash is the
mutable one.
Functions
# TBD
Object-oriented programming
# TBD
Asynchronous programming
# TBD
Buffers
NodeJS handles raw binary data with the classes Buffer
and Blob
, while Raku
does so with the roles Buf and Blob, which are
mutable and immutable buffers respectively. In Raku, a `Buf` composes a `Blob`
so all `Blob` methods are available to `Buf` objects.
The following table summarizes the similarities and differences between buffer constructs in NodeJS and Raku:
Class/Role | NodeJS | Raku | |
---|---|---|
`Buffer`/`Buf` | Fixed-length sequence of bytes (No methods such as `push`, `pop`, etc.) | Sequence of bytes that can grow or shrink dynamically. You can use methods such as `push`, `pop`, etc. |
Iterable using the `for..of` syntax | It can be iterated over using a looping construct. | |
Each byte can be updated using array indexing, e.g., `buf[i]++`. | Same as NodeJS. | |
`Blob` | Fixed-length sequence of bytes (No methods such as `push`, `pop`, etc.) | Same as NodeJS. |
It's not iterable. | It can be iterated over using a looping construct. | |
Each byte is immutable. | Same as NodeJS. |
Creating buffers
In NodeJS, there are a few ways to create a new buffer. You can use the static
method Buffer.alloc
to allocate a buffer of n
bytes of zero, unless the
fill
argument is provided.
const zeroBuf = Buffer.alloc(8);
const charBuf = Buffer.alloc(8, 97, 'utf-8');
console.log(zeroBuf); // OUTPUT: «<Buffer 00 00 00 00 00 00 00 00>»
console.log(charBuf); // OUTPUT: «<Buffer 61 61 61 61 61 61 61 61>»
In Raku, you can use the allocate method:
my $zero-blob = Blob.allocate(8);
my $char-blob = Blob.allocate(8, 97);
say $zero-blob; # OUTPUT: «Blob:0x<00 00 00 00 00 00 00 00>»
say $char-blob; # OUTPUT: «Blob:0x<61 61 61 61 61 61 61 61>»
my $zero-buf = Buf.allocate(8);
my $char-buf = Buf.allocate(8, 97);
say $zero-buf; # OUTPUT: «Buf:0x<00 00 00 00 00 00 00 00>»
say $char-buf; # OUTPUT: «Buf:0x<61 61 61 61 61 61 61 61>»
You can also initialize a buffer to the contents of an array of integers:
const buf = Buffer.from([ 114, 97, 107, 117 ]);
console.log(buf); // OUTPUT: «<Buffer 72 61 6b 75>»
In Raku, you can do the same by using the new constructor:
my $blob = Blob.new(114, 97, 107, 117);
say $blob; # OUTPUT: «Blob:0x<72 61 6B 75>»
my $buf = Buf.new(114, 97, 107, 117);
say $buf; # OUTPUT: «Buf:0x<72 61 6B 75>»
Similarly, you can initialize a buffer to the binary encoding of a string using
the from
method:
const buf = Buffer.from('NodeJS & Raku', 'utf-8');
console.log(buf); // OUTPUT: «<Buffer 4e 6f 64 65 4a 53 20 26 20 52 61 6b 75>»
In Raku, you call the encode method on a string which returns a Blob:
my $blob = "NodeJS & Raku".encode('utf-8');
say $blob; # OUTPUT: «utf8:0x<4E 6F 64 65 4A 53 20 26 20 52 61 6B 75>»
Note: In Raku, you must encode a character explicitly when passing its blob to a buffer-related method.
To decode a binary encoding of a string, you call the toString
method on the
buffer:
const buf = Buffer.from([ 114, 97, 107, 117 ]);
console.log(buf.toString('utf-8')); // OUTPUT: «raku»
In Raku, you call the decode method on the buffer:
my $blob = Blob.new(114, 97, 107, 117);
say $blob.decode('utf-8'); # OUTPUT: «raku»
Writing to a buffer
In NodeJS, you write to a buffer using the write
method:
const buf = Buffer.alloc(16);
buf.write('Hello', 0, 'utf-8');
console.log(buf); // OUTPUT: «<Buffer 48 65 6c 6c 6f 00 00 00 00 00 00 00 00 00 00 00>»
buf.write(' world!', 5, 'utf-8');
console.log(buf); // OUTPUT: «<Buffer 48 65 6c 6c 6f 20 77 6f 72 6c 64 21 00 00 00 00>»
In Raku, there's not a write
method. However you can use the
splice method to overwrite elements of a buffer
with other elements:
my $buf = Buf.allocate(16);
$buf.splice(0, 5, 'Hello'.encode('utf-8'));
say $buf; # OUTPUT: «Buf:0x<48 65 6C 6C 6F 00 00 00 00 00 00 00 00 00 00 00>»
$buf.splice(5, 7, ' world!'.encode('utf-8'));
say $buf; # OUTPUT: «Buf:0x<48 65 6C 6C 6F 20 77 6F 72 6C 64 21 00 00 00 00>»
Reading from a buffer
There are many ways to access data in a buffer, from accessing individual bytes to extracting the entire content to decoding its contents.
const buf = Buffer.from('Hello', 'utf-8');
console.log(buf[0]); // OUTPUT: «72»
In Raku, you can also index bytes of a buffer with []:
my $blob = 'Hello'.encode('utf-8');
say $blob[0]; # OUTPUT: «72»
In NodeJS the most common way to retrieve all data from a buffer is with the
toString
method (assuming the buffer is encoded as text):
const buf = Buffer.from('Hello');
const buf = Buffer.alloc(16);
buf.write('Hello world', 0, 'utf-8');
console.log(buf.toString('utf-8')); // OUTPUT: «Hello world!\u0000t»
We can provide an offset and a length to toString
to only read the relevant
bytes from the buffer:
console.log(buf.toString('utf-8', 0, 12)); // OUTPUT: «Hello world!»
In Raku, you can do the same using the decode
method:
my $buf = Buf.allocate(16);
$buf.splice(0, 12, 'Hello world'.encode('utf-8'));;
say $buf.decode('utf-8').raku; # OUTPUT: «Hello world!\0\0\0\0>»
However, you cannot both slice and decode a buffer with decode
. Instead
you can use subbuf to extract the relevant
part from the invocant buffer and then decode
the returned buffer:
say $buf.subbuf(0, 12).decode('utf-8').raku; # OUTPUT: «Hello world!>»
More useful methods
Buffer.isBuffer
In NodeJS, you can check if an object is a buffer using the isBuffer
method:
const buf = Buffer.from('hello');
console.log(Buffer.isBuffer(buf)); // OUTPUT: «true»
In Raku, you can smartmatch against either Blob or Buf (remember that Buf composes Blob):
my $blob = 'hello'.encode();
my $buf = Buf.allocate(4);
say $blob ~~ Blob; # OUTPUT: «True»
say $blob ~~ Buf; # OUTPUT: «False»
say $buf ~~ Buf; # OUTPUT: «True»
say $buf ~~ Blob; # OUTPUT: «True»
Buffer.byteLength
To check the number of bytes required to encode a string, you can use
Buffer.byteLength
:
const camelia = '🦋';
console.log(Buffer.byteLength(camelia)); // OUTPUT: «4»
In Raku, you can use the bytes method:
my $camelia = '🦋';
say $camelia.encode.bytes; # OUTPUT: «4»
NOTE: The number of bytes isn't the same as the string's length. This is because many characters require more bytes to be encoded than what their lengths let on.
length
In NodeJS, you use the length
method to determine how much memory is
allocated by a buffer. This is not the same as the size of the buffer's
contents.
const buf = Buffer.alloc(16);
buf.write('🦋');
console.log(buf.length); // OUTPUT: «16»
In Raku, you can use the elems method:
my $buf = Buf.allocate(16);
$buf.splice(0, '🦋'.encode.bytes, '🦋'.encode('utf-8'));
say $buf.elems; # OUTPUT: «16»
copy
You use the copy
method to copy the contents of one buffer onto another.
const target = Buffer.alloc(24);
const source = Buffer.from('🦋', 'utf-8');
target.write('Happy birthday! ', 'utf-8');
source.copy(target, 16);
console.log(target.toString('utf-8', 0, 20)); // OUTPUT: «Happy birthday! 🦋»
There's no copy
method in Raku, however you can use the splice
method for
the same result:
my $target = Buf.allocate(24);
my $encoded-string = 'Happy birthday! '.encode('utf-8');
$target.splice(0, $encoded-string.bytes, $encoded-string);
my $source = '🦋'.encode('utf-8');
$target.splice(16, $source.bytes, $source);
say $target.subbuf(0, 20).decode('utf-8'); # OUTPUT: «Happy birthday! 🦋»
slice
You can slice a subset of a buffer using the slice
method, which returns a
reference to the subset of the memory space. Thus modifying the slice will also
modify the original buffer.
// setup
const target = Buffer.alloc(24);
const source = Buffer.from('🦋', 'utf-8');
target.write('Happy birthday! ', 'utf-8');
source.copy(target, 16);
// slicing off buffer
const animal = target.slice(16, 20);
animal.write('🐪');
console.log(animal.toString('utf-8'); // OUTPUT: «🐪»
console.log(target.toString('utf-8', 0, 20)); // OUTPUT: «Happy birthday! 🐪»
Here we sliced off target
and stored the resulting buffer in animal
, which
we ultimately modified. This resulted on target
being modified.
In Raku, you can use the subbuf method:
# setup
my $target = Buf.allocate(24);
my $encoded-string = 'Happy birthday! '.encode('utf-8');
$target.splice(0, $encoded-string.bytes, $encoded-string);
my $source = '🦋'.encode('utf-8');
$target.splice(16, $source.bytes, $source);
# slicing off buffer
my $animal = $target.subbuf(16, 20);
$animal.splice(0, $animal.bytes, '🐪'.encode('utf-8'));
say $animal.decode; # OUTPUT: «🐪»
say $target.subbuf(0, 20).decode('utf-8'); # OUTPUT: «Happy birthday! 🦋»
However, unlike NodeJS's slice
method, subbuf
returns a brand new buffer.
To get a hold of a writable reference to a subset of a buffer, use
subbuf-rw:
# setup
my $target = Buf.allocate(24);
my $encoded-string = 'Happy birthday! '.encode('utf-8');
$target.splice(0, $encoded-string.bytes, $encoded-string);
my $source = '🦋'.encode('utf-8');
$target.splice(16, $source.bytes, $source);
# slicing off buffer
$target.subbuf-rw(16, 4) = '🐪'.encode('utf-8');
say $target.subbuf(0, 20).decode('utf-8'); # OUTPUT: «Happy birthday! 🐪»
The networking API
Net
In Raku, there are two APIs for dealing with networking: IO::Socket::INET (for synchronous networking), and IO::Socket::Async (for asynchronous networking).
IO::Socket::INET currently only supports TCP connections. Its API resembles that of C's socket API. If you're familiar with that, then it won't take long to understand how to use it. For example, here's an echo server that closes the connection after receiving its first message:
my IO::Socket::INET $server .= new:
:localhost<localhost>,
:localport<8000>,
:listen;
my IO::Socket::INET $client .= new: :host<localhost>, :port<8000>;
$client.print: 'Hello, world!';
my IO::Socket::INET $conn = $server.accept;
my Str $msg = $conn.recv;
say $msg; # OUTPUT: Hello, world!
$conn.print($msg);
say $client.recv; # OUTPUT: Hello, world!
$conn.close;
$client.close;
$server.close;
By default, IO::Socket::INET connections are IPv4 only. To use IPv6 instead,
pass :family(PF_INET6)
when constructing a server or a client.
In contrast, IO::Socket::Async supports both IPv4 and IPv6 without the need
to specify which family you wish to use. It also supports UDP sockets. Here's
how you would write the same echo server as above asynchronously (note that
Supply.tap
is multithreaded; if this is undesirable, use Supply.act
instead:
my $supply = IO::Socket::Async.listen('localhost', 8000);
my $server = $supply.tap(-> $conn {
$conn.Supply.tap(-> $data {
say $data; # OUTPUT: Hello, world!
await $conn.print: $data;
$conn.close;
})
});
my $client = await IO::Socket::Async.connect('localhost', 8000);
$client.Supply.tap(-> $data {
say $data; # OUTPUT: Hello, world!
$client.close;
$server.close;
});
await $client.print: 'Hello, world!';
The equivalent code in Node.js looks like this:
const net = require('net');
const server = net.createServer(conn => {
conn.setEncoding('utf8');
conn.on('data', data => {
console.log(data); # OUTPUT: Hello, world!
conn.write(data);
conn.end();
});
}).listen(8000, 'localhost');
const client = net.createConnection(8000, 'localhost', () => {
client.setEncoding('utf8');
client.on('data', data => {
console.log(data); # OUTPUT: Hello, world!
client.end();
server.close();
});
client.write("Hello, world!");
});
HTTP/HTTPS
Raku doesn't natively support HTTP/HTTPS. However, CPAN packages such as Cro help fill the gap.
DNS
Raku does not currently support the majority of the features that Node.js's DNS module implements. IO::Socket::INET and IO::Socket::Async can resolve hostnames, but features like resolving DNS records and reverse IP lookups are not implemented yet. There are some modules that are a work in progress, such as Net::DNS::BIND::Manage, that aim to improve DNS support.
Punycode
Punycode support is available through the Net::LibIDN, Net::LibIDN2, and IDNA::Punycode modules on CPAN.
The filesystem API
# TBD
Modules and packages
# TBD