Hacker News new | ask | show | jobs
by ljackman 2380 days ago
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.

3 comments

I disagree with so many points in your list I don't even know if it's productive to start listing them.

From implying that C# lambdas are not infinitely more useful, powerful, and intuitive than "functional interfaces"

To the issues implying Properties are bad compared to the completely and utterly ridiculous situation in Java (which is exactly how they're implemented in Kotlin by the way).

And your "critique" of "dynamic", do you even know what it's for or how it works? Go read up on what the "DLR" is

You seem to have fundamental issues with the fact C# goes for being a useful language for it's users over being Java. Half your reasons are literally "it doesn't do it how Java does" or "it does X but Java doesn't so X is bad". The other half are complaints about insanely powerful features C# has that have minor issues that in no way take away from the fact.. they're incredibly powerful and useful features.

-

To me the moment you're trying to defend Java generics and type erasure, vs C# which paid the price early and has reaped the rewards for years, you should already you're on the wrong side of things...

> To me the moment you're trying to defend Java generics and type erasure, vs C# which paid the price early and has reaped the rewards for years, you should already you're on the wrong side of things...

Those erased generics that you call "half baked" are the reason why language interop works so much better in Java that in .NET. The combination of subtype polymorphism and parametric polymorphism means choosing a variance strategy. If you reify generics with subtypable arguments that means that you must bake a variance strategy into the platform -- which is what .NET has done -- even though languages have very different ones (in particular, untyped and typed languages will have different variance). So on the Java platform, the Java language, Kotlin, and Clojure all have different variance, yet they can share data structures with no runtime conversion. The cost of this platform compatibility is exactly three very minor annoyances [1] in the Java language. For the price of these three minor annoyances, the reward Java has reaped is a large polyglot ecosystem that's a favorite of language implementors. The Java language, too, the one with those actual three minor inconveniences, is also much more popular than C#. So overall, I think it is hard to argue that C#'s rewards from reified generics are greater than Java's from erased generics.

It really doesn't take much of an effort to defend Java's choice once you know what the ramifications are and what the results have been. Partly because of the mistake of reified generics, .NET is de-facto a one-language platform. The language, like most programming languages, is fine, but given Java's growing emphasis on being one of the best runtimes for Python, Ruby and other languages as well, it's very clear that the two platforms have very different goals. Reifying subtypable generics is a good choice for a one-language platform but a bad choice for a polyglot one.

[1]: In order of decreasing annoyance: no overloading by generic argument, no `new T[]`, no `instanceof List<String>`. All three are very minor concerns, and the last is possibly even an antipattern.

You're seriously overselling how much value non-Java JVM based languages are bringing to the ecosystem...

Java is still the only JVM language with more than 1% usage on any industry ranking of languages.

All non-JVM languages combined represent 10% of JVM usage

https://snyk.io/blog/jvm-ecosystem-report-2018/

As someone who writes a lot of Kotlin for a living, something like 80% of the improvements Kotlin brings that I use on a day to day basis are features to give it the same level of ergonomics as C#... like reified generics...

-

And your comment that .NET is a de-factor one-language platform makes it sound like you've never heard of the DLR (or F# and VB for that matter)

To me the biggest reason DLR languages are not as big as things like JRuby is C# is a pretty damn good language. There's much less value is trying to cobble together existing languages and subpar runtimes when the defacto language is modern, developing at a steady clip, and "delightful" to use.

Those under-10% Java market-share languages would make up about half of .NET's. Everything looks small in comparison when you're as incredibly successful as Java (some of those languages you find so insignificant are about as big as Go, much bigger than Rust, and probably 10x as big as Elixir or Haskell). I just find it funny to argue that second-tier products (in terms of popularity) know about "value" more than leading ones. And none of that changes Java's focus and strategy as a polyglot runtime. Java is already on its way to be the best Python runtime, and it's getting competitive with the very top JS runtimes out there.

You can argue over language preference, as some programmers do, all you like. I have very different preferences from yours, and many other programmers have preferences different from the both of us and that's OK. You say you prefer programming Java in Kotlin rather than the Java language? That's pefectly fine and part of Java's strategy for the past 20 years. The Java language is intentionally conservative because it seems many more people like conservative, slow-changing languages, but the Java platform will make sure that it runs Clojure, Kotlin, JS, Python and Java language programs as well as anything.

Ugh, I started reading this before I realized it's the same person who thinks C#'s under the hood improvements in the last decade can be handwaved away.

Yeah, 10% of Java's market share is not larger than C#

https://stackify.com/popular-programming-languages-2018/

If it was, those languages would be showing up on Github's survey above C# as well, they all consider Java independently not as a combination of all JVM languages.

-

You've contorted this conversation so ridiculously out of shape then beating the horse you laid on it.

My original comment was a rebuttal to this comment:

> 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#.

If you read the whole comment, it was not about the JVM, it was not about confusing this issue with "oh yeah well the language sucks but that's so you can run Python on it's runtime".

No idea why you are so insistent on making it about anything but the actual language called Java, not Clojure or Kotlin or Js or Python.

Because Java is both the name of a platform and the name of a language for that platform, and from its original design, the platform has been the main focus. Clojure, Kotlin and the Java language are all Java platform languages. And you'll just have to come to terms with the objective fact that other developers might disagree with your subjective language preferences. In fact, statistics would suggest that most of them would (as they would with any of us; I don't think a majority of developers would agree with any single language preference ranking). Developers know that most other developers disagree with them over language preference. That's why I'd rather speak of the platform than the language. Clearly we have different language preferences -- as most developers do -- and there is no right and wrong there.
So when are we getting F# designers in Visual Studio?
Are you confusing IDE support with language support on a given runtime?
If you want me to talk about language support on a given runtime instead, try to use F# on .NET Native, or VB.NET on Blazor.
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.

