Basics · Lesson 1.12

Convert

Source
Convert between numeric, character and boolean types with as, and see what a value that does not fit becomes.
You'll need: Integer, Float, Boolean, Character

Rux converts between types on its own only when nothing can be lost. Every other conversion has to be asked for with as — because every other conversion can change the value. Writing as is how you tell the compiler, and the next reader, that the change is intended.

Widening happens on its own

Widening within one family and one signedness — int8 to int32, float32 to float64 — can never lose anything, so the compiler does it silently:

let small: int8 = 100;
let widened: int32 = small;

Everything else needs as

flowchart TD
    src["A value of type A, needed as type B"] --> w{"Same family and signedness,<br/>and B is wider?"}
    w -- "yes" --> auto["Converted automatically"]
    w -- "no" --> as["Write value as B"]
    as --> n["Narrower integer:<br/>keeps the low bits"]
    as --> s["Other signedness:<br/>same bits, read differently"]
    as --> f2i["Float to integer:<br/>truncates toward zero, saturates"]
    as --> i2f["Integer to float, char to number,<br/>bool to number and back"]

Narrowing keeps the low bits

300 needs nine bits; an int8 has eight, and what survives is 300 − 256 = 44:

let big: int32 = 300;
let narrowed = big as int8;

Changing signedness re-reads the bits

−1 has every bit set, and eight set bits read as an unsigned number are 255:

let negative: int32 = -1;
let reinterpreted = negative as uint8;

Floats and integers

Integers and floats are different families, so crossing between them always needs as. Float to integer drops the fraction — it truncates toward zero rather than rounding:

let ratio = 3.9;
let negativeRatio = -3.9;

ratio as int32 is 3, and negativeRatio as int32 is -3. A float too large for the integer type does not keep low bits the way narrowing does: it saturates to the largest (or smallest) value the type holds, so 1e10 as int32 is 2147483647.

A narrower float keeps fewer digits, so float64 to float32 needs as as well: 3.141592653589793 becomes 3.1415927.

Characters and booleans

A character is a number underneath — its code point — and as shows it: 'A' as uint32 is 65. A boolean becomes 1 or 0; in the other direction, zero is false and anything else is true.

as never fails

None of these conversions reports a problem. A conversion with as always produces a value, and when the original does not fit, that value is simply a different one. To find out first whether a value fits, compare it with the target type's limits — or use the checked conversions of Checked convert.

ConversionExampleResult
Widenint8 100 → int32100
Narrowint32 300 as int844
Signednessint32 −1 as uint8255
Float → int3.9 as int323
Float → int, too big1e10 as int322147483647
Int → float7 as float647.0
Char → int'A' as uint3265
Bool ↔ inttrue as int, 5 as bool1, true

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// A conversion happens on its own only when nothing can be lost: widening within one family and
// one signedness, such as `int8` to `int32` or `float32` to `float64`. Every other conversion is
// asked for with `as`, because every other conversion can change the value. Writing `as` is how
// you tell the compiler, and the next reader, that the change is intended.
import Io::PrintLine;

func Main() -> int {
    // Widening needs no `as`. Every `int8` is also an `int32`, so the compiler converts silently.
    let small: int8 = 100;
    let widened: int32 = small;
    PrintLine("widen        int8 {} becomes int32 {}", small, widened);

    // Narrowing must be asked for, and keeps only the low bits that fit. 300 needs nine bits, an
    // `int8` has eight, and what survives is 300 - 256.
    let big: int32 = 300;
    let narrowed = big as int8;
    PrintLine("narrow       int32 {} becomes int8 {}", big, narrowed);

    // Changing signedness keeps the bits and reads them differently: -1 has every bit set, and
    // eight set bits read as an unsigned number are 255.
    let negative: int32 = -1;
    let reinterpreted = negative as uint8;
    PrintLine("sign         int32 {} becomes uint8 {}", negative, reinterpreted);

    // Integers and floats are different families, so crossing between them always needs `as`.
    // Float to integer drops the fraction: it truncates toward zero rather than rounding.
    let ratio = 3.9;
    let negativeRatio = -3.9;
    PrintLine("float->int   {} becomes {}", ratio, ratio as int32);
    PrintLine("float->int   {} becomes {}", negativeRatio, negativeRatio as int32);

    // A float too large for the integer type does not keep low bits the way narrowing does. It
    // saturates: it becomes the largest value the type holds, or the smallest, below the range.
    let huge = 1e10;
    PrintLine("float->int   {} becomes {}", huge, huge as int32);

    let count: int32 = 7;
    PrintLine("int->float   {} becomes {}", count, count as float64);

    // A narrower float keeps fewer digits, so float64 to float32 needs `as` as well.
    let pi = 3.141592653589793;
    PrintLine("narrow float {} becomes {}", pi, pi as float32);

    // A character is a number underneath, its code point, and `as` shows it.
    let letter = 'A';
    PrintLine("char->int    {} becomes {}", letter, letter as uint32);

    // A boolean becomes 1 or 0. In the other direction, zero is false and anything else is true.
    let yes = true;
    let five = 5;
    PrintLine("bool->int    {} becomes {}", yes, yes as int);
    PrintLine("int->bool    {} becomes {}", five, five as bool);

    // None of the conversions above reported a problem. A conversion with `as` never fails: it
    // always produces a value, and when the original does not fit, that value is simply a
    // different one. To find out first whether a value fits, compare it with the target type's
    // limits — comparisons are the next part, and choosing what to do with the answer the one
    // after.
    return 0;
}

Run it

cd Examples/Basics/Convert
rux run
widen        int8 100 becomes int32 100
narrow       int32 300 becomes int8 44
sign         int32 -1 becomes uint8 255
float->int   3.9 becomes 3
float->int   -3.9 becomes -3
float->int   10000000000.0 becomes 2147483647
int->float   7 becomes 7.0
narrow float 3.141592653589793 becomes 3.1415927
char->int    A becomes 65
bool->int    true becomes 1
int->bool    5 becomes true

Common mistakes

Expecting rounding.
2.99 as int32 is 2, not 3: conversion truncates toward zero. Round first if that is what you mean — the Math lesson has Round.
Expecting an error on overflow.
as never fails. 300 as int8 quietly becomes 44. When the value might not fit, check it first or use Checked convert.

Try it yourself

  1. Convert -1 as uint16 and -1 as uint32. Predict the results before you run.
  2. Print the code points of the letters in R, u, x with as uint32.
  3. Convert -1e10 to int32 and check that it saturates to the smallest value.

Learn more