That's a good point. Generally, I opt for languages that either compile away their nicities to avoid runtime hits, such as Rust being compiled to WASM, or languages that bring nicities in runtimes that they _completely own_, such as Java on the JVM.
The problem with Kotlin and the like is that they can't easily compile away their features due to inherent runtime dependencies, e.g. garbage collection, making them poorly suited to environments with a very minimal runtime like WASM, while also being at the mercy of the host language creating runtime abstractions that have mismatches with their own language's features.
Although it'd be unfair for me to say the JVM is designed only for Java; invokedynamic and non-reified generics both assist JVM targetting for non-Java languages such as Clojure.
`Async`/`await` or something like Kotlin's `suspend` are great language features for certain domains in which a developer needs to manage blocking system calls: in lower-level languages such as Rust or C, you probably don't want to pay for a lightweight "task runtime" Like Go's or Erlang's. They bring not only a scheduling overhead but also FFI complications.
However, for application languages that can afford a few extra nicities like garbage collection, I fail to understand why the stackless coroutine model (`suspend` in Kotlin) or `async`/`await` continue to be the developer's choice. Why do languages like Kotlin adopt these features, specifically?
Manually deciding where to yield in order to avoid blocking a kernel thread seems outside of the domain of problems that those using a _higher level_ language want to solve, surely?
The caller should decide whether to do something "in the background". And this applies to non-IO capabilities too, as sometimes pure computations are also expensive enough to warrant not blocking the current task.
Go and Erlang seem to have nailed this, so I'm glad Java is following in their footsteps rather than the more questionnable strategy of C# and Kotlin. (Lua's coroutines and Scheme's `call-with-current-continuation` deserve an honourable mention too.)
The whitelisting looks great. Even with the remaining concerns I raised in the other comment, the ability to whitelist only allowed domains for network connections is a massive step up security-wise, even if they are allowed for the entire program (until revoked globally).
That's a good start, supporting the dropping of privileges after performing something on startup.
What about keeping the network allowed in the layer handling, say, inbound HTTP connections, but blocking it in the data access layer or purely computational component?
From what I can see, this doesn't work with global boolean flags in the runtime, instead requiring isolated tasks with whitelisted capabilities passed in, some form of "immutable, set-once, dynamically-scoped capability flags", or something like that.
The problem with the global boolean flag approach is that if any part of a service needs it constantly, the entire program gets it, even obscure subdependencies for generating colour pickers.
Don't get me wrong, it's an incremental improvement over's Node.js blase approach. It's also quite niche to see languages support this feature. E was one of them. There was another newer Python-like language with this too, starting with an `M`, but its name escapes me.
I'd recommend Deno's developers look at E a bit more before committing too much to the platform boolean flag approach. Or I've misunderstood their approach and it actually does more than I'm giving it credit for.
It looks like a good evolutionary improvement over Node.js. However, I have a concern about its security claims. Perhaps someone on the project can allay these concerns.
My brief skimming of its site indicates that its security model is based around the ability to disable, say, network access for whole Deno programs. However, does it allow starting up with network access, allowing a subset of the program to handle it, and then dropping those rights for the rest of the program, _especially_ subdependencies?
A modern Node.js web service will have hundreds, if not thousands, of indirect dependencies. Some network access will be required for at least the Express routing, or an equivalent.
For a Deno equivalent, this would amount to enabling network access for all hundreds of those subdependencies, not reproducing the isolation in capability-based security such as WebAssembly nanoprocesses.
Have I missed something here? That doesn't seem like much of an improvement over Node.js except for very small and contained programs. Yes, Deno's dropping of centralised package repositories and package.json might alleviate this problem _somewhat_, but the same fundamental issue seems to remain.
I wonder whether Project Loom will create a split in the Kotlin ecosystem between coroutine-based concurrency and Loom lightweight thread-based concurrency.
Java will have a similar problem with CompletableFuture and third-party projects like Reactor used in Spring WebFlux.
However, Kotlin baking in language syntax for a competing concurrency model will exacerbate the split.
With Java already having type inference, concise lambdas, and with upcoming records that can already be faked in current versions via Lombok, the value proposition of Kotlin seems questionable outside of Android with its held-back version of Java.
Clojure is another story entirely, offering a very different and compelling way of thinking about computing on the JVM. By contrast, my developing view is that Kotlin is different enough to require learning something new yet not different enough from modern vanilla java to offer substantial advantages.
There might be, but unfortunately I don't know it. I learnt Java initially from the first edition of Just Java, read from various other sources about the 1.5 and 1.6 additions it didn't cover, and then learned new features from OpenJDK proposals from 1.7 onwards as they were released.
The biggest impact of the 5 years for working developers has probably been:
* Streaming API and lambdas allowing usual `map`/`filter`/`reduce` transformation pipelines line many other languages (lazy, unlike JS's `Array` methods, parallelism considered via "spliterators" unlike Python).
* Default methods in interfaces, _not_ making them traits, but arguably more "trait-like". It allowed them to add many useful methods onto existing types without needing bifurcate the common APIs.
* A REPL. Probably not a big deal for a professional developer, who likely uses IntelliJ's "Evaluate Expression" feature already for a similar feature, but pretty useful for education I guess.
* Better at being stuffed inside Docker containers with resource limits. Also, max permgen space no longer a problem really (it's where you had to set an upper memory limit when invoking Java programs, which was annoying). Just generally less annoying at being deployed in modern infrasructures.
* APIs like `File.lines` that mean you don't need to recite War and Peace just to do a buffered operation across lines in a file. Generally more ergonomic APIs that finally make basic operations sane.
* Type inference with `var`. Basically the same as C#'s `var` or Go's `:=`.
* More native executable-producing tooling. jlink, jpackager, etc. The dream of ubiquitous, shared Java runtimes installed everywhere didn't really work out, and they seem to have realised that.
* Ecosystem moving away from XML. Still there, just less common. Runtime annotations are used more heavily, which I'm honestly not sure is a massive improvement. Hopefully more lambda-heavy APIs will reduce use of runtime annotations; see Spring WebFlux's functional handlers for annotation-based controllers for a comparison.
There's a lot more, but I'd argue it won't be as immediately visible for the working developer more than those points. Modules were important for the ecosystem, but most devs probably aren't worrying about that day-to-day. Gradle and Maven still dominate the building/packaging side.
Thanks for the clarification on the final NRT behaviour. Just to say, my point about "didn't align with good taste" was itself lacking taste. I'm sure each of those features I critiqued made sense as they were proposed at the time and were just considering different use cases and tradeoffs.
Did it drop the idea of assembly-wide opt-ins to stricter behaviour, meaning all NRTs can be reasoned about in the same way without considering a configuration flag somewhere like PHP? That does sound like an improvement.
Doing the change gradually, without breaking existing code or requiring potentially-ecosystem-breaking opt-ins does seem eminantly sensible and user-friendly. I was being too harsh to C# here. Java's `Optional` doesn't even warn about it itself being null, for example. You need static tooling and code analysis for that. C#'s solution does at least try to solve that, albeit at the cost of more complexity.
I said "monad-like" rather than "monadic" for that reason, but arguably it doesn't even go far enough to be considered monadic-like. Certainly this article would agree: https://blog.developer.atlassian.com/optional-broken/
Fair enough; I do miss keyword and default arguments from C# when using Java. A long list of overloads and argument forwarding to fake default arguments gets old.
FWIW, I could have produced an equally-long list of Java flaws. I'm not terribly attached to either language. I just wanted to challenge the meme that "C# is a better Java" I hear on the Internet every other week.
"Infinitely" more useful seems like a stretch. Both languages made valid design decisions with their lambdas: autogenerated types means not every variant of lambda needs a backing interface, but it also means that the types are a world unto themselves and not integrated with well-established interfaces and abstract classes in the way that Java lambdas are, resulting in conflicting mechanisms for passing code to adhere to a requirement. I find the unification of SAM types and lambdas to be elegant in a class-based OOP language (and actually preferable to the Smalltalk/Ruby block model too), but it's clearly subjective.
My criticism of properties hiding side-effects as attribute reads is mostly derived from one of the earliest books about the CLR; was it "The CLR via C#"? I'll have to check. The critique isn't Java-inspired; getters have the same problem of course. The point is that a getter is a method call, so you expect potential side-effects. You don't expect side-effects from a property read, although C# does tend to use capital letters for properties, to be fair.
While on the topic of CLR, I realise the DLR exposed dynamic typing primarily to make the CLR a better target for dynamic languages; I was arguing that exposing that up to C# wasn't necessary. C# is the flagship CLR language, sure, but that doesn't mean it must expose _every_ feature of it. Java also added `invokedynamic` for similar reasons but didn't feel the need to expose it to its flagship language directly in language syntax.
pron covered the nuances around generic type erasure in another comment better than I did, so I won't reiterate. Like you, I still mildly prefer reified generics over erasure as a language user, but the points raised by pron and Bracha are absolutely real. The ability to do runtime type checks and default values on generics seem like antipatterns, so I'm glad Java doesn't support those _specific_ features of reification even if I like a lot of the others parts.
These C# issues arguably demonstrate haphazard additions that didn't align with good taste:
* Too many overlapping concepts for referring to code by value and defining them inline: events, delegates, anonymous delegates, and lambdas.
* Lambdas that generate magic types rather than slotting into SAM types. This works great for functional languages, sure, but doesn't fit well into a class-based OOP language.
* `ref` and `out` parameters to appease archaic COM APIs.
* Tuple unpacking, which makes sense independently but then bizzarely tries to integrate with `out` parameters.
* A nice "ex nihlo" object literal syntax that shoots itself in the foot by undermining immutability due to requiring settable fields. (TBF, later versions fixed this IIRC.)
* Inline functions in methods in a language that already has lambdas and methods; it's just bloat.
* `as` casting that yields nulls. Did it really need a whole new syntax just to handle null more concisely specifically for casting?
* `partial` classes. Encouraging even more code generation with features like this is a questionable idea and wasn't necessary in other languages.
* `dynamic`. Even if there are use cases for it, it's strange in an otherwise static language with a top-level Object type anyway. I'm saying this as someone who's perfectly happy with fully dynamic languages like Erlang and Lisp. I just don't see the point of adding it to C# specifically.
* C-style enumerations.
* Properties. Invoking side-effects on something that is syntactically indistinguishable from an attribute read is a bad idea. Auto-generation of Java's verbose getters would be long overdue, but the caller site shouldn't be the same a la C#.
* Extension methods. It seems quite ad hoc compared to static addition of types to common operations in other languages, like imported traits in Rust or typeclasses in Haskell.
* Proposed "shapes". Looks like a good idea by itself but will overlap too much with default method implementation and other existing mechanisms.
* Interfaces prefixed with `I`. Not strictly a language problem but an ecosystem one. I shouldn't need to know what _sort_ of type I'm dealing with, that's '90s Hungarian notation.
* Nullable reference types. Getting rid of null is good, but this proposal became confusing. They mentioned opting in assembly-wide for a while but there was then a conversation about having it just warn in some cases. I need to read the latest literature around this, but it seemed less elegant than Java just adding a monad-like Optional type and not adding loads of special-case operations with question marks everywhere.
Despite this, I still admire a lot of the design decisions behind C#. LINQ was great. `async/await`, despite my belief of its inferiority to Goroutines/Project Loom/Erlang processes, was still a great innovation from Midiori at the time. Value types were obviously right to be implemented early on. Assemblies were a good idea. Private by default rather than package-accessibility was a nice touch, as was the `override` keyword. The C# team are smart people who know what they are doing!
As an aside, I used to be firmly against erased generics, but reading more about the tradeoffs from the likes of Gilad Bracha has caused me to reconsider.
While Java's slow and cautious evolution frustrates developers, it still arguably demonstrates longer-term thinking than the constant accrual of features in its contemporaries such as JavaScript and C#.
That isn't to say the designers of JavaScript and C# don't think carefully about the addition of new features; indeed, was it Anders Heljsberg who made the point about all new proposed features starting with negative points?
But Java's recent and upcoming additions show _taste_: adopting a single lambda notation that fitted smoothly with the surrounding class-based OOP paradigm via SAM types (rather than magically-generated-type-style lambdas succeeding two overlapping "delegate"/"event"-like features); implementing (sadly somewhat-incompatible) modules that can curtail unbounded reflection, opening the door to greater reasonability and performance in the future; proposing Project Loom to avoid the async/sync API split that hit Python, C#, and even Kotlin Coroutines; and now Valhalla, which was quietly mulled on for years before arriving at this very reasonable solution that considers myriad angles.
I like this approach to language design and think it bodes well for the language's future. It's a sweet spot between being necessarily conservative, dealing with developers' real-world problems, and giving time to mull over new language feature designs and not just implementing as soon (and haphazardly) as possible to please vocal developers.
Some templating languages such as Jsonnet[0] add built-in templating and just enough programmability to cover basic operations like templating and iteration.
I originally felt it was overly complex, but after seeing some of the Go text/template and Ansible Jinja examples in the wild, it actually seems like a good idea.
Perhaps we should more strongly distinguish between “basic” data definition formats and ones that need to be templated. JSON5 for the former and Jsonnet for the latter, for example.
The problem with Kotlin and the like is that they can't easily compile away their features due to inherent runtime dependencies, e.g. garbage collection, making them poorly suited to environments with a very minimal runtime like WASM, while also being at the mercy of the host language creating runtime abstractions that have mismatches with their own language's features.
Although it'd be unfair for me to say the JVM is designed only for Java; invokedynamic and non-reified generics both assist JVM targetting for non-Java languages such as Clojure.