diff --git a/_config.yml b/_config.yml index cfca4643..a93078e6 100644 --- a/_config.yml +++ b/_config.yml @@ -18,7 +18,6 @@ email: simon@ochsenreither.de description: > baseurl: "" # the subpath of your site, e.g. /blog url: "" # the base hostname & protocol for your site -twitter_username: oxnrtr github_username: soc plugins: - jekyll-redirect-from @@ -28,18 +27,22 @@ collections: output: true name: "Standards" published: true - hardware: - output: true - name: "Hardware" - published: true languages: output: true name: "Languages" published: true + runtimes: + output: true + name: "Runtimes" + published: true interfaces: output: true name: "Interfaces" published: true + hardware: + output: true + name: "Hardware" + published: true scala: output: true name: "Scala" @@ -56,12 +59,12 @@ defaults: layout: post - scope: - type: hardware + type: languages values: layout: post - scope: - type: languages + type: runtimes values: layout: post - @@ -69,6 +72,11 @@ defaults: type: interfaces values: layout: post + - + scope: + type: hardware + values: + layout: post - scope: type: scala diff --git a/_drafts/the-cost-of-everything.md b/_drafts/the-cost-of-everything.md deleted file mode 100644 index fbf93702..00000000 --- a/_drafts/the-cost-of-everything.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: "The Cost of Everything" -date: 2018-12-30 12:00:00 +0200 ---- - -| Merit Points | Item | Example | -| -----------: | ------------- | ------- | -| -100| Adding a language feature to do something that can already be done | | -| -100| Adding a language feature to do something that can be implemented with a library | | -| -80| Adding a language feature to do something that can be implemented with a macro | | -| -60| Adding a language feature to do something that can be achieved by fixing a compiler bug | | -| -10| Adding a new element to the standard library | | -| -100| Adding a new element to the global namespace | | -| -80| Adding a new element to `util` | | -| +10| Removing an existing element from the standard library while retaining abstraction and functionality | | -| +100| Removing superfluous language syntax | | -| +100| Removing a non-working language feature | | -| +80| Removing a language feature that can be implemented with code | | -| +40| Removing a pointless distinction | | diff --git a/_includes/pagination.html b/_includes/pagination.html index afdc65ca..656d544e 100644 --- a/_includes/pagination.html +++ b/_includes/pagination.html @@ -9,7 +9,7 @@ {% assign previous_url = page.previous.url %} {% assign previous_title = page.previous.title %}
{% endif %}{% endif %} @@ -17,13 +17,13 @@ {% assign next_url = page.page_next_url %} {% assign next_title = page.page_next_title %}|| | Boolean Or |
+| 3 | `&&` | Boolean And |
+| 4 | `==`, `!=`, `<`, `<=`, `>`, `>=`, `===`, `!==` | Comparisons |
+| 5 | `+`, `-`, |, `^` | Addition, Subtraction, Bitwise Or, Bitwise Xor |
+| 6 | `*`, `/`, `&` | Multiplication, Division, Bitwise And |
+{: .table-medium .table-layout-auto }
+
+[^leijen]: [Division and Modulus for Computer Scientists](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/divmodnote-letter.pdf)
+[^boute]: [The Euclidean Definition of the Functions div and mod](https://dl.acm.org/doi/pdf/10.1145/128861.128862)
\ No newline at end of file
diff --git a/_languages/comparing-and-sorting.md b/_languages/comparing-and-sorting.md
index 299e849f..2d1d1169 100644
--- a/_languages/comparing-and-sorting.md
+++ b/_languages/comparing-and-sorting.md
@@ -1,46 +1,55 @@
---
title: "Language Design: Comparing and Sorting"
-date: 2018-10-31 12:00:00 +0200
+date: 2018-10-31
+update: 2022-06-11
---
Similarly to [equality and identity](equality-and-identity-part1), 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:
+Consider the following `Comparable` trait as it frequently exists across many languages
+(like [Haskell](https://hackage.haskell.org/package/base-4.16.1.0/docs/Data-Ord.html),
+[Rust](https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html),
+[Swift](https://developer.apple.com/documentation/swift/comparable), ...):
```ml
trait Comparable[T]
- fun < (that: T): Boolean = ...
- fun > (that: T): Boolean = ...
+ fun < (that: T): Bool
+ fun > (that: T): Bool
+ ...
```
-... and an IEEE754-conformant implementation of `Comparable` for floating point values, such that `-0.0 < +0.0`, and `Float.NaN < Float.PositiveInfinity` are both false.
-
-As it becomes obvious, such an implementation of _partial order_ used to correctly _compare_ values, cannot be used to correctly _sort_ values (_total order_).[^1]
+... and an IEEE754-conformant comparison implementation for floating point values,
+i. e. `-0.0 < +0.0`, and `Float.NaN < Float.PositiveInfinity` are both false.
-Worded differently, an implementation of _comparison_ operations for floating point values cannot be used as a stand-in for _sorting_ operations on floating point values.[^2]
+As it becomes obvious, such an implementation of _partial order_ can be used to _compare_ values,
+but cannot be used to correctly _sort_ values (_total order_).[^1]
-Conveniently, IEEE754 standardizes a `totalOrder` relation in §5.10, defining how floating point numbers should be sorted.
-The only requirement language-wise is to introduce a distinct trait which represents _total ordering_, enabling a clean separation of _comparisons_ and _sorting_ operations:
+The reason is that the implementation of _comparison operations_ for floating point values (a partial order, IEEE754 §5.11)
+cannot be used as a stand-in for _sorting_ operations on floating point values.
+Conveniently, IEEE754 standardizes a `totalOrder` relation in §5.10, defining how floating point numbers are sorted.
+The only requirement language-wise is to introduce a distinct trait[^2] which represents _total ordering_,
+enabling a clean separation of _comparison operations_ from _sorting operations_:
```ml
trait Sortable[T]
- fun sortsBefore(that: T): Boolean = ...
- fun sortsAfter (that: T): Boolean = ...
+ fun sortsBefore(that: T): Bool
+ fun sortsAfter (that: T): Bool
+ ...
```
This enables the use of each individual trait for its specific purpose, without conflating different concerns:
```ml
-// compare values using Comparable
+// example of comparing values using Comparable
fun compareReversed[T : Comparable](x: T, y: T) = y < x
-// sort values using Sortable
+// example of sorting values using Sortable
fun sort[T : Sortable](values: Array[T]) =
...
- x sortsBefore y
+ if values(i).sortsBefore(values(j)) { ... }
...
```
diff --git a/_languages/consistent-keyword-length.md b/_languages/consistent-keyword-length.md
new file mode 100644
index 00000000..6d56458e
--- /dev/null
+++ b/_languages/consistent-keyword-length.md
@@ -0,0 +1,47 @@
+---
+title: "Language Design: Use Consistent Keyword Length"
+date: 2018-09-07
+update: 2023-09-16
+---
+
+**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` (alternative to `continue`)
+- `exit` (alternative to `return`)
+- `yeet` (alternative to `throw`)
+
+**3 letters** "small" definitions (members):
+
+- `fun` (function)
+- `let` (immutable binding)
+- `var` (mutable binding)
+
+---
+
+Unused alternatives:
+
+**6 letters** "invasive" control flow:
+
+- `return`
+- `throws`
+
+**2 letters** control flow:
+
+- `if`/`do`/`or`
diff --git a/_languages/notes-on-rust.md b/_languages/design-mistakes-in-rust.md
similarity index 97%
rename from _languages/notes-on-rust.md
rename to _languages/design-mistakes-in-rust.md
index 5f8430d5..6fb66065 100644
--- a/_languages/notes-on-rust.md
+++ b/_languages/design-mistakes-in-rust.md
@@ -1,6 +1,7 @@
---
-title: "Language Design: Notes on Rust"
+title: "Language Design: Mistakes in Rust"
date: 2017-07-30 12:00:00 +0200
+redirect_from: "languages/notes-on-rust"
---
It's an impressive language, but the user interface needs a lot of work.
diff --git a/_languages/drop-break-and-continue.md b/_languages/drop-break-and-continue.md
new file mode 100644
index 00000000..f2328ec3
--- /dev/null
+++ b/_languages/drop-break-and-continue.md
@@ -0,0 +1,74 @@
+---
+title: "Language Design: Drop `break` and `continue`"
+date: 2022-12-10
+markdeep: true
+---
+
+_**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 loops aren't using `break` or `continue`.
+
+Of the remaining 100 loops, perhaps 90 loops use `break`, and 10 loops use `continue`.
+
+Of those 90 loops with `break`s, 80 are easily convertible to equivalent code not using `break`.
+
+Of the 10 loops with `continue`s maybe 5 are easily convertible.
+
+Boundary 1 (upper bar of change): Things a hypothetical language "v2.0" is not allowed to improve for compatibility reasons.
+Boundary 2 (lower bar of change): Things that a hypothetical language "v2.0" needs to improve for such an effort to be worthwhile to contributors and users.
+| Name | -Example | +Name | +Example | Explanation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
to |
-
|
+ array.toList +int32Value.toFloat64 +dictionary.to[Queue] |
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
as |
-
|
+ int64Value.asFloat64 +int64Value.as[Float64] +stringBuffer.asByteBuffer +map.asSetOfEntries +setOfEntries.asMap |
|
| Name | -Example | +Name | +Example | Explanation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| – | -
|
+ List(1, 2, 3) +Array(12.3, 45.6) +Set("a", "b", "c") |
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
of |
- Person.of(name, age) |
+ of(val1, ...) |
+ Person.of(name, age) |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from |
-
|
+ from(val) |
+ Person.from(personEntity) +Person.from(family) |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
parse |
-
|
+ parse(string) |
+ Person.parse(string) +Int64.parse(string) |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
with |
-
|
+ with(val) |
+ person.withAge(23) +person.with(age = 23) |
+ +--- + +Naming scheme: + +- `...Else...` indicates going from `Option[T]` to `T` +- `...Get` indicates a closure argument +- all panicking methods contain `...Panic` diff --git a/_languages/naming-conventions-stream.md b/_languages/naming-conventions-stream.md deleted file mode 100644 index 7a594ee3..00000000 --- a/_languages/naming-conventions-stream.md +++ /dev/null @@ -1,263 +0,0 @@ ---- -title: "Language Design: Naming Conventions – Part 4: Stream" -date: 2022-06-07 12:00:00 +0200 ---- - -#### Mapping - -
+> `()` _groups_ expressions, parameter/argument lists or tuples +> `{}` _sequences_ statements or definitions + +... and `<`/`>` is only used as a comparison operator, and not misused as a makeshift bracket. + +This substantially simplifies the mental model beginners need to adopt before writing their first program +(_"`()` is for values, `[]` is for types"_), and encourages the elimination of syntactic special cases like collection literals ... + +``` +Array(1, 2, 3) /* instead of */ [ 1, 2, 3 ] +Set("a", "b", "c") /* instead of */ { "a", "b", "c" } +``` + +... and array indexing in favor of standard function call syntax[^nim]: ``` -Array.get(1, 2, 3) /* instead of */ [1, 2, 3] someList.get(0) /* instead of */ someList[0] array.set(0, 23.42) /* instead of */ array[0] = 23.42 map.set("name", "Joe") /* instead of */ map["name"] = "Joe" ``` -At this stage, some small amount of syntax sugar can be considered that would allow every type with -a `get` method to be written as `instance(arg)` and a `set` method written as `instance(index, arg)`, -leading to the following code:[^pythonscala] +A [small amount of syntax sugar](useful-syntax-sugar) can be considered, leading to the following code:[^pythonscala] ``` -Array(1, 2, 3) /* instead of */ [1, 2, 3] someList(0) /* instead of */ someList[0] array(0) = 23.42 /* instead of */ array[0] = 23.42 map("name") = "Joe" /* instead of */ map["name"] = "Joe" @@ -88,15 +99,19 @@ map("name") = "Joe" /* instead of */ map["name"] = "Joe" #### Coda -Thankfully, the number of languages using `[]` for generics seems to increase lately – with Scala, Python, Nim and Go joining Eiffel, which was pretty much the sole user of `[]` for decades. +Thankfully, the number of languages using `[]` for generics seems to increase lately – +with Scala, Python, and Nim joining Eiffel, which was pretty much the sole user of `[]` for decades. -It remains to be seen whether this turns into tidal change similar to the widespread [adoption of `ident: Type` over `Type ident`](https://soc.me/languages/type-annotations) in modern languages. +~~It remains to be seen whether this turns into tidal change similar to the widespread +[adoption of `ident: Type` over `Type ident`](https://soc.me/languages/type-annotations) in modern languages.~~ +_With the recent adoption of `[]` for generics by Go and Carbon this seems to be the likely outcome._ [^related]: [Parsing Ambiguity: Type Argument v. Less Than](https://keleshev.com/parsing-ambiguity-type-argument-v-less-than) is a similar article focusing on some of these issues in more depth. [^java]: Java: The syntax inconsistency is due to the difficulty a compiler would have to tell whether some token stream of `instance` `.` `foo` `<` is the left side of a comparison (with `<` being the "less-than" operator) or the start of a generic type argument within a method call. [^csharp]: C#: See [ECMA-334, 4th Edition, §9.2.3 – Grammar Ambiguities](https://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf) -[^cpp]: C++: See [Wikipedia – C++11 right angle bracket](https://en.wikipedia.org/wiki/C%2B%2B11#Right_angle_bracket) +[^cpp1]: C++: See [What are all the syntax problems introduced by the usage of angle brackets in C++ templates?](https://stackoverflow.com/questions/7304699/what-are-all-the-syntax-problems-introduced-by-the-usage-of-angle-brackets-in-c) +[^cpp2]: C++: See [Wikipedia – C++11 right angle bracket](https://en.wikipedia.org/wiki/C%2B%2B11#Right_angle_bracket) [^javalit]: Java pretty much abandoned arrays – they never integrated them with collections in 1.2, let alone generics in 1.5. [^jslit]: JavaScript stopped giving out new collection literals almost immediately after its first release – no collection type added since received its own literals (`Set`, `Map`, `ByteBuffer`, ...). [^nim]: Nim uses `[]` for generics, but employs [a hack to _also_ use `[]` for lookup](https://nim-lang.org/docs/manual.html#procedures-method-call-syntax). diff --git a/_languages/the-cost-of-everything.md b/_languages/the-cost-of-everything.md new file mode 100644 index 00000000..4bd84fd6 --- /dev/null +++ b/_languages/the-cost-of-everything.md @@ -0,0 +1,13 @@ +--- +title: "Language Design: The Cost of Everything" +date: 2022-07-07 13:00:00 +0200 +--- + +| 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 library | diff --git a/_languages/threads-futures-async.md b/_languages/threads-futures-async.md index 4933241e..e9b670dc 100644 --- a/_languages/threads-futures-async.md +++ b/_languages/threads-futures-async.md @@ -1,4 +1,5 @@ --- title: "Language Design: Threads, Futures and Async" date: 2022-06-05 12:00:00 +0200 +published: false --- diff --git a/_languages/type-annotations.md b/_languages/type-annotations.md index d626da52..330fb9ca 100644 --- a/_languages/type-annotations.md +++ b/_languages/type-annotations.md @@ -1,6 +1,7 @@ --- title: "Language Design: Use `ident: Type`, not `Type ident`" -date: 2017-07-21 12:00:00 +0200 +date: 2017-07-21 +update: 2022-08-21 redirect_from: "/articles/language-design/type-annotations" --- @@ -10,8 +11,7 @@ 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 whether a type -annotation is present (and how long it is) or not[^type-inference]: +This means that the vertical offset of names stays consistent, regardless of a type annotation's presence or absence[^type-inference]: ```scala let x: String = "hello" @@ -63,7 +63,7 @@ the three properties mentioned above: **C#** -```csharp +```java T id consistency | Definition before usage | +| | Input before output | Definition/usage consistency | Definition before usage | |--------------|:-------------------:|:----------------------------:|:-----------------------:| -| ***Java*** | No | No | Yes | -| ***C#*** | No | Yes | No | -| ***Kotlin*** | Yes | No | Yes | -| ***Ceylon*** | No | Yes | No | -| ***Scala*** | Yes | Yes | Yes | -{: style="width:100%"} +| ***Java*** | ❌ | ❌ | ✅ | +| ***C#*** | ❌ | ✅ | ❌ | +| ***Kotlin*** | ✅ | ❌ | ✅ | +| ***Ceylon*** | ❌ | ✅ | ❌ | +| ***Core*** | ✅ | ✅ | ✅ | +{: .table-medium .table-layout-auto} [^type-inference]: type inference means that the compiler can figure out types without having a developer writing them down explicitly [^curly]: focusing on curly-brace languages here, as languages like Haskell, ML and OCaml, Idris have slightly different design optima diff --git a/_languages/typing-terminology.md b/_languages/typing-terminology.md new file mode 100644 index 00000000..0a6fdc42 --- /dev/null +++ b/_languages/typing-terminology.md @@ -0,0 +1,54 @@ +--- +title: "Language Design: Typing Terminology" +date: 2022-08-06 +--- + +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. + +Most individual distinction are a scale, not a strict yes/no checkbox. + +--- + +## Static ⟷ Dynamic (Typing Modality/Presence) + +It refers to what mode of the program it exists in, the analysis stage (where you get e.g. syntax errors too) or the execution stage. More Static: Haskell, CommonLisp, More Dynamic: SmallTalk, Scheme + +### Manifest ⟷ Inferred (Typing Apparency) + +It describes the degree to which types need to mentioned in the program text. + +More Manifest: Java, C, More Inferred: Python, Haskell. + +### Nominal ⟷ Structural (Typing Morphology) + +It pertains to how types are described and referred to and when they are judged equal. + +More Nominal: Rust, D, More Structural: Ruby, OCaml. + +→ Mention Java SAM types. + +### Reified ⟷ Erased (Typing Preservation) + +reification: runtime- vs. user-exposed? + +### Compile-time vs. Run-time Reflection? + +Not directly typing related, but typing preservation choices have a direct impact on what's possible. +(Maybe as sub-point of reified vs. erased?) + +## Untyped Languages + +### Tagged ⟷ Untagged + +--- + +→ cite Benjamin Pierce +→ cite Bob Harper + +--- + +Not really typing: + +## Strong ⟷ Weak (Typing Discipline/Value Convertibility) + +which I think should be called Typing discipline because it pertains to the number of type errors you get (compile time or runtime, doesn't matter.) Stronger: SML, Python, Weaker: JavaScript, C. diff --git a/_languages/unary-operators.md b/_languages/unary-operators-are-unnecessary.md similarity index 52% rename from _languages/unary-operators.md rename to _languages/unary-operators-are-unnecessary.md index 2d4d329b..ac71ce18 100644 --- a/_languages/unary-operators.md +++ b/_languages/unary-operators-are-unnecessary.md @@ -1,8 +1,12 @@ --- -title: "Language Design: Unary Operators" -date: 2019-09-21 12:00:00 +0200 +title: "Language Design: Unary Operators are Unnecessary" +date: 2019-09-21 +update: 2022-07-17 +redirect_from: "/languages/unary-operators" --- +_**TL;DR:** Use methods._ + Many languages provide unary operators, usually written as a prefix to the value they apply to. The most common unary operators are: @@ -13,23 +17,30 @@ The most common unary operators are: - `+`: useless (on numbers) Except for reasons of tradition and familiarity, their privileged position in many languages is unnecessary. -Considering they provide rather limited benefits – while adding complexity to the core language – -it is questionable whether unary operators are a good place to spend a language's complexity budget on. +They provide rather limited benefits – while adding complexity to the core language. + +Unary operators are a waste of a language's complexity budget. An alternative is to define methods on the respective types, dropping unary operators altogether: -- `not` replaces `!` on booleans -- `not` replaces `~` on numbers -- `negate` replaces `-` on numbers +- `not` replaces `!` on booleans: `someBool.not` instead of `!someBool` +- `not` replaces `~` on integers: `1.not` instead of `~1` +- `negate` replaces `-` on numbers: `1.negate` instead of `-1` This also elegantly solves the question whether -```scala +```ml let x = 1 -x.abs ``` -evaluates to `1` or `-1`, by requiring users to write `x.negate.abs` – thereby leaving no ambiguity to precedence. +should evaluate to `1` or `-1`, as + +```ml +x.negate.abs +``` + +is completely unambiguous. There are two additional benefits to the use methods instead of operators: @@ -43,5 +54,27 @@ There are two additional benefits to the use methods instead of operators: the same operation on the more common fixed-size types (`Int`, `Long`, ...) could benefit from returning an optional result to indicate that a negative value may lack a positive counterpart. +--- + +#### Appendix + +Incomplete list of languages and their interpretation of `-1.abs`: + +| | -1.abs | let x = 1; -x.abs | +|------------:|-------:|------------------:| +| C# | -1 | -1 | +| D | -1 | -1 | +| Dart | -1 | -1 | +| Fantom | -1 | -1 | +| Groovy | -1 | -1 | +| Kitten | 1 | n.a. | +| JavaScript | -1 | -1 | +| Nim | -1 | -1 | +| Raku | -1 | -1 | +| Ruby | 1 | -1 | +| Rust | -1 | -1 | +| Scala | 1 | -1 | +| Smalltalk | 1 | n.a. | +{: .table-medium .table-width-small} -[^1]: The Rust community had a similar [discussion](https://internals.rust-lang.org/t/the-is-not-empty-method-as-more-clearly-alternative-for-is-empty/) about this topic. \ No newline at end of file +[^1]: The Rust community had a similar [discussion](https://internals.rust-lang.org/t/the-is-not-empty-method-as-more-clearly-alternative-for-is-empty/) about this topic. diff --git a/_languages/unified-condition-expressions-comparison.md b/_languages/unified-condition-expressions-comparison.md new file mode 100644 index 00000000..3e5298a5 --- /dev/null +++ b/_languages/unified-condition-expressions-comparison.md @@ -0,0 +1,145 @@ +--- +title: "Language Design: Unified Condition Expressions – Comparison with Rust" +date: 2022-11-07 +page_previous_title: "Unified Condition Expressions – Exceptions" +page_previous_url: "unified-condition-expressions-exceptions" +--- + +##### simple if expression + +```ml +if x == 1.0 { "a" } +else { "z" } +``` + +This translates straight-forward to Rust: + +```rust +if x == 1.0 { "a" } +else { "z" } +``` + +##### multiple cases, equality relation + +```ml +if x +... == 1.0 { "a" } +... == 2.0 { "b" } +else { "z" } +``` + +In Rust, using `match` is idiomatic: + +```rust +match x { + 1.0 => "a", + 2.0 => "b", + _ => "z" +} +``` + +##### multiple cases, any other relation + +```ml +if x +... == 1.0 { "a" } +... != 2.0 { "b" } +else { "z" } +``` + +Rust requires the use `match` with guards (`match` on its own only supports equality relations), or an `if` expression: + +```rust +match x { + 1.0 => "a" if x == 1.0 { "a" } + x if x != 2.0 => "b" else if x != 2.0 { "b" } + _ => "z" else { "z" } +``` + +##### multiple cases, method calls + +```ml +if x +... .isInfinite { "a" } +... .isNaN { "b" } +else { "z" } +``` + +In Rust one would use `match` with guards, or an `if` expression: + +```rust +match x { + x if x.is_infinite() => "a" if x.is_infinite() { "a" } + x if x.is_nan() => "b" else if x.is_nan() { "b" } + _ => "z" else { "z" } +} +``` + +##### "if-let", statement[^rust-if-let][^swift-if-let] + +```ml +if opt_number is Some(i) { /* use `i` */ } +``` + +Rust requires a special construct to pattern match or introduce bindings: + +```rust +if let Some(i) = opt_number { /* use `i` */ } +``` + +##### "if-let", expression[^rust-if-let][^swift-if-let] + +```ml +let result = if opt_number + is Some(i) { i } + else { 0 } +``` + +Rust uses the `let-equals-if-let-equals` pattern: + +```rust +let result = if let Some(i) = opt_number { + i +} else { + 0 +} +``` + +##### "if-let" chains[^rust-if-let-chains] + +```ml +let result = if opt_number.contains(1.0) { 1.0 } else { 0 } +``` + +Rust proposes the `if-let` chains syntax: + +```rust +let result = if let Some(i) && i == 1.0 = opt_number { + i +} else { + 0 +} +``` + +##### "let-else"[^rust-let-else][^swift-guard-let] + +```ml +let i = if opt_number + is Some(i) { i } + else { return 0 } +``` + +Rust's `let-else` allows binding a fallible pattern without introducing nesting: + +```rust +let Some(i) = opt_number else { + return 0; +}; +``` + + +[^rust-if-let]: Rust `if-let` – https://doc.rust-lang.org/book/second-edition/ch06-03-if-let.html +[^swift-if-let]: Swift `if-let` – https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/OptionalChaining.html +[^rust-if-let-chains]: Rust `if-let` chains – https://github.com/rust-lang/rust/issues/53667 +[^rust-let-else]: Rust `let-else` – https://blog.rust-lang.org/2022/11/03/Rust-1.65.0.html#let-else-statements +[^swift-guard-let]: Swift `guard-let` – https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html diff --git a/_languages/unified-condition-expressions-exceptions.md b/_languages/unified-condition-expressions-exceptions.md index 11eaabb8..3a1b2df3 100644 --- a/_languages/unified-condition-expressions-exceptions.md +++ b/_languages/unified-condition-expressions-exceptions.md @@ -1,7 +1,12 @@ --- title: "Language Design: Unified Condition Expressions – Exceptions" -date: 2018-04-28 12:00:00 +0200 +date: 2018-04-28 +update: 2022-06-24 redirect_from: "/languages/unified-condition-syntax-advanced" +page_previous_title: "Unified Condition Expressions – Implementation" +page_previous_url: "unified-condition-expressions-implementation" +page_next_title: "Unified Condition Expressions – Comparison with Rust" +page_next_url: "unified-condition-expressions-comparison" --- A reasonable question that might be asked is whether this design can be extended to also handle thrown exceptions, @@ -25,4 +30,3 @@ depending on the expressiveness of the core language. Considering the costs and the complexity involved, it may be a better approach to simply drop exceptions from the design of the language and do without this additional layer of control flow. - \ No newline at end of file diff --git a/_languages/unified-condition-expressions-implementation.md b/_languages/unified-condition-expressions-implementation.md index f1cf8a19..5feb57e2 100644 --- a/_languages/unified-condition-expressions-implementation.md +++ b/_languages/unified-condition-expressions-implementation.md @@ -1,7 +1,12 @@ --- title: "Language Design: Unified Condition Expressions – Implementation" -date: 2019-09-21 12:00:00 +0200 +date: 2019-09-21 +update: 2024-04-01 redirect_from: "/languages/unified-condition-expressions-parsing" +page_previous_title: "Unified Condition Expressions – Introduction" +page_previous_url: "unified-condition-expressions" +page_next_title: "Unified Condition Expressions – Exceptions" +page_next_url: "unified-condition-expressions-exceptions" --- #### How to Parse? @@ -11,17 +16,17 @@ it makes sense to build a feature-reduced version of unified condition expressio 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, and any old implementations of -ternary operators, switch-cases or if-expressions can be removed. +they can then be switched over to the "real" keyword, old implementations of ternary operators, +switch-cases or if-expressions can be removed and their uses migrated to unified condition expressions. ##### Level 1: Basics ```ml -case // separate keyword - person .. // `..` to indicate end of common condition fragment - == john { true } - == jane { true } +if person + // `...` to indicate start of individual condition fragment + ... == john { true } + ... == jane { true } else false ``` @@ -33,65 +38,90 @@ fragment with each individual branch has to be taken into account, but the common fragment has to be retained until code-generation. -##### Level 2: Partial Conditions +##### Level 2: Pattern Matching + +The core insight is that pattern matching occurs either always (`switch`&`case`, `match`&`case`) or never +(`if`&`then`&`else`, `?`&`:`) with "legacy" approaches. + +With unified condition expressions, this choice can be made for each branch individually, using the `is` keyword: ```ml -case person == .. // partial common condition fragment - john { true } - jane { true } +if person + ... is Person("john", _, 42) { true } // paternn match + ... .age > 23 { false } // no pattern match else false ``` -At level 2, the notion of the condition's common fragment is made more flexible: -Now the common fragment can be partial; i. e. the common fragment may not be a valid -expression on its own. +##### Level 3: Bindings -The challenge here is how such code can be expressed best in the AST. +The main design task is picking a convention/rule that decides whether an identifier inside a pattern match introduces a +new binding with that name, or refers to an existing binding of that name in scope. +Possible design options include ... -##### Level 3: Partial Branches +1. ... using a keyword or symbol (for instance `let` or `@`) to introduce bindings in patterns: -```ml -case person .. - .firstName == "john" { true } - .age + 23 > jane.age { true } -else false -``` + ```ml + let age = 43 + if person + // refers to the `age` binding defined earlier + ... is Person("john", "miller", age) { age.toString } + // `let` introduces a new binding for jane's last name + ... is Person("jane", let lastName, 23) { lastName } + else false + ``` -At this stage the focus is on checking and ensuring that the syntax introduced -at level 1 is supporting the whole language, and is not special-cased, e. g. -to binary comparison operators. +2. ... using a keyword or symbol (for instance `$`) to reference existing bindings in scope: -Level 3 requires introducing indentation-based syntax. -Depending on how complex the rest of the language is, this can be a rather big leap. + ```ml + let age = 43 + if person + // `$` refers to the `age` binding defined earlier + ... is Person("john", "miller", $age) { age.toString } + // introduces a new binding for jane's last name + ... is Person("jane", lastName, 23) { lastName } + else false + ``` +3. ... using casing rules to distinguish bindings from references: -##### Level 4: Pattern Matching + ```ml + let Age = 43 + if person + // uppercase refers to the `Age` binding defined earlier + ... is Person("john", "miller", Age) { age.toString } + // lowercase introduce a new binding for jane's last name + ... is Person("jane", lastName, 23) { lastName } + else false + ``` -```ml -case person is .. - Person("john", _, 42) { true } - Person("jane", "smith", _) { true } -else false -``` +##### Optional: Partial Conditions +The notion of the condition's common fragment can be made more flexible: -##### Level 5: Bindings +The common fragment can be partial; i. e. the common fragment may not be a valid expression on its own: ```ml -case person is .. - Person("john", "miller", $age) { age.toString } // introduce binding for john's age - Person("jane", $lastName, 23) { lastName } // ... and jane's last name +if person == // partial common condition fragment + ... john { true } + ... jane { true } else false ``` +The challenge here is how such code can be expressed best in the AST. + + +##### Optional: Indentation-based syntax + +Introducing an indentation-based syntax allows dropping `...` from the unified condition syntax +without introducing problems in other places. -##### (Optional) Unified condition expressions: Indentation +Similarly, `{}` could be replaced with `then`. ```ml -case person == // no `..` needed to indicate end of common condition fragment - john then true // {} has been replaced with then +if person == // no `...` needed to indicate end of common condition fragment + john then true // optional: replace `{}` with `then` jane then true else false ``` diff --git a/_languages/unified-condition-expressions.md b/_languages/unified-condition-expressions.md index 01f4fdfc..5f726073 100644 --- a/_languages/unified-condition-expressions.md +++ b/_languages/unified-condition-expressions.md @@ -1,7 +1,10 @@ --- title: "Language Design: Unified Condition Expressions – Introduction" -date: 2018-01-21 12:00:00 +0200 +date: 2018-01-21 +update: 2024-04-01 redirect_from: "/languages/unified-condition-syntax" +page_next_title: "Unified Condition Expressions – Implementation" +page_next_url: "unified-condition-expressions-implementation" --- #### Idea @@ -16,17 +19,16 @@ with a single, unified condition expression that scales from simple one-liners t #### Motivation -The intention is to cut the different syntax options down to a single one that is still easily recognizable by users, -not to minimize keywords (i. e. `a == b ? c : d`) or turn conditions into methods (like Smalltalk). +- Cut the different syntax options down to a single one that is still easily recognizable by users. +- Make this design scale seamlessly from simple cases to complicated ones. -#### Principles + Minimizing the number of keywords or turning condition syntax into method calls (like Smalltalk) are non-goals. -- The condition can be split between a common _discriminator_ and individual cases. - - This requires doing away with mandatory parentheses around the conditions. - - This strongly suggests using a keyword (`then`) to introduce branches, instead of using curly braces, - based on readability considerations. -- The keyword `if` is chosen over other options like `match`, `when`, `switch` or `case` - because it is keyword the largest number of developers are familiar with. +#### Considerations + +- The condition can be split between a common _discriminator_ and individual cases. + This requires doing away with mandatory parentheses around conditions. +- `if` has been chosen in code examples as the primary keyword, other reasonable keyword choices are `match`, `when`, `switch` or `case`. #### Examples @@ -44,10 +46,10 @@ else "z" ##### one comparison operator on multiple targets ```ml -if x == if x /* same as */ - 1.0 then "a" == 1.0 then "a" if x == 1.0 then "a" - 2.0 then "b" == 2.0 then "b" else if x == 2.0 then "b" - else "z" else "z" else "z" +if x == /* same as */ if x /* same as */ + 1.0 then "a" == 1.0 then "a" if x == 1.0 then "a" + 2.0 then "b" == 2.0 then "b" else if x == 2.0 then "b" + else "z" else "z" else "z" ``` ##### different comparison operators, equality and identity @@ -66,26 +68,26 @@ if xs /* same as */ else "z" else "z" ``` -##### pattern matching (`is`), introducing bindings (`$`) +##### pattern matching (`is`), introducing bindings, flow typing ```ml if alice - .age < 18 then "18" - is Person("Alice", $age) then "$age" - is Person("Bob", _)$person then "{$person.age}" - else "0" + .age < 18 then "18" + is Person("Alice", _) then "{$person.age}" + is Person("Bob", let age) then "$age" + else "0" ``` ##### pattern matching using "if-let"[^rust][^swift] ```ml -if person is Person("Alice", $age) then "$age" else "o" +if person is Person("Alice", let age) then "$age" else "o" ``` ##### wildcards (`_`) and pattern guards ```ml -if person /* same as */ if person is - is Person("Alice", _) then "alice" Person("Alice", _) then "alice" - is Person(_, $age) && age >= 18 then "adult" Person(_, $age) && age >= 18 then "adult" - else "minor" else "minor" +if person /* same as */ if person is + is Person("Alice", _) then "alice" Person("Alice", _) then "alice" + is Person(_, let age) && age >= 18 then "adult" Person(_, let age) && age >= 18 then "adult" + else "minor" else "minor" ``` #### Related Work diff --git a/_languages/unions.md b/_languages/unions.md new file mode 100644 index 00000000..5c834e3c --- /dev/null +++ b/_languages/unions.md @@ -0,0 +1,164 @@ +--- +title: "Language Design: Unions" +date: 2021-08-26 +update: 2022-11-26 +redirect_from: "/languages/better-enums" +redirect_from: "/languages/nondefinitional-enums" +--- + +_**TL;DR:** Tagged unions whose variants do not require syntactic wrappers._ + +### Introduction + +A "traditional" enum (ADT) definition as it exists in various languages defines both the enum itself +(`Pet`), as well as its variants (`Cat` and `Dog`): + + enum Pet { + Cat(name: String, lives: Int), + Dog(name: String, age: Int) + } + let pet: Pet = Cat("Molly", 9) + +Some languages like Rust, C or C++ provide untagged unions, where the chosen variant has to be specified on creation and access: + + union Pet { + cat: Cat, + dog: Dog + } + let pet = Pet { cat: Cat("Molly", 9) } + +Other languages provide untagged union types where the union type itself (`Pet`) is defined, +and its variants (`Cat` and `Dog`) refer to existing types in scope that may or may not allow detecting the chosen variant[^untagged-unions]: + + type Pet = Cat | Dog + let pet: Pet = Cat("Molly", 9) + +#### Observation + +- ADTs are generally tagged unions (their variants can be told apart, even if they contain the same values) +and come with wrappers (`Cat`, `Dog`) around their payloads. +- Untagged unions do not contain metadata (runtime tags) to distinguish variants, but require that every access is qualified with variant information. +- Union types do not contain metadata (runtime tags) to distinguish variants and do not use syntactic wrappers. + +
+ (This fixes a mistake that some languages like Rust or Haskell made with their enums/ADTs.[^enum-variants-1][^enum-variants-2]) +2. Variants can be reference types or value types (as they refer to "real" `class` or `value` definitions). +3. No "stutter", where variant names have to be invented to wrap existing types. (Rust has this issue.) +4. Union values can be passed/created more easily, as no syntactic wrapping is required. +5. Variants can be re-used in different unions. +6. The ability to build ad-hoc unions out of existing types obviates the need for a separate type alias feature. + +--- + +#### Example for 1., 2., 3. + + enum Option[T] { Some(value: T), None } + +... would receive little benefit from being written as ... + + union Option[T] of Some[T], None + value Some[T](value: T) + module None + +..., but even trivial ADTs like a JSON representation would benefit. + +Instead of ... + + enum JsonValue { + JsonObject(Map[String, JsonValue]) + JsonArray (Array[JsonValue]), + JsonString(String), + JsonNumber(Float64), + JsonBool (Bool), + JsonNull, + ... + } + +... one would write (with `Array`, `Float64` and `String` being existing types in the language): + + union JsonValue of + Map[String, JsonValue] + Array[JsonValue], + String, + Float64 + Bool, + JsonNull, + ... + + module JsonNull + +#### Example for 4. + +No wrapping required when passing arguments (unlike "traditional" enum approaches): + + fun someValue(value: JsonValue) = ... + someValue(JsonString("test")) // "traditional" approach + someValue("test") // with non-definitional unions + +#### Example for 5. + +Consider this class definition: + + class Name(name: String) + +With non-definitional unions, `Name` can be used multiple times – in different unions (and elsewhere): + + union PersonIdentifier of + Name, + ... // other identifiers like TaxId, Description, PhoneNumber etc. + + union DogTag of + Name, + ... // other identifiers like RegId, ... + +--- + +Non-definitional unions reduce indirection at use-sites and can be used in more scenarios (compared to more "traditional" enums), +while not changing their runtime costs or representation. + +[^untagged-unions]: `type Num = Int | Int` does not allow detecting whether an `Int` instance is the first or the second variant; the definition is equivalent to `type Num = Int` +[^sealed]: Unlike sealed interfaces in Java though, `Cat` and `Dog` are not subtypes of `Pet` in non-definitional unions. +[^enum-variants-1]: [Types for enum variants](https://github.com/rust-lang/rfcs/pull/1450) +[^enum-variants-2]: [Enum variant types](https://github.com/rust-lang/rfcs/pull/2593) diff --git a/_languages/useful-syntax-sugar.md b/_languages/useful-syntax-sugar.md new file mode 100644 index 00000000..260f1db4 --- /dev/null +++ b/_languages/useful-syntax-sugar.md @@ -0,0 +1,153 @@ +--- +title: "Language Design: Useful Syntax Sugar" +date: 2022-07-10 +--- + +#### `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 + +```java +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 also replace special-purpose syntax used to construct various data structures. + +Instead of e. g. + +```java +int[] someArray = int[] { 1, 2, 3 }; +``` + +one can write + +``` +let someArray = Array(1, 2, 3) +/* same as */ +let someArray = Array.get(1, 2, 3) +``` + +assuming a definition like + +``` +module Array + fun get[T](vals: T*): Array[T] = ... +``` + +---- + +Of course `Array` is just one example; this rule applies to other data structures and use-cases equally: + +``` +let countriesAndCapitals = + Map("France" -> "Paris", "Germany" -> "Berlin", ...) +countriesAndCapitals("France") // "Paris" + +let baroqueComposers = Set("Bach", "Händel", "Vivaldi", ...) +baroqueComposers("Rammstein") // false +``` + +#### `set` sugar + +##### Rule + +> `x.set(y, z)` can be written as `x(y) = z` + +##### Explanation + +Instead of special-purpose syntax that is used for indexing operations (writing) in many languages, like + +```java +someArray[0] = 23; +``` + +one can write + +``` +someArray(0) = 23 +/* same as */ +someArray.set(0, 23) +``` + +assuming a definition like + +``` +class Array[T] + fun set(idx: Int64, val: T): Unit = ... +``` + +---- + +Of course `Array` is just one example; this rule applies to other data structures and use-cases equally: + +``` +let countriesAndCapitals = + Map("France" -> "Paris", "Germany" -> "Berlin", ...) +countriesAndCapitals("England") = "London" // new entry added + +let baroqueComposers = Set("Bach", "Händel", "Vivaldi", ...) +baroqueComposers("Monteverdi") = true // new entry added +``` + +#### `set...` sugar + +##### Rule + +> `x.setY(z)` can be written as `x.y = z` + +##### Explanation + +Instead of special-purpose syntax for properties and their setters, like + +```c# +struct Rating { + int value { + get { return value; } + set { + if (value < 0 || value > 100) + throw new ArgumentOutOfRangeException(); + this.value = value; + } + } +} + +someRating.value = 97; +``` + +one can keep writing + +``` +someRating.value = 97 +``` + +assuming a definition like + +``` +struct Rating(var value: Int32) + fun setValue(val: Int32) = ... +``` + +but does not have to pay the complexity cost of adding properties to the language. diff --git a/_languages/winding-down-rust-feature-development.md b/_languages/winding-down-rust-feature-development.md new file mode 100644 index 00000000..4acd9c65 --- /dev/null +++ b/_languages/winding-down-rust-feature-development.md @@ -0,0 +1,18 @@ +--- +title: "Time to wind down Rust feature development" +date: 2022-12-16 +--- + +_**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. + +Looking at the last few years of feature additions I have issues coming up with a feature whose +benefits are larger than its costs. + +- async/await: too early to tell whether it was actually worth it +- if let: growing extensions proposals at an impressive rate diff --git a/_layouts/post.html b/_layouts/post.html index 0c2a990e..fe44c59b 100644 --- a/_layouts/post.html +++ b/_layouts/post.html @@ -11,7 +11,7 @@ {{ title_parts[0] }} |