Skip to content

Scalars & Strings

Nomi has three numeric scalar types — Int, Float, Decimal — plus Bool and String.

Arithmetic, comparisons, and the logical operators and / or / ! all read as you’d expect:

fn main(): Bool {
  dbg 42 + 8
  dbg 10 - 3
  dbg 2 * 21
  dbg 20 / 4
  dbg -(3 + 4)
  dbg 42 > 10
  dbg True and !False
}

Numbers can use digit separators, alternate radices (hex / binary / octal), or scientific notation — different spellings of the same values:

fn main(): Float {
  dbg 1_000_000
  dbg 0xFF
  dbg 0b1010
  dbg 1.0e10
}

Float is fast but approximate: small rounding errors accumulate, and familiar identities like the one below quietly fail. Decimal is the exact alternative — use it whenever rounding error is a bug (money, billing, anywhere correctness matters more than speed). Decimal literals carry a d suffix:

fn main(): Decimal {
  // Float carries base-2 imprecision
  dbg 0.1 + 0.2 == 0.3

  // Decimal is exact
  dbg 0.1d + 0.2d == 0.3d

  // Sum four prices to the cent — exactly
  dbg 19.99d + 5.00d + 2.50d + 0.99d
}

Strings — interpolation and concatenation

Section titled “Strings — interpolation and concatenation”

Strings interpolate ${expr} and concatenate with +:

import std/io: IO

fn main() {
  name = "Nomi"
  IO.print("Hello, ${name}!")
  IO.print("1 + 2 = ${1 + 2}")
  IO.print("Hello" + ", " + "Nomi")
}

IO.print accepts any value that can be displayed, so you rarely need to stringify before printing — interpolation calls each value’s Display.to_string for you.

+, -, *, and / are backed by standard operator interfaces: Add, Subtract, Multiply, and Divide. The built-in scalar impls cover numeric arithmetic; + also covers string concatenation. Custom types can implement the same interfaces when the operator is the clearest domain operation.

Nomi’s text model has three pieces:

  • String is text. Nomi has no separate Char type, so even one-character text is a String.
  • A grapheme is one user-visible character. é and many emoji count as one grapheme even when they are built from multiple Unicode values. Nomi represents graphemes as String values.
  • Codepoint is an integer-backed Unicode scalar value: a valid number in Unicode’s scalar-value range. Use it when you need to inspect the lower-level values that make up text.

Most string operations use the grapheme view: String.length, String.graphemes, String.slice, and String.reverse work in user-visible characters rather than raw bytes.

Use String.codepoints when you need the lower-level scalar values:

fn main(): List<Int> {
  dbg String.length("café")
  dbg String.graphemes("café")

  "café"
  |> String.codepoints()
  |> Iter.map(Codepoint.to_int)
  |> Iter.to_list()
  |> dbg
}

String equality is byte-based, so visually identical text can compare unequal when it uses different Unicode forms. Normalize with String.normalize before comparing text from mixed sources.

A """…""" literal spans multiple lines. Interpolation works the same way, and leading indentation common to every line is stripped:

import std/io: IO

fn main() {
  name = "Alice"
  query = """
    SELECT *
    FROM users
    WHERE name = '${name}'
    """
  IO.print(query)
}

The next chapter — Collections — uses these scalar values inside lists, maps, and sets.