Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Methods

Bop dispatches methods with .name(args...). Every built-in method on primitives, arrays, strings, and dicts is listed here. User-defined methods on structs use the same syntax — see Structs & Enums for the fn Type.method(self, ...) form.

Methods on every value

Three methods work on any value — introspection + stringification. They’re dispatched before the type-specific tables, so they’re always available:

MethodReturnsNotes
x.type()stringOne of "int", "number", "string", "bool", "none", "array", "dict", "fn", "struct", "enum", "module", "iter"
x.to_str()stringDisplay repr — same as what print(x) would emit for a single arg
x.inspect()stringDebug repr — strings are wrapped in "...", nested strings stay quoted inside arrays / dicts
x.is_none()booltrue iff x is the none value. Equivalent to x == none.
x.is_some()boolInverse of .is_none()true for every value except none.
print((42).type())                 // "int"
print("hi".to_str())               // "hi"
print("hi".inspect())              // "hi"   (quoted)
print([1, "two"].inspect())        // [1, "two"]

print(none.is_none())              // true
print((0).is_none())               // false — `0` is falsy but not `none`
print(first_result().is_some())    // check an optional return without `== none`

.is_none() / .is_some() cover Bop’s “any variable can be none” story — they work on every receiver, not just Option-shaped ones (Bop doesn’t have Option). Equivalent to x == none / x != none, but reads better in method chains.

Parens around numeric literals

Number literals need parens before a method call because . is otherwise a decimal point:

// print(42.type())   // parse error — `42.t…` looks like a decimal
print((42).type())     // "int"
print((-5).abs())      // 5

Identifiers don’t have this problem: x.type(), count.to_str(), etc.

Numeric methods — int and number

All of these work on both int and number receivers. Return type is noted per method; most math operations always widen to number.

MethodReturnsDescription
x.abs()int / numberAbsolute value. Preserves receiver type. (-5).abs()5 (int), (-2.7).abs()2.7 (number). Integer overflow on (i64::MIN).abs() is a runtime error.
x.sqrt()numberSquare root.
x.sin(), x.cos(), x.tan()numberTrig. Angles in radians.
x.exp()numbere^x.
x.log()numberNatural log.
x.pow(e)numberx raised to e.
x.floor(), x.ceil(), x.round()int / numberRound toward -∞, +∞, or nearest (ties away from zero). Returns int when the rounded result fits in i64, number otherwise (so rounding a number that overflows i64 stays a number instead of raising). Int receivers pass through unchanged.
a.min(b), a.max(b)int / numberPair-wise. Preserves type when both sides match; widens to number on mixed int/number.
x.to_int()intTruncates toward zero. (3.7).to_int()3, (-2.7).to_int()-2.
x.to_float()numberWidens intnumber; number passes through.
print((9).sqrt())                   // 3
print((0).cos())                    // 1
print((2).pow(10))                  // 1024
print((3).min(7))                   // 3
print((1).max(2.5))                 // 2.5   (widened)
print((3.7).floor())                // 3     (int)
print((3.7).floor().type())         // "int"

Boolean methods — bool

MethodReturnsDescription
b.to_int()inttrue.to_int()1, false.to_int()0.
b.to_float()numbertrue.to_float()1 (as number), false.to_float()0.

Plus the universal type / to_str / inspect.

String methods — string

See Strings for worked examples.

MethodReturnsDescription
s.len()intNumber of Unicode code points.
s.contains(sub)boolWhether sub appears anywhere.
s.starts_with(prefix)bool
s.ends_with(suffix)bool
s.index_of(sub)intByte index of first occurrence, or -1 if not found.
s.split(sep)arraySplit into an array of strings on sep.
s.replace(old, new)stringReplace every occurrence.
s.upper(), s.lower()stringCase conversion.
s.trim()stringStrip leading / trailing whitespace.
s.slice(start, end)stringSubstring by code-point index.
s.to_int()intParse. "3.7".to_int() parses as float then truncates → 3. Raises on junk.
s.to_float()numberParse. Raises on junk.
s.iter()iterLazy iterator over Unicode code points. See Iter methods.

Array methods — array

See Arrays for worked examples.

MethodReturnsDescription
arr.len()intNumber of elements.
arr.push(v)noneAppend.
arr.pop()valueRemove and return the last element.
arr.has(v)boolStructural equality check.
arr.index_of(v)intIndex of first match, or -1.
arr.insert(i, v)noneInsert at index, shifting right.
arr.remove(i)valueRemove at index, returning the removed value.
arr.slice(start, end)arraySub-array.
arr.reverse()noneIn-place.
arr.sort()noneIn-place, numeric or lexicographic depending on element types.
arr.join(sep)stringJoin after stringifying each element.
arr.iter()iterLazy iterator over the elements. See Iter methods.

