Skip to content

Scalars & Strings

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

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

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.
  • Bytes is immutable binary data, and Byte is one byte in that buffer. Use it for UTF-8 boundaries, file/network payloads, and Go []byte FFI.

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.to_codepoints when you need the lower-level scalar values:

import {
  std/codepoints.Codepoint
}

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

  "café"
  |> String.to_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.

Use String.to_bytes to encode text as UTF-8 bytes. Bytes is iterable, so the generic Iter.* functions work on it, and Byte.to_int exposes each Byte as an integer when you need to inspect it. Bytes + Bytes concatenates buffers; the named form is Bytes.concat.

import {
  std/io
}

fn main() {
  raw = String.to_bytes("café")
  dbg Bytes.length(raw)
  dbg raw |> Iter.map(Byte.to_int) |> Iter.to_list()

  prefix = String.to_bytes("go:")
  case Bytes.to_string(prefix + raw) {
    Ok(text) -> io.print(text)
    Err(reason) -> io.print(reason)
  }
}

Byte itself is not numeric. Convert through Byte.from_int and Byte.to_int when a boundary really needs integer values.

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

import std/io

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

A backtick string is raw: escapes, ${...} interpolation, and $$ dollar doubling are not processed. Use it for embedded text where those characters should pass through literally:

import std/io

fn main() {
  pattern = `\d{3,4}`
  template = `price: ${PRICE}`
  query = `
    SELECT *
    FROM invoices
    WHERE total > ${MIN_TOTAL}
    `

  io.print(pattern)
  io.print(template)
  io.print(query)
}

Multi-line raw strings use the same indentation rules as triple-quoted strings.

For compiled regular expressions, use the Regex typed literal form shown in Typed Literals.

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