Collection Types
The collection types are Arr
for arrays, Str
for strings, and Tab
for tables. Their
API has a few quirks to bear in mind.
Error-Handling
We try to keep panics to an absolute minimum. Because there are a lot of things which can
potentially go wrong when interacting with a collection (out-of-bounds errors, failed type
conversions, mutating a borrowed collection), almost every collection method returns a
GResult
. You'll need to make generous use of the ?
operator.
Interior Mutability
All of the collection types are both aliasable and mutable - in Rust, this requires interior mutability. There are two ways this could potentially have worked:
-
Allow the Rust programmer to explicitly lock the interior of the type, as though working with a
RefCell
, and then mutate it freely - perhaps even directly accessing the internal storage. -
Build the API around "atomic" method calls which lock the type, do something to it, and then unlock it before returning.
GameLisp previously chose the first solution, but it ended up cluttering the Rust code with
too many calls to borrow()
and borrow_mut()
, so we switched to the second option. The
second option also causes fewer lifetime problems in practice, since borrow()
handles tend to be
scoped for a longer lifetime than necessary.
The only methods which aren't completely atomic are those which create a Rust iterator over a collection. If a collection is mutated while Rust code is iterating over it, an error will occur.
Because we don't allow the caller to create direct references into a collection's storage, none of
our collections can be indexed using Rust's []
syntax. You'll need to use the get()
and
set()
methods instead.
Construction
There is no way to place an Arr
, Str
or Tab
directly on the Rust stack. Instead, they're
always accessed indirectly via a Root
smart pointer: Root<Arr>
, Root<Str>
, Root<Tab>
.
New collections can be allocated using global functions in the glsp
namespace, such as
glsp::arr
, glsp::str_from_iter
, and glsp::tab_with_capacity
.
Alternatively, we provide convenience macros for constructing each type of collection, in the
same spirit as Rust's vec![]
macro.
arr!
constructs a new array, returning it as a Root<Arr>
:
#![allow(unused_variables)] fn main() { arr![]; arr![elem; n]; //elem, repeated n times arr![100i32, 20u8, "hello", arr![]]; //types are converted using the IntoVal trait arr![a, ..src, b]; //splays the contents of src using the Splay trait }
str!
has the same syntax as Rust's format!
macro, but it returns a Root<Str>
rather than a String
:
#![allow(unused_variables)] fn main() { str!(""); str!("hello, str"); str!("{}, {}", "hello", glsp::sym("str")?); }
tab!
constructs a new table from a number of key-value pairs. Optionally, it can
clone another table and then insert each of the key-value pairs into it, similar to Rust's
struct update syntax:
#![allow(unused_variables)] fn main() { let empty_tab = tab! { }; let base_tab = tab! { (glsp::sym("a")?, 1), (glsp::sym("b")?, 2) }; let extended_tab = tab! { (glsp::sym("b")?, 20), (glsp::sym("c")?, 30), ..base_tab }; assert!(extended_tab.len() == 3); }
Conversion to a Val
is fallible: the arr!
and tab!
macros will panic if you pass them
something which can't be represented as a Val
, like std::u32::MAX
. In the unlikely event
that you need to catch this type of error, the try_arr!
and try_tab!
macros are
identical to their counterparts, except that they return a GResult<Root<_>>
.
Deques
Within GameLisp, arrays and strings both support the deque
interface, so that it's possible to
write a function which is generic over both types. The same is true for their Rust API.
The relevant traits are DequeOps
, DequeAccess
and DequeAccessRange
. You'll need to
make sure those traits are in scope (perhaps by importing the
prelude) when working with arrays or strings.
The DequeAccess
and DequeAccessRange
implementations are generic over
all of Rust's built-in integer types, so there's no need to convert indexes to usize
. Negative
indexes count backwards from the end of the deque.
#![allow(unused_variables)] fn main() { let n: f32 = arr.get(30_u8)?; arr.set(-1, n * 2.0)?; }
Type Erasure
If you need to define a variable, field or non-generic function argument which can be either a
Root<Arr>
or a Root<Str>
, you can use the Deque
enum. It implements the DequeOps
,
DequeAccess
and DequeAccessRange
traits, so you don't need to unwrap it in order to
manipulate its contents.
#![allow(unused_variables)] fn main() { fn push_one_pop_one(deq: Deque, to_push: Val) -> GResult<Val> { deq.push(to_push)?; deq.pop_start() } }
There are similar enums for GameLisp's other abstract types:
-
Num
stores ani32
orf32
. It supports all of Rust's arithmetic operators, and also a subset of Rust's standard API for integer types, like thediv_euclid()
method. -
Callable
stores those primitive types which can receive a GameLisp function call. -
Expander
stores those primitive types which can be used as a macro expander. -
Iterable
stores those primitive types which can be used to construct aGIter
.
Iteration
Collections provide three separate iteration methods.
iter()
is an infallible iterator which returns the collection's natural type: Val
for Arr
,
char
for Str
and (Val, Val)
for Tab
.
iter_to<T>()
performs type conversion from its natural type to T
. Because type conversion is
fallible, the iterator's Item
type is GResult<T>
, so each item will need to be individually
unwrapped.
#![allow(unused_variables)] fn main() { let mut counter = 0u32; for n in arr.iter_to::<u32>() { counter += n?; } }
giter()
allocates and returns a new GameLisp iterator, Root<GIter>
, over the collection.
Due to some limitations in Rust's type system, it's not possible to call iteration methods on a
Tab
directly. Instead, you'll need to use the entries()
adapter method: for example,
tab.entries().iter()
or tab.entries().keys_to::<u32>()
.
Cloning
The clone()
method name has already been claimed by Rust. If you call clone()
for a
Root<Arr>
, it will create a new Root
which refers to the same array, rather than cloning the
array itself.
When you need to clone the contents of a collection, the correct methods are shallow_clone()
and deep_clone()
.