Dict methods — dict

See Dictionaries for worked examples.

MethodReturnsDescription
d.len()intNumber of entries.
d.keys()arrayAll keys as strings.
d.values()arrayAll values.
d.has(key)boolWhether key exists.
d.iter()iterLazy iterator over keys, in declaration order. See Iter methods.

Result methods — Result

Result is an engine built-in. All combinators are methods on the built-in type — no import required. Ok(x) and Err(e) are parser-level shorthand for Result::Ok(x) / Result::Err(e) in both expression and pattern position.

MethodReturnsDescription
r.is_ok()booltrue when r is Result::Ok(_).
r.is_err()booltrue when r is Result::Err(_).
r.unwrap()valuePayload on Ok; raises a runtime error on Err (message includes the .inspect() of the payload).
r.expect(msg)valuePayload on Ok; raises with msg on Err.
r.unwrap_or(default)valuePayload on Ok; default on Err.
r.map(f)ResultOk(v)Ok(f(v)); Err(e) passes through.
r.map_err(f)ResultErr(e)Err(f(e)); Ok(v) passes through.
r.and_then(f)ResultOk(v)f(v) (expected to return a Result); Err(e) passes through.
print(Ok(5).is_ok())                           // true
print(Err("bad").unwrap_or(0))                 // 0
print(Ok(5).map(fn(v) { return v * 2 }))       // Result::Ok(10)
print(Err("x").map(fn(v) { return v * 2 }))    // Result::Err("x")

fn halve(x) {
  if x % 2 == 0 { return Ok((x / 2).to_int()) }
  return Err("odd")
}
print(Ok(8).and_then(halve).and_then(halve))   // Result::Ok(2)

Iter methods — iter

An iter is Bop’s lazy iterator. Values you can iterate over — arrays, strings, dicts, built-in iterators, and user-defined containers — all participate in the same protocol:

  1. v.iter() returns an iterator.
  2. it.next() advances it, returning Iter::Next(value) or Iter::Done.

for x in v uses this protocol, so anything with a working .iter() method works with for.

MethodReturnsDescription
it.next()Iter::Next(v) / Iter::DoneAdvance by one. Cloning an iterator shares its cursor (like Python / Rust / JS) — two names pointing at the same iterator advance together.
it.iter()iterReturns the same iterator. Makes for x in it work whether it is already an iterator or a fresh iterable.
let it = [10, 20, 30].iter()
print(it.type())                // "iter"
print(it.next())                // Iter::Next(10)
print(it.next())                // Iter::Next(20)

for x in it { print(x) }        // 30  (picks up from the current cursor)

User-defined iterables

A struct can participate in the iterator protocol by implementing .iter() (and, if it’s its own iterator, .next()):

struct Bag { items }
fn bag_of(arr) { return Bag { items: arr } }
fn Bag.iter(self) { return self.items.iter() }   // delegate to the backing array

let b = bag_of(["x", "y", "z"])
for v in b { print(v) }                           // x  y  z

That’s the minimal shape. A container that wraps an array and delegates .iter() is the 80% case. User types with genuine internal state (like a lazy counter) work the same way — define fn Counter.iter(self) to return an iterator (either the backing data’s iterator, or self if self also has .next()).

The Iter enum

.next() returns one of two variants of the built-in Iter enum — always in scope, no use required:

enum Iter {
  Next(value),
  Done,
}

Pattern-match directly:

let it = [1, 2].iter()
let r = it.next()
print(match r {
  Iter::Next(v) => "got: " + v.to_str(),
  Iter::Done    => "exhausted",
})
// got: 1

Struct / enum methods

User-declared. See Structs & Enums. Method dispatch on a struct tries the universal common methods first, then looks up fn TypeName.method declared in the same module.

Same-module rule: user-declared methods must live in the module that declares the type. You can’t extend a type imported from another module, and you can’t add methods to the built-ins (int, string, array, Result, Iter, …). See Methods must live in the type’s own module for the rationale and the “use a free function instead” workaround.

Module methods

If you use path as m, m is a Value::Module. m.type()"module", m.inspect()"<module path>". Otherwise . on a module accesses its exports:

use std.math as m
print(m.PI)             // exported constant
print(m.type())         // "module"   (universal method, not an export)