Language Design: Multi-Color Number Literals
TLDR: Use multiple colors to make the various components of your number literals easily distinguishable. Different colors for: base prefix type suffix float exponent digits value Instead of: Do:
TLDR: Use multiple colors to make the various components of your number literals easily distinguishable. Different colors for: base prefix type suffix float exponent digits value Instead of: Do:
The recent development of Java is an interesting case, as it faces these questions more acutely with the introduction of value types in Project Valhalla . Recap Java-before-value-types worked like this: == Object#equals primitive types ( int , float , byte , …) primitive equality not available reference types ( java.lang.Integer , java.lang.Float , java.lang.Byte , …) reference equality value…
(Inspired by Almost Rules .) Syntax : is followed by a type except inside struct initializers, where it is followed by a value except function result types, which are preceded by -> generics use <> except in expression contexts, which uses ::<> invocations use () except where {} or [] is used, because “they convey important information” except for macro invocations, where () , {} , [] are…
Many languages’ Option and Result types suffer from an organically-grown and therefore inconsistently named set of functions. To avoid this, a simple naming scheme can be used to derive a full set of useful methods with predictable names for these types. The examples below use variant names Some and None for Option , and variant names Pass and Fail for Result . 1 Naming Scheme Given a function…
Rust has distinct syntactic facilities for … invoking functions initializing structs and enums initializing tupled structs and enums … that provide different affordances and features: method arguments are positional and cannot be named initializer arguments are named, and what looks like positional arguments is a special shorthand notation initializer arguments for tuple structs and enums look…
TL;DR: Regardless on where you stand on the “Rust 2.0”, Rust’s current approach to language evolution is not sustainable. Whenever a language considers adding a feature, the cost of having to remove the feature (for any reasons) should be factored in from the start. In Rust case, where fixing pretty much any anything after release is close to impossible – that cost function goes toward infinity.…
TL;DR: Optimize for the common case, not the exotic ones. First of all: The argument is not that break and continue in loops aren’t … useful convenient sometimes the best option … That’s not the argument being made. The argument that is being made is that break and continue are … … optimizing for an infrequent special case … Consider a codebase that contains 1000 loops. Out of those 1000, 900…
simple if expression if x == 1.0 { "a" } else { "z" } This translates straight-forward to Rust: if x == 1.0 { "a" } else { "z" } multiple cases, equality relation if x ... == 1.0 { "a" } ... == 2.0 { "b" } else { "z" } In Rust, using match is idiomatic: match x { 1.0 => "a" , 2.0 => "b" , _ => "z" } multiple cases, any other relation if x ... == 1.0 { "a" } ... != 2.0 { "b" } else { "z" } Rust…
I was never too happy with the existing naming approaches for the Result type: Success/Failure pro: both names have the same length, like Option’s Some/None con: quite long, concern that people may use Option over Result Ok/Err pro: short con: names don’t have the same length, leading to inconsistent indentation when pattern matching con: Err is not a word The new naming ticks all the boxes:…
TLD;DR: Your compiler should treat schema definitions as a valid (alternate) source syntax of your programming language. What’s the goal Failed Alternatives poorly integrated source generation compiler plugins macros/type providers annotating program texts
Most people think only in terms of the dichotomy between Nominal-Manifest-Static-Strong and Structural-Inferred-Dynamic-Weak in any given discussion of programming language type system design. And it is exhausting. (from what does strong and weak typing mean to you ) Most individual distinction are a scale, not a strict yes/no checkbox. ( inspiration ) Typed ⟷ Untyped (Typing Modality/Presence) A…
get sugar Rule x.get(y) can be written as x(y) Explanation Instead of special-purpose syntax that is used for indexing operations (reading) in many languages, like int firstValue = someArray [ 0 ]; one can write let firstValue = someArray(0) /* same as */ let firstValue = someArray.get(0) assuming a definition like class Array[T] fun get(idx: Int64): T = ... In combination with varargs, it can…
Merit Points Item -100 Adding a language feature to do something that can already be done -90 Adding a language feature to do something that can be implemented in a library -80 Adding a language feature to do something that can be achieved by fixing a compiler bug -70 Adding a new element to the global namespace -60 Adding a new element to a util namespace -20 Adding a new element to the standard…
TL;DR: Properties are a hack employed to retrofit “nice” syntax into languages that already shipped with fields and methods. Instead, design the language to deliver the same (or more) benefits with fields! Why do properties exist? The core feature of properties, in rough terms, is that (unlike getters and setters) property invocations look like field access, but retain the possibility to add logic…
In the past, many languages did not pick up easily adoptable language design improvements and opted for familiarity instead, often in a misguided attempt to keep perceived language complexity down. Examples include 1 : C’s broken operator precedence 2 spread to many other languages, most of whom have little in common with C. C++’s use of <> for generics, which was adopted by languages that –…
Rust designers recognized the issues with Haskell’s approach, but were not able to address the issues with Rust’s Eq and PartialEq traits. The main cause of this failure is the sub-typing relationship between PartialEq and Eq : It requires that an implementation of partial order is consistent with an implementation of total order. This works for many types, but not for floating point types, for…
Projections Function Name Code Example Explanation map(<fun>) List(1, 2, 3).map(_ + 1) --> List(2, 3, 4) Returns a stream in which fun is applied to each element. mapMany(<fun>) mapMulti flatMap mapFlat mapAndFlatten List(1, 2).mapMany(x -> List(x, x)) --> List(1, 1, 2, 2) List(1, 2).mapMany(x -> Some(x)) --> List(1, 2) List(1, 2).mapMany(x -> None) --> List() Returns a stream in which fun is…
Function Name Code Example Explanation – List(12.3, 45.6)(0) --> Some(12.3) Map("key", "val")("key") --> Some("val") retrieves the value at the given index/key at(idx) Array(12.3, 45.6).at(1) --> Some(Ref(arr, 1)) returns a reference to the given position in the array contains(val) List(1.0, -0.0, NaN).contains(0.0) --> true List(1.0, -0.0, NaN).contains(NaN) --> true Map("key",…
There is no good reason¹ why some type names need to start with a lower-case letter ( int , float , str , …) and others with an upper-case letter ( String , BigInt , Array , …). Instead: Pick one naming rule, and stick to it while building your language. ¹Stupid Reasons fAmiLiAriTy But types with lower-cased names are “primitives”!!1! Akkkchually, they aren’t lower-cased type names, they are…
Kotlin Kotlin gave up on it , as they couldn’t figure out how to recognize annotation usages as early in the compiler pipeline as modifiers previously. This lead to the determination that modifiers (without the prefix @ ) had to stay, but annotations would not always be able to omit the prefix @ , leading to inconsistencies. Ceylon Ceylon tried the route in which everything is an annotation, but…
TL;DR: If your language has annotations 1 , it doesn’t need modifiers. Drop modifiers. Modifiers (such as public , static or abstract ) were traditionally built into languages; as keywords, they were part of the core language syntax. Annotations (such as @deprecated , @test , @derive ) are usually defined in libraries; similar to a class or interface, there exists a source file that defines each…
Overview Syntactic Wrapping No Syntactic Wrapping No Runtime Tags untagged union (C union , C++ union , Rust union ) union type (TypeScript union type ) Runtime Tags discriminated union/tagged union (Rust enum , F# discriminated union ) typed union (Algol united mode , Core union , C# nominal type union ) Untagged Unions Some languages like C, C++ or Rust provide untagged unions, where the chosen…
static members properties ( see ) <> for generics ( see ) [] for arrays ( see ) Type ident instead of ident: Type ( see ) having if-then-else and switch/case and a ternary operator ( see ) having both modifiers and annotations ( see ) async / await separate namespaces for methods and fields method overloading namespace declarations doubling as imports special syntax for casting using cast syntax…
TL;DR: Use [] instead of <> for generics. It will save you a lot of avoidable trouble down the road. 1. <> is hard to read for humans < and > are usually already used as comparison and bitshift operators, which (as binary operators) conform to a completely different grammatical structure compared to their use as brackets. This, often in combination with other design mistakes – like the use of both…
A smaller language, not a bigger one namespaces: types, terms, packages , fields , methods , labels modifiers: keywords , annotations nesting: packages , modules, static members: fields, methods, properties control flow: if-then-else , return, while, break , continue , loop , exceptions , throw , catch constructors: primary, secondary literals: octal number literals , class literals , ……
TL;DR: Use methods. Many languages provide binary operators, usually for operations on numbers (addition, multiplication), bits (shifts) and boolean values. In general, this language facility has been overused, forcing users to learn and recall precedence and associativity of dozens of operators. Additionally, some popular operators have additional problems: The problem with & Many older language…
TL;DR: Unary operators are a waste of a language’s complexity budget. Replace them with methods. Many languages provide unary prefix operators – symbols placed in front of the value they apply to – such as: ! : Logical complement (on booleans) ~ : Bitwise complement (on numbers) - : Numeric complement (on numbers) + : useless (on numbers) Except for reasons of tradition and familiarity, their…
As a first approximation – especially if an existing language shall be adapted – it makes sense to build a feature-reduced version of unified condition expressions using a different keyword, in parallel to existing syntax. After unified condition expressions have gained sufficient maturity and functionality, they can then be switched over to the “real” keyword, old implementations of ternary…
Basic goals, as mentioned in the previous parts: You should not lose values inside a data structure. Here is the simple example again, demonstrating the issue: elem (0.0/0.0) [0.0/0.0] -- False To be clear, elem is picked as the simplest example possible. 1 Status Quo Why is Eq not doing its job, or rather – what is its job description in the first place? According to Data.Eq , not much: The…
Similarly to equality and identity , most languages have severely restricted facilities to handle distinct ordering relationships like comparison and sorting. Languages usually provide only a single operation/protocol, often requiring workarounds for some data types in which the comparison operation and the sorting operation return distinct results. Consider the following Comparable trait as it…
6 letters namespacing – declaring and managing namespaces: module (unifies “object” and “package”) import export 5 letters “big” definitions (types): class (reference type) value (value type, alternative to struct ) union (alternative to enum ) trait (interface/typeclass) alias (type alias) mixin 4 letters control flow: case / then / else or when / then / else loop (alternative to while ) skip…
Function Name Code Example Explanation to array.toList int32Value.toFloat64 dictionary.to[Queue] implies a (potentially lossy) conversion of a value result type might use Option or Result types to encode failures as int64Value.asFloat64 int64Value.as[Float64] stringBuffer.asByteBuffer map.asSetOfEntries setOfEntries.asMap implies a verbatim reinterpretation/wrapping/viewing of a value replacement…
Function Name Code Example Explanation – List(1, 2, 3) Array(12.3, 45.6) Set("a", "b", "c") primary way of construction resulting instance contains provided arguments verbatim of(val1, ...) Person.of(name, age) secondary way of construction resulting instance contains provided arguments verbatim result type might use Option or Result types to encode failures from(val) Person.from(personEntity)…
A reasonable question that might be asked is whether this design can be extended to also handle thrown exceptions, and whether such an extension could completely replace the try-catch-finally idiom. One language that has done something similar is Ocaml, which has extended its pattern matching syntax/semantics . One option might be something along the lines of if readColorFromUri(location) //…
Rust’s Into Rust’s std::convert::Into let’s you define conversion functions for structs like Person : pub struct Person { name: String } impl Into<String> for Person { fn into(self: Person) -> String { self.name } } This way functions can be defined that don’t e. g. require a String , but accept anything that can be converted into a string. The conversion then happens with an explicit call to…
Idea Replace the different syntactic forms of if statements/expressions switch on values match on patterns and pattern guards if - let constructs with a single, unified condition expression that scales from simple one-liners to complex pattern matches. Motivation Cut the different syntax options down to a single one that is still easily recognizable by users. Allow the design to scale seamlessly…
Scala has the concept of package objects which allow the declaration of methods, classes etc. that appear as if they existed directly inside a package, not enclosed in an object or a class. Given the definition of a method qux for a package foo.bar , the method can be called with foo.bar.qux() . The Issue Package objects are useful, but the way they are defined is pretty weird, and one of the…
Most languages have a notion of equality comparisons based on value equality . Many of them also provide a more restricted equality comparison that works only on references, often called reference equality . Here are a few examples: Java == implements reference equality on reference types. Object.equals and Objects.equals implement reference equality by default, but can be overridden to implement…
Containment The core issue is that equality on its own is insufficient to implement some, rather mundane, algorithms. Asking the simple question “is some element contained in this data structure” in different languages demonstrates the problem: Java: List.of(Float.NaN).contains(Float.NaN); // true JavaScript: [NaN].includes(NaN) // true Rust: &[0.0/0.0].contains(0.0/0.0) // false C#:…
Let’s take a step back and think about what these equality operations do: Reference equality acts as a built-in, hard-coded comparison of the references themselves. Value equality compares equality according to a user-defined implementation. Is it possible to re-interpret reference equality that retains the existing behavior for reference types, but adds intuitive and useful behavior for value…
(A random grabbag of things that I haven’t managed to include into the main article yet.) Strings don’t offer indexing, because it doesn’t make sense for UTF-8. Correct! But Strings offer slicing … WAT? Inconsistent naming. str and String , Path and PathBuf etc. Closures could be made to look much closer to functions, but somehow aren’t. “associated” functions in trait impls. I’d prefer separating…
Two interconnected design decisions achieve a particularly interesting sweet-spot in language design: The ident: Type syntax allows consistent and straight-forward placement of generics, compared to languages which use Type ident 1 : Generics ( [T] ) always follow the name of a class or a method, both at the definition-site and at the use-site. A clearly defined use of brackets results in a more…
In expressive languages, developers generally need to use fewer temporary variables. This means that in a typical piece of code there are fewer names defined, but those names carry higher importance. The ident: Type syntax lets developers focus on the name by placing it ahead of its type annotation. This means that the vertical offset of names stays consistent, regardless of a type annotation’s…
As Rust 2.0 is not going to happen, Rust users will never get these language design fixes: 1 2 3 4 A note on the lower bar of a hypothetical Rust 2.0 An article touching on "Rust 2.0" and its reactionary reception made it apparent that language evolution has two boundaries, not one: Boundary 1 (upper bar of change): Things a hypothetical language "v2.0" is not allowed to improve for compatibility…
TL;DR: The desire to make unrelated types act as if they were in a sub-typing relationship, which neither exists nor should exist, combined with syntax sugar that makes static dispatch look like dynamic dispatch creates a perfect storm of unintended, harmful consequences. Implicit numeric conversions 1 are a special compiler feature in Scala that adds “convenience” conversions between number…