Numeric types available in Raku
Int
The Int
type offers arbitrary-size integer numbers. They can get as big as your computer memory allows, although some implementations choose to throw a numeric overflow error when asked to produce integers of truly staggering size:
say 10**600**600# OUTPUT: «Numeric overflow»
Unlike some languages, division performed using /
operator when both operands are of Int type, would produce a fractional number, without any rounding performed.
say 4/5; # OUTPUT: «0.8»
The type produced by this division is either a Rat or a Num type. The Rat is produced if, after reduction, the fraction's denominator is smaller than 64 bits, otherwise a Num type is produced.
The div and narrow routines can be helpful if you wish to end up with an Int result, whenever possible. The div operator performs integer division, discarding the remainder, while narrow fits the number into the narrowest type it'll fit:
say 5 div 2; # OUTPUT: «2»# Result `2` is narrow enough to be an Int:say (4/2).narrow; # OUTPUT: «2»say (4/2).narrow.^name; # OUTPUT: «Int»# But 2.5 has fractional part, so it ends up being a Rat type:say (5/2).narrow.^name; # OUTPUT: «Rat»say (5/2).narrow; # OUTPUT: «2.5»# Denominator is too big for a Rat, so a Num is produced:say 1 / 10⁹⁹; # OUTPUT: «1e-99»
Raku has a FatRat type that offers arbitrary precision fractions. How come a limited-precision Num is produced instead of a FatRat type in the last example above? The reason is: performance. Most operations are fine with a little bit of precision lost and so do not require the use of a more expensive FatRat type. You'll need to instantiate one yourself if you wish to have the extra precision.
Num
The Num type offers double-precision floating-point decimal numbers, sometimes called "doubles" in other languages.
A Num literal is written with the exponent separated using the letter e
. Keep in mind that the letter e
is required even if the exponent is zero, as otherwise you'll get a Rat rational literal instead:
say 42e0.^name; # OUTPUT: «Num»say 42.0.^name; # OUTPUT: «Rat»
Case-sensitive words Inf and NaN represent the special values infinity and not-a-number respectively. The U+221E INFINITY (∞
) character can be used instead of Inf:
Raku follows the IEEE 754-2008 Standard for Floating-Point Arithmetic as much as possible, with more conformance planned to be implemented in later language versions. The language guarantees the closest representable number is chosen for any given Num literal and does offer support for negative zero and denormals (also known as "subnormals").
Keep in mind that output routines like say or put do not try very hard to distinguish between how Numeric types are output and may choose to display a Num as an Int or a Rat number. For a more definitive string to output, use the raku method:
say 1e0; # OUTPUT: «1»say .5e0; # OUTPUT: «0.5»say 1e0.raku; # OUTPUT: «1e0»say .5e0.raku; # OUTPUT: «0.5e0»
Complex
The Complex type numerics of the complex plane. The Complex objects consist of two Num objects representing the real and imaginary portions of the complex number.
To create a Complex, you can use the postfix i
operator on any other non-complex number, optionally setting the real part with addition. To use the i
operator on NaN
or Inf
literals, separate it from them with a backslash.
say 42i; # OUTPUT: «0+42i»say 73+42i; # OUTPUT: «73+42i»say 73+Inf\i; # OUTPUT: «73+Inf\i»
Keep in mind the above syntax is just an addition expression and precedence rules apply. It also cannot be used in places that forbid expressions, such as literals in routine parameters.
# Precedence of `*` is higher than that of `+`say 2 * 73+10i; # OUTPUT: «146+10i»
To avoid these issues, you can choose to use the Complex literal syntax instead, which involves surrounding the real and imaginary parts with angle brackets, without any spaces:
say 2 * <73+10i>; # OUTPUT: «146+20i»multi how-is-it (<2+4i>)multi how-is-it (|)how-is-it 2+4i; # OUTPUT: «that's my favorite number!»how-is-it 3+2i; # OUTPUT: «meh»
Rational
The types that do the Rational role offer high-precision and arbitrary-precision decimal numbers. Since the higher the precision the larger the performance penalty, the Rational types come in two flavors: Rat and FatRat. The Rat is the most often-used variant that degrades into a Num in most cases, when it can no longer hold all of the requested precision. The FatRat is the arbitrary-precision variant that keeps growing to provide all of the requested precision.
Rat
The most common of Rational types. It supports rationals with denominators as large as 64 bits (after reduction of the fraction to the lowest denominator). Rat
objects with larger denominators can be created directly, however, when Rat
s with such denominators are the result of mathematical operations, they degrade to a Num object.
The Rat literals use syntax similar to Num literals in many other languages, using the dot to indicate the number is a decimal:
say .1 + .2 == .3; # OUTPUT: «True»
If you try to execute a statement similar to the above in many common languages, you'll get False
as the answer, due to imprecision of floating point math. To get the same result in Raku, you'd have to use Num literals instead:
say .1e0 + .2e0 == .3e0; # OUTPUT: «False»
You can also use /
operator with Int or Rat objects to produce a Rat:
say 3/4; # OUTPUT: «0.75»say 3/4.2; # OUTPUT: «0.714286»say 1.1/4.2; # OUTPUT: «0.261905»
Keep in mind the above syntax is just a division expression and precedence rules apply. It also cannot be used in places that forbid expressions, such as literals in routine parameters.
# Precedence of power operators is higher than divisionsay 3/2²; # OUTPUT: «0.75»
To avoid these issues, you can choose to use the Rational literal syntax instead, which involves surrounding the numerator and denominator with angle brackets, without any spaces:
say <3/2>²; # OUTPUT: «2.25»multi how-is-it (<3/2>)multi how-is-it (|)how-is-it 3/2; # OUTPUT: «that's my favorite number!»how-is-it 1/3; # OUTPUT: «meh»
Lastly, any Unicode character with property No
that represents a fractional number can be used as a Rat literal:
say ½ + ⅓ + ⅝ + ⅙; # OUTPUT: «1.625»
Num
If a mathematical operation that produces a Rat answer would produce a Rat with denominator larger than 64 bits, that operation would instead return a Num object. When constructing a Rat (i.e. when it is not a result of some mathematical expression), however, a larger denominator can be used:
my = 1 / (2⁶⁴ - 1);say ; # OUTPUT: «0.000000000000000000054»say .^name; # OUTPUT: «Rat»say .nude; # OUTPUT: «(1 18446744073709551615)»my = 1 / 2⁶⁴;say ; # OUTPUT: «5.421010862427522e-20»say .^name; # OUTPUT: «Num»my = Rat.new(1, 2⁶⁴);say ; # OUTPUT: «0.000000000000000000054»say .^name; # OUTPUT: «Rat»say .nude; # OUTPUT: «(1 18446744073709551616)»say .Num; # OUTPUT: «5.421010862427522e-20»
FatRat
The last Rational type—FatRat—keeps all of the precision you ask of it, storing the numerator and denominator as two Int objects. A FatRat is more infectious than a Rat, so many math operations with a FatRat will produce another FatRat, preserving all of the available precision. Where a Rat degrades to a Num, math with a FatRat keeps chugging along:
say ((42 + Rat.new(1,2))/999999999999999999).^name; # OUTPUT: «Rat»say ((42 + Rat.new(1,2))/9999999999999999999).^name; # OUTPUT: «Num»say ((42 + FatRat.new(1,2))/999999999999999999).^name; # OUTPUT: «FatRat»say ((42 + FatRat.new(1,2))/99999999999999999999999).^name; # OUTPUT: «FatRat»
There's no special operator or syntax available for construction of FatRat objects. Simply use the FatRat.new
method, giving numerator as first positional argument and denominator as the second.
If your program requires a significant amount of FatRat creation, you could create your own custom operator:
sub infix:<🙼>say (1🙼3).raku; # OUTPUT: «FatRat.new(1, 3)»
Keep in mind that output routines like say or put do not try very hard to distinguish between how Numeric types are output and may choose to display a Num as an Int or a Rat number. For a more definitive string to output, use the raku method:
say 1.0; # OUTPUT: «1»say ⅓; # OUTPUT: «0.333333»say 1.0.raku; # OUTPUT: «1.0»say ⅓.raku; # OUTPUT: «<1/3>»
For even more information, you may choose to see the Rational object in the nude, displaying its numerator and denominator:
say ⅓; # OUTPUT: «0.333333»say 4/2; # OUTPUT: «2»say ⅓.raku; # OUTPUT: «<1/3>»say <4/2>.nude; # OUTPUT: «(2 1)»
In many languages division by zero is an immediate exception. In Raku, what happens depends on what you're dividing and how you use the result.
Raku follows IEEE 754-2008 Standard for Floating-Point Arithmetic, but for historical reasons 6.c and 6.d language versions do not comply fully. Num division by zero produces a Failure, while Complex division by zero produces NaN
components, regardless of what the numerator is.
As of 6.e language, both Num and Complex division by zero will produce a -Inf, +Inf
, or NaN depending on whether the numerator was negative, positive, or zero, respectively (for Complex the real and imaginary components are Num and are considered separately).
Division of Int numerics produces a Rat object (or a Num, if after reduction the denominator is larger than 64-bits, which isn't the case when you're dividing by zero). This means such division never produces an Exception or a Failure. The result is a Zero-Denominator Rational, which can be explosive.
A Zero-Denominator Rational is a numeric that does role Rational, which among core numerics would be Rat and FatRat objects, which has denominator of zero. The numerator of such Rationals is normalized to -1
, 0
, or 1
depending on whether the original numerator is negative, zero or positive, respectively.
Operations that can be performed without requiring actual division to occur are non-explosive. For example, you can separately examine numerator and denominator in the nude or perform mathematical operations without any exceptions or failures popping up.
Converting zero-denominator rationals to Num follows the IEEE 754-2008 Standard for Floating-Point Arithmetic conventions, and the result is a -Inf
, Inf
, or NaN
, depending on whether the numerator is negative, positive, or zero, respectively. The same is true going the other way: converting ±Inf
/NaN
to one of the Rational types will produce a zero-denominator rational with an appropriate numerator:
say <1/0>.Num; # OUTPUT: «Inf»say <-1/0>.Num; # OUTPUT: «-Inf»say <0/0>.Num; # OUTPUT: «NaN»say Inf.Rat.nude; # OUTPUT: «(1 0)»
All other operations that require non-IEEE 754-2008 Standard for Floating-Point Arithmetic division of the numerator and denominator to occur will result in X::Numeric::DivideByZero
exception to be thrown. The most common of such operations would likely be trying to print or stringify a zero-denominator rational:
say 0/0;# OUTPUT:# Attempt to divide by zero using div# in block <unit> at -e line 1
Allomorphs are subclasses of two types that can behave as either of them. For example, the allomorph IntStr is the subclass of Int and Str types and will be accepted by any type constraint that requires an Int or Str object.
Allomorphs can be created using angle brackets, either used standalone or as part of a hash key lookup; directly using method .new
and are also provided by some constructs such as parameters of sub MAIN
.
say <42>.^name; # OUTPUT: «IntStr»say <42e0>.^name; # OUTPUT: «NumStr»say < 42+42i>.^name; # OUTPUT: «ComplexStr»say < 1/2>.^name; # OUTPUT: «RatStr»say <0.5>.^name; # OUTPUT: «RatStr»= "42";sub MAIN() # OUTPUT: «IntStr»say IntStr.new(42, "42").^name; # OUTPUT: «IntStr»
A couple of constructs above have a space after the opening angle bracket. That space isn't accidental. Numerics that are often written using an operator, such as 1/2
(Rat, division operator) and 1+2i
(Complex, addition) can be written as a literal that doesn't involve the use of an operator: angle brackets without any spaces between the angle brackets and the characters inside. By adding spaces within the angle brackets, we tell the compiler that not only we want a Rat or Complex literal, but we also want it to be an allomorph: the RatStr or ComplexStr, in this case.
If the numeric literal doesn't use any operators, then writing it inside the angle brackets, even without including any spaces within, would produce the allomorph. (Logic: if you didn't want the allomorph, you wouldn't use the angle brackets. The same isn't true for operator-using numbers as some constructs, such as signature literals, do not let you use operators, so you can't just omit angle brackets for such numeric literals).
The core language offers the following allomorphs:
Type | Allomorph of | Example |
---|---|---|
IntStr | Int and Str | <42> |
NumStr | Num and Str | <42e0> |
ComplexStr | Complex and Str | < 1+2i> |
RatStr | Rat and Str | <1.5> |
Note: there is no FatRatStr
type.
Keep in mind that allomorphs are simply subclasses of the types they represent. Just as a variable or parameter type-constrained to Foo
can accept any subclass of Foo
, so will a variable or parameter type-constrained to Int will accept an IntStr allomorph:
sub foo(Int )foo <42>; # OUTPUT: «IntStr»my Num = <42e0>;say .^name; # OUTPUT: «NumStr»
This also applies to parameter coercers:
sub foo(Int(Cool) )foo <42>; # OUTPUT: «IntStr»
The given allomorph is already an object of type Int, so it does not get converted to a "plain" Int in this case.
The power of allomorphs would be severely diminished if there were no way to "collapse" them to one of their components. Thus, if you explicitly call a method with the name of the type to coerce to, you'll get just that component. The same applies to any proxy methods, such as calling method .Numeric
instead of .Int
or using the prefix:<~>
operator instead of .Str
method call.
my := IntStr.new: 42, "forty two";say .Str; # OUTPUT: «forty two»say +; # OUTPUT: «42»say <1/99999999999999999999>.Rat.^name; # OUTPUT: «Rat»say <1/99999999999999999999>.FatRat.^name; # OUTPUT: «FatRat»
A handy way to coerce a whole list of allomorphs is by applying the hyper operator to the appropriate prefix:
say map *.^name, <42 50e0 100>; # OUTPUT: «(IntStr NumStr IntStr)»say map *.^name, +«<42 50e0 100>; # OUTPUT: «(Int Num Int)»say map *.^name, ~«<42 50e0 100>; # OUTPUT: «(Str Str Str)»
The above discussion on coercing allomorphs becomes more important when we consider object identity. Some constructs utilize it to ascertain whether two objects are "the same". And while to humans an allomorphic 42
and regular 42
might appear "the same", to those constructs, they're entirely different objects:
# "42" shows up twice in the result: 42 and <42> are different objects:say unique 1, 1, 1, 42, <42>; # OUTPUT: «(1 42 42)»# Use a different operator to `unique` with:say unique :with(&[==]), 1, 1, 1, 42, <42>; # OUTPUT: «(1 42)»# Or coerce the input instead (faster than using a different `unique` operator):say unique :as(*.Int), 1, 1, 1, 42, <42>; # OUTPUT: «(1 42)»say unique +«(1, 1, 1, 42, <42>); # OUTPUT: «(1 42)»# Parameterized Hash with `Any` keys does not stringify them; our key is of type `Int`:my = 42 => "foo";# But we use the allomorphic key of type `IntStr`, which is not in the Hash:say <42>:exists; # OUTPUT: «False»# Must use curly braces to avoid the allomorph:say :exists; # OUTPUT: «True»# We are using a set operator to look up an `Int` object in a list of `IntStr` objects:say 42 ∈ <42 100 200>; # OUTPUT: «False»# Convert it to an allomorph:say <42> ∈ <42 100 200>; # OUTPUT: «True»# Or convert the items in the list to plain `Int` objects:say 42 ∈ +«<42 100 200>; # OUTPUT: «True»
Be mindful of these object identity differences and coerce your allomorphs as needed.
As the name suggests, native numerics offer access to native numerics—i.e. those offered directly by your hardware. This in turn offers two features: overflow/underflow and better performance.
NOTE: at the time of this writing (2018.05), certain implementations (such as Rakudo) offer somewhat spotty details on native types, such as whether int64
is available and is of 64-bit size on 32-bit machines, and how to detect when your program is running on such hardware.
Native type | Base numeric | Size |
---|---|---|
int | integer | 64-bits |
int8 | integer | 8-bits |
int16 | integer | 16-bits |
int32 | integer | 32-bits |
int64 | integer | 64-bits |
uint | unsigned integer | 64-bits |
uint8 | unsigned integer | 8-bits |
uint16 | unsigned integer | 16-bits |
uint32 | unsigned integer | 32-bits |
uint64 | unsigned integer | 64-bits |
num | floating point | 64-bits |
num32 | floating point | 32-bits |
num64 | floating point | 64-bits |
atomicint | integer | sized to offer CPU-provided atomic operations. (typically 64 bits on 64-bit platforms and 32 bits on 32-bit ones) |
To create a natively-typed variable or parameter, simply use the name of one of the available numerics as the type constraint:
my int32 = 42;sub foo(num )class
At times, you may wish to coerce some value to a native type without creating any usable variables. There are no .int
or similar coercion methods (method calls are latebound, so they're not well-suited for this purpose). Instead, simply use an anonymous variable:
some-native-taking-sub( (my int $ = ), (my int32 $ = ) )
Trying to assign a value that does not fit into a particular native type, produces an exception. This includes attempting to give too large an argument to a native parameter:
my int = 2¹⁰⁰;# OUTPUT:# Cannot unbox 101 bit wide bigint into native integer# in block <unit> at -e line 1sub f(int ) ; say f 2⁶⁴# OUTPUT:# Cannot unbox 65 bit wide bigint into native integer# in sub f at -e line 1# in block <unit> at -e line 1
However, modifying an already-existing value in such a way that it becomes too big/small, produces overflow/underflow behavior:
my int = 2⁶³-1;say ; # OUTPUT: «9223372036854775807»say ++; # OUTPUT: «-9223372036854775808»my uint8 ;say ; # OUTPUT: «0»say -= 100; # OUTPUT: «156»
Creating objects that utilize native types does not involve direct assignment by the programmer; that is why these constructs offer overflow/underflow behavior instead of throwing exceptions.
say Buf.new(1000, 2000, 3000).List; # OUTPUT: «(232 208 184)»say my uint8 = 1000, 2000, 3000; # OUTPUT: «232 208 184»
While they can be referred to as "native types", native numerics are not actually classes that have any sort of methods available. However, you can call any of the methods available on non-native versions of these numerics. What's going on?
my int8 = -42;say .abs; # OUTPUT: «42»
This behavior is known as "auto-boxing". The compiler automatically "boxes" the native type into a full-featured higher-level type with all the methods. In other words, the int8
above was automatically converted to an Int and it's the Int class that then provided the abs method that was called.
This detail is significant when you're using native types for performance gains. If the code you're using results in a lot of auto-boxing being performed you might get worse performance with native types than you would with non-natives:
my = -42;my int = -42;# OUTPUT: «0.38180862»# OUTPUT: «0.938720»
As you can see above, the native variant is more than twice slower. The reason is the method call requires the native type to be boxed, while no such thing is needed in the non-native variant, hence the performance loss.
In this particular case, we can simply switch to a subroutine form of abs, which can work with native types without boxing them. In other cases, you may need to seek out other solutions to avoid excessive autoboxing, including switching to non-native types for a portion of the code.
my = -42;my int = -42;# OUTPUT: «0.38229177»# OUTPUT: «0.3088305»
Since there are no classes behind native types, there are no type objects you'd normally get with variables that haven't been initialized. Thus, native types are automatically initialized to zero. In 6.c language, native floating point types (num
, num32
, and num64
) were initialized to value NaN
; in 6.d language the default is 0e0
.
It is possible to have native candidates alongside non-native candidates to, for example, offer faster algorithms with native candidates when sizes are predictable, but to fallback to slower non-native alternatives otherwise. The following are the rules concerning multi-dispatch involving native candidates.
First, the size of the native type does not play a role in dispatch and an int8
is considered to be the same as int16
or int
:
multi foo(int )multi foo(int32 )foo my int = 42;# OUTPUT:# Ambiguous call to 'foo(Int)'; these signatures all match:# :(int $x)# :(int32 $x)
Second, if a routine is an only
—i.e. it is not a multi
—that takes a non-native type but a native one was given during the call, or vice-versa, then the argument will be auto-boxed or auto-unboxed to make the call possible. If the given argument is too large to fit into the native parameter, an exception will be thrown:
-> int ( 42 ); # OK; auto-unboxing-> int ( 2¹⁰⁰ ); # Too large; exception-> Int ( 2¹⁰⁰ ); # OK; non-native parameter-> Int ( my int $ = 42 ); # OK; auto-boxing
When it comes to multi
routines, native arguments will always be auto-boxed if no native candidates are available to take them:
multi foo (Int )say foo my int $ = 42; # OUTPUT: «42»
The same luxury is not afforded when going the other way. If only a native candidate is available, a non-native argument will not be auto-unboxed and instead an exception indicating no candidates matched will be thrown (the reason for this asymmetry is a native type can always be boxed, but a non-native may be too large to fit into a native):
multi f(int )my = 2;say f ;# OUTPUT:# Cannot resolve caller f(Int); none of these signatures match:# (int $x)# in block <unit> at -e line 1
However, this rule is waived if a call is being made where one of the arguments is a native type and another one is a numeric literal:
multi f(int, int)f 42, my int ; # Successful call
This way you do not have to constantly write, for example, $n +> 2
as $n +> (my int $ = 2)
. The compiler knows the literal is small enough to fit to a native type and converts it to a native.
The language offers some operations that are guaranteed to be performed atomically, i.e. safe to be executed by multiple threads without the need for locking with no risk of data races.
For such operations, the some operations native type is required. This type is similar to a plain native int, except it is sized such that CPU-provided atomic operations can be performed upon it. On a 32-bit CPU it will typically be 32 bits in size, and on an a 64-bit CPU it will typically be 64 bits in size.
# !!WRONG!! Might be non-atomic on some systemsmy int ;await ^100 .map: ;say ; # OUTPUT: «98»# RIGHT! The use of `atomicint` type guarantees operation is atomicmy atomicint ;await ^100 .map: ;say ; # OUTPUT: «100»
The similarity to int
is present in multi dispatch as well: an atomicint
, plain int
, and the sized int
variants are all considered to be the same by the dispatcher and cannot be differentiated through multi-dispatch.
Numeric "infectiousness" dictates the resultant type when two numerics of different types are involved in some mathematical operations. A type is said to be more infectious than the other type if the result is of that type rather than the type of the other operand. For example, Num type is more infectious than an Int, thus we can expect 42e0 + 42
to produce a Num as the result.
The infectiousness is as follows, with the most infectious type listed first:
Complex
Num
FatRat
Rat
Int
say (2 + 2e0).^name; # Int + Num => OUTPUT: «Num»say (½ + ½).^name; # Rat + Rat => OUTPUT: «Rat»say (FatRat.new(1,2) + ½).^name; # FatRat + Rat => OUTPUT: «FatRat»
The allomorphs have the same infectiousness as their numeric component. Native types get autoboxed and have the same infectiousness as their boxed variant.