A lot of work has gone into making Cone's grammar simple and clear. The lesser benefit is that code creation is faster and easier to remember. The greater benefit is that the resulting concise, readable code is easier to understand and improve, especially when one's eyes are not distracted by unnecessary, space-consuming boilerplate.
On another page, we will review Cone's rich code reuse mechanisms which can also play a huge role in keeping code small, by making it easier to create a single piece of code that well serves a number of diverse needs. For now, let's concentrate on Cone's grammatical shortcuts that help keep Cone programs small and easy-to-understand:
To make code more forgiving, statement-ending semicolons are often optional. Likewise, curly braces can optionally be omitted. The compiler makes use of significant indentation (in these situations only) to infer the end of multi-line statements and to delimit a block's statements.
fn fact(nbr u32): if nbr<=1: 1 else: n*fact(n-1)
This approach eliminates both the visual distraction and the need to type these punctuation characters. Another benefit is that code takes up less space vertically, allowing more of the meat of the program to be visible in a code editor. The resulting code often looks like a neat, clean, declarative outline.
These features are entirely optional. If a programmer dislikes significant indentation and optional semicolons, then semi-colons and braces may be explicitly specified where needed.
'with' blocks are one of Cone's more distinctive features. They make it possible to create declarative-looking code blocks that concisely build or manipulate complex data collections (such as 3D content).
A 'with' block starts with an expression which establishes the value/collection to focus on. Throughout the course of the block, the pseudo-variable this refers to that value. More importantly, the dot ('.') and '<-' operators can implicitly refer to this:
// A with block, where 'this' refers to the Frosty npc with $.npcs["Frosty"]: with .inventory: // work with Frosty's inventory .. <- magnifying_glass // add to the inventory <- cowboy_hat .size *= .reducer // equivalent to: this.size = this.size * this.reducer with .dictionary: // work with Frosty's dictionary .["carrot"]: "wortel" // add a word and its translation .["coal"]: "steenkool" .jump(2.0) // equivalent to: this.jump(2.0)
Although 'with' blocks can sometimes resemble and act like method cascades, Cone's 'with' block mechanism is considerably more versatile and flexible. In human conversations, we regularly use pronouns like "it" or "we" as placeholders to refer to something specific within the context of each conversation. Such pronouns make conversations pithier and less stilted. 'this' plays a similar role in Cone programs.
When used in conjunction with significant indentation, 'with' blocks make it possible for Cone, as a procedural language, to express and work with content as concisely and declaratively as any markup language. Furthermore, the indented, compositional structure of Cone's code greatly facilitates the modular "snap-together" assembly of strings, collections, and 3D world parts.
A key reason most statically-typed languages are somewhat more verbose than dynamic languages is the requirement for type annotations scattered throughout the code. Type inference and defaults go a long way towards reducing this overhead.
Cone supports unidirectional type inference, which means that type annotations are generally required on function signatures and object creation, but can mostly be avoided otherwise (particularly variable declarations). Furthermore, Cone supports type defaults for the most common usage. For example, '&' defaults to a const, borrowed reference. Similarly, methods allow a parameter's type to be omitted, should it be the same as the type that the method is declared within.
High-level Control Abstractions
Although if and while control structures offer general-purpose flexibility, there are certain control flow patterns that are common enough that more-concise formulations are worth baking into Cone:
- each loops collapses the detailed mechanics of iterating over a sequence of values into a single line. Invisible to the coder, it automates iterator creation, value binding, incrementation and destruction in a safe, type-generic way.
- match collapses the detailed mechanics of matching a single value to a sequence of patterns in a type-generic way. Key to its simplicity is the ability to safely bind extracted contents from any successful matches.
- RAII and move semantics collapses the detailed mechanics of ensuring that allocated resources are safely disposed of at the end of their lexical lifetime, avoiding the need for explicit invocation of finalizers and free logic.
- panic collapses the detailed mechanics of safely handling exception conditions, providing a fast path to the exit door that ensures open resources have been successfully disposed of.
As the earlier indentation example showed, the return keyword can often be omitted. Functions that don't return a value don't need it. Functions that do return one or more values, can specify the values to return on the last line. This inference bubbles into if and match control structures, as illustrated above.
Implicit return is consistent with Cone treating blocks and if/match structures as expressions. Just like return inference, the last value in each block is treated as the computed value for that block. Such consistency simplifies code refactoring and makes macro expansions more versatile.
Tuples (ad hoc structs) greatly improve the convenience of working with multiple values simultaneously. Implicit use of tuples avoids the code overhead involved with declaring the data structure and then individually moving around (destructuring) each element of the tuple. Important use cases for tuples are parallel assignment, multiple return values and marshalling queued-messaging data for use by a thread or actor.
a,b = b,a // parallel assignment used to swap values success, result = action(val) // multiple return values
Infix punctuation operators are both concise and readily digestible, far more so than named functions and methods. Cone allows any type to define appropriate methods for a large number of these operators, include equality comparisons, pattern matching, arithmetic and logic operators, and even various operator assignments (e.g., '+=').