In Lua, all the syntax for accessing structured data - in any structure - is at some level an alias for a single "get" operation, which takes a table and a value (the key) and either returns the value most recently stored in that table for that key, or nil
, if no value has been stored in the table for that key. All the syntax for building structures alias for a single "put" operation, which takes a table, a key, and a second value (the data), and modifies the table so that subsequent calls to get for that table and that key will return that data.
You can access the raw operation, passing any value you like as the key or data, using Lua's table syntax. "Get" is spelled tab[key]
, and "put" is spelled tab[key] = value
. Syntax like tab.username
is translated to tab["username"]
for you, which means you can use syntax that's more convenient and conventional, but the semantics are governed by the underlying operation.
In Rust, with structured data, there's no generic get or put operation. Instead, each structure defines a family of operations - two per field, one for setting only and exactly that field and one for retrieving the most recent value. The collection of these operations that are valid in any given program is defined by the declaration of the type.
For example, given
struct Signup {
username: String,
email: String,
}
then there is an operation that takes a Signup value and returns the username, and an operation that takes a Signup value and a String and modifies the Signup so that future calls to the username-retrieving function return the new value. Ditto for the email field - there's a get operation specific to that field, and a set operation. They're spelled signup.username
, signup.username = value
, signup.email
, and signup.email = value
.
This is a surprisingly fundamental difference. For example, it implies that code that tries to access a field which is not declared simply won't compile - the operation it's trying to invoke does not exist. In Lua, the operation does exist, and will set the field you asked for, even if other code operating on that value has no idea that field could have useful information in it. It also means that there's little need in Rust for functions like Lua's keys(tab)
, since there's no way for code to generically access fields by key in the first place.
Rust's approach is a common approach shared by a lot of compiled and statically-typed languages. It requires noticably more work from the programmer in a lot of cases, because every type your program operates on needs to be specified somewhere, whereas in Lua a structure can be defined by use in an ad-hoc way. On the other hand, Rust can tell you if you make a spelling mistake or try to assign to a field that isn't expected to exist; Lua can't, because it has no mechanism to distinguish that from any other get or put to a field in a table. Rust can also tell you if you accidentally try to assign a number or an array to a Signup
's string-valued username field; Lua can't, because there's no specification to tell it that that is a mistake in the first place.
Tuples are equivalent to structs, save that the fields are named .0
, .1
, and so on instead of .username
and .email
. I mean that literally - the two categories of type have identical semantics, and differ only in syntax. Tuples are useful when it's not meaningful to give each element of your structure a name, but otherwise they're interchangeable.
Arrays (and slices) are a little more complicated. Unlike a struct or a tuple, an array can only store values of a single type - but the accessor for that data is an operation that takes an array and a usize
(the index), and returns a value, while the modifier is an operation that takes an array, an index, and an new value, and modifies the array at that index. The syntax for this will be familiar: arr[idx]
retrieves a value, and arr[idx] = val
sets it. They're more like Lua tables than structs are, because do allow modifying values using a key determined at runtime, but they're not identical. For example, accessing or storing a value at an index that's greater than or equal to an array's size will immediately panic your program, whereas Lua will treat it like any other table key and store/retrieve the value. And, unlike Lua, the only valid type for the index is usize
, not any arbitrary value you happen to have on hand.
I think the thread got a little astray on why this matters in the process of discussing the data structure most like a Lua table (HashMap
and friends). The differences you're asking about, and the ones that make the most practical impact on programming in each language, are less about how you might translate table code to Rust, and more about how the difference in the design of the languages' respective data structure systems impacts programmer experience.