> 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.

Nullable Reference Types (NRTs) is released, so it's important to talk about what exists in an LTS form today rather than something from a draft proposal.

Firstly, there's the surface-level stuff. Reference types can be explicitly be marked as `foo?` to indicate to the compiler that the type is nullable. Mismatches are warnings to ensure backwards compatibility, since billions of lines of perfectly valid code today can't just start emitting errors across an entire codebase.

But the far more interesting side of NRT isn't that, but the compiler analysis that goes into it. It's an incredibly advanced and thorough flow-based typing system that catches numerous complicated scenarios, and a system that can be (and is) improved over time without incurring a risk of a breaking change. This analysis is equally applied to the existing nullable value types, so it's a unified model.

The other interesting side of NRT is that isn't a one-and-done feature. There's a long rollout period where the .NET ecosystem adopts this way of dealing with reference types, and to do so there need to be tools for component authors and application developers to adopt it incrementally and at their own pace. Everything in the design is incredibly deliberate and well thought-out, with numerous past designs (such as a "sidecar" format for annotations and a mechanism for managing updates to that in parallel with a package or framework!). It is imperfect, but perhaps the best that can be done given the constraints a 20 year old language imposes.

That said, I really the world could be different. Since I prefer (and work on) a typed functional language where `null` isn't much of a problem due to a different core language design, the incredible amount of engineering effort that went into NRT for C# feels slightly strange to me. But my only reasonable alternative to not making progress on this problem is, "just use a different language", which most developers do not find reasonable.

Additionally, the pedantic side of me doesn't feel that Java's optional is an any way reminiscent of monadic programming. Java simply lacks numerous features to enable this style of programming in a way that the majority of Java developers would utilize.

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/

> 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.

There's three levels of opt-in/out:

* Source directives, for opting in/out a single scope (typically a whole file, but it can in be as small as a single method)

* Project file directive via MSBuild property, for opting in/out a single project/assembly

* The MSBuild property can be set in a Directory.build.props file, opting in/out all projects in a directory and its children

In the long term, this property will likely be implicitly set to true for new projects (and perhaps all projects in the very long term), but for now everything is opt-in.

Personally, I think that one of the biggest areas for tooling improvement is to better recognize when you're in a nullable-enabled context. It's pretty obvious when you look at source code, but when you've got a million LoC codebase and only a subset uses it, making developers aware of that is certainly a challenge.

The other interesting thing to consider here is that even if the C# compiler freezes all future improvements to NRT analysis, there is a level of breaking changes that will occur over time as packages and frameworks adopt the feature and rev their versions. This transformation won't be without pain for existing codebases, and some may never adopt the feature (especially if they're the kind of codebase that doesn't really modernize). Interesting times ahead.

This mostly reads to me as a list of things I miss when I go back to Java.
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.