Design Decision Grab Bag
Shim intentionally makes design decisions that are not universally accepted. Here is where these design decisions are explicitly acknowledged.
Semicolons
Initially Shim used semicolons. Semicolons were made optional to keep the language simple and to improve velocity. Surveying modern languages, it’s only Rust in the C-like family of languages that kept semicolons.
Keeping them only seemed to hurt the language. Even though it made the parsing rules a bit more complicated, it’s a net simplicity win. As an example, it’s much nicer to switch between if-else statements and expressions now (no trailing semicolon to add/remove).
Operator Overloading
Operator overloading is surprisingly hated by certain factions of developers. Shim didn’t need to support operator overloading in the language since many of the types that would benefit would likely be provided by the Shim host application anyways (think matrix or vector types). However, it’s very nice to be able to prototype in Shim itself, even if a type would end up being promoted to the host application anyways.
C++ famously shows how operator overloading can be abused. But bad APIs can be created with or without operator overloading.
No Function Overloading
Shim does not have any sort of function overloading. When you can branch dynamically on an input type, this sort of overloading isn’t required. Function overloading can make it hard to understand a codebase when one identifier can refer to multiple code blocks.
No Inheritance
Shim very intentionally has no inheritance. This page will never be enough to convince the pro-inheritance crowd of the rationale. Go, Rust, C, Zig, Haskell and many other languages do not have the feature. It’s not required even if muscle memory makes it feel that way.
English and and or Rather Than && and ||
Coming from C/Rust the logical operators are the sigils && and ||. Since
these operators do short-circuiting it makes more sense to me to use a keyword
for these operations. If part of an expression gets skipped/unevaluated it seems
like that should be a keyword. It’s very much not like < or +.
Shim still uses ! for logical negation. I like that since I think it does a
better job of showing operator precedence than the keyword not. On the other
hand, I really like value not in collection as an expression in Python, which
doesn’t really work with !. You could do value nin collection or value !in collection,
but that’s something that would need to be decided on going forward. For now
there’s still !(value in collection), but it would be nice to not need to add
() to your mental stack when understanding code.
No Trailing Closures / Trailing Lambdas
In some modern languages like Swift and Kotlin you have syntax like:
myThing.foo(13) {
print("Done")
}
That snippet is equivalent to passing 13 and a closure that prints “Done” as the
two arguments to myThing.foo. That can serve to make interesting DSLs. In
particular, Swift and Kotlin use that to great effect for making UI libraries.
This syntax is helpful for things like callbacks and DSLs. It’s exactly because of its DSL capabilities that I think it’s best to avoid. I’d much prefer most Shim code to have a relatively consistent texture/feel, similar to the sameness folks feel about Go.
It seems like this syntax is particularly helpful for async/callback heavy code.
In Shim, where the target is largely games, this type of code is uncommon. Even
where there’s mappy/filtery code it would be better as chained .map().filter().
Struct Initialization Does Not Use Curly Braces
Coming from the C-style family of languages, folks might expect Shim to use {}
for struct initialization. Using TypeName() rather than TypeName {} is mostly
inspired by Python. Struct initialization in Shim is very much a function call.
If there are alternative constructors (like is often the case with TypeName::new()
in Rust) it’s a function call anyways.
Shim takes the approach that it’s better to keep {} for blocks (and only blocks)
and keep () for calling code (which struct initialization can do for members
that have default expressions).
Blessed Syntax
Ginger Bill (Odin language creator) has an interesting post on Blessed Syntax and Ergonomics. It’s a great read that underscores the design decisions in Odin where Ginger Bill defends excluding certain features from the Odin programming language. It’s a great read and I’m a fan of his approach. Shim takes a slightly different tactic. The language itself is fairly minimal. However, we explicitly encourage users to fork and customize the language. There is functionality (like variable-length arguments, keyword-only arguments, positional-only arguments) that is only possible if you extend the language in Rust. Things like dynamically looking up or assigning attributes can’t be done in the language itself, but can be done in Rust.
For some functionality, users must go outside of the language. Shim is not meant to be a Generalized language (where all the built-in functionality can be implemented by user types), but can have the effect of a Generalized language since the language can be extended.
Even something like inheritance (which I’m not a big fan of) could be fairly easily implemented on top of the current language. An LLM could bang that out in several minutes (add the superclass to the AST, update the compiler to initialize all structs with an optional parent, and update attribute access to fallback on the superclass).
I allowed operator overloading in Shim for practical reasons. I want folks to be able to prototype new data types in Shim directly, even if they will later move those data types to Rust ShimNative types anyways.