80 comments

[ 4.3 ms ] story [ 119 ms ] thread
Not sure about this take. Every language doesn't have to have every feature of every other language. Simplicity is in itself a feature.

C# at this point is already probably 'larger' as far as feature set goes other mainstream languages.

I’m not overly enthusiastic about most of his suggestions. I thin C# has, or is gaining, more appropriate alternatives for all of these.

In C# we don’t like functions with more than a few arguments. If you have 10 fields in your class, a constructor with 10 arguments is not appropriate. Instead we prefer object initialisers, but this requires exposing the ‘set’ method, and allowing mutability. This is resolved in C# 9, with init-only properties.

> If you have 10 fields in your class, a constructor with 10 arguments is not appropriate. Instead we prefer object initialisers, but this requires exposing the ‘set’ method, and allowing mutability.

Why not have the ctor accept a single options object? At least if initialization is your only reason for allowing mutable props.

I read that as "i'm complaining that C# isn't a functional language" ?

Funny that the counterexamples are from F# which also runs on the dot niet platform? And you could concievably mix and match them in the same project?

You can but the business case for introducing F# can be tricky. It's a better developer experience but due to the difficulty (or perceived difficulty) in hiring F# developers it's seen as a liability.
(comment deleted)
And to add: the existing imperative/oo programming workforce is of no use.
Does VS not have something like "return value of pure method not used"? Because JetBrains ReSharper/Rider gives that warning.

> in a C# notebook

Anyone has any idea what they mean here?

(comment deleted)
Likely a Jupyter notebook used with C#
C# Jupyter Notebooks, or something like that, I imagine
The complaint about 'implicit information loss' is a bit weird, the code calls a function/method that may do other things, the compiler doesn't know whether you intend to ignore what's returned or not. To return 'true' when you meant to return the result of the function call on the previous line is just a bug. There are going to be bugs.
C++ introduced the [[nodiscard]] attribute for this exact use case. A C# [NoDiscard] attribute would do the same thing with the help of a Roslyn analyzer I guess.
C# already by default warns on discarded return values.
No it does not, unless the discarded value is a Task in an async method. (or sometimes an IDisposable depending on what analyzers you have running)
I think what he meant is the different (implicitly assumed by human brain) output of those two statements:

someList.Add(obj); someStr.Format(„%d“);

While the first one cleary modifies the list someList, the second one only _returns_ the modified string and someStr’s value does not change. IMHO fornatted text is a different (new) object therefore I don‘t see his issue, and I also think there‘s a shift towards immutable, which would clear such a thing up.

I feel like your example is talking about having the result "in-place" or not, which is an entirely different from what he's complaining (semicolon suspends output).
> In project programming this hit me this week with a bug: > value.Format(...); > return true; > when I actually wanted > return value.Format(...); > This is "implicit information loss" [..]

I don‘t see any semicolon missing.

Oh sorry, didn't notice you're talking about the first half of point #1.

To be honest, I have no idea what he's expecting with that "return true". How would that be different in any other language?

I think he‘s expecting someStr.Format() to format the value of the object someStr _instead_ of returning a new object containing the fornatted string.
The only one I've _really_ wanted are #2 and #3. #3 may be coming with new versions thanks to "value classes" or something like that, but "anonymous interface objects" are the only thing I actually miss from Java.
Use LinFu

    select new DynamicObject(new
 {
   A = value.A,
   B = value.C + "_" + value.D
 }).CreateDuck<DummyInterface>();
you could use method chaining in order to create any dsl you want in any language you want. Linq to objects is nice and you certainly have lambdas and initialization syntax and both support duck typing so the post falls around the I have no idea what I'm doing category.
I very much hope no C# PMs are reading this and note ideas. C# is a fine language, but it's already packed with too many features and ideas from too many other languages. No need to make even bigger.
C# PMs are already implementing everything from F#. Everything you see there will be eventually C# as well. There's no doubt.
I'm pretty sure C# PMs will read this if they are on Twitter. Even if they don't follow the author the original tweet will be pushed into your timeline if you follow people that tweet about C#. A lot of the development in C# is inspired by functional languages so when the rant is authored by the creator of F# they will likely pay attention... and perhaps have a good laugh as they most likely know Don personally.
Very strange take in my opinion. I haven't used C# in a while but I guess the compiler doesn't warn when you discard a return value? I suppose that's kind of annoying when debugging. I don't know about many languages that do this by default though.

I also don't understand how you can have a "return true" for a function that's supposed to return a formatted value.

I don't know what a C# notebook is, but the difference between a statement and an expression is made in any imperative programming language I know. However, the example is very strange; why would putting a semicolon at the end of the line make any difference? Why should it even compile without it?

As for the ICompare interface, I think the author has a point. The example class they give is unnecessarily complex, but they're right that the lack of concise inline class definitions can sometimes be annoying. However, I'm pretty sure you can use lamba functions to do the exact same thing the author is showing in F#, down to storing the comparator as a Func<> variable.

The lack of HTML generation is also a weird one. C# is not a template language and is not intended to be one. You can use ASP to generate HTML pages fine, but I don't understand the value of a DSL to generate HTML in pure C#. Is the loj 't that the syntax can't be radically reformed? Because I consider that to be a good thing.

The point I take away from this rant is that the author is trying to use an imperative language with some functional language features as a fully functional language. These aren't shortcomings of C#, this is the result of using the wrong tool (or using the tool wrong) for the job.

The compiler does warn when you discard a value, but I assume a C# notebook is something like Jupyter? It is probably a special case in that environment.

There is Comparer.Create convenience method which creates an IComparer instance from a lambda. But yeah it is a workaround.

Sounds fine to me. Unused valuable will get you some squiggle warnings in the editor, but not in a notebook.

As far as "language gotcha" articles go this is pretty weak IMO

The semicolon at the end of a statement is possibly matlab-inspired.
> The compiler does warn when you discard a value

The C# compiler doesn't; the F# compiler does.

While not a warning by default, it does inform (IDE0058) in the editor. placing dotnet_diagnostic.IDE0058.severity = warning in your .editorconfig will promote it.
and with warnings as error its also forbidden.
It is strange how quickly I stopped reading after the first listed example.

Return boolean | string? Perhaps they're thinking of TypeScript :-D

I've written many thousands of lines of both TS and C#, and I've learned two things about type systems as a result:

1. Structural (TS) typing is 1,000x better than nominal typing (C#, Java, most other mainstream languages)

2. Union types are incredibly useful and reduce a lot of boilerplate and overloading.

I'm curious, what do you find are the advantages of structural typing?
I assume, from their statement, it's simply the ergonomics of not having to explicitly name ad-hoc sum types. Which I do agree goes a long way in ergonomics and reduced friction for leveraging the type system.
With nominal type systems, you have to worry about explicitly associating types with one another.

With structural typing, you can have types that satisfy a type requirement incidentally. This helps you to write in less coupled code.

For example, let's say you have the `walk(Walker walker, int distance)` function. With nominal typing, you have to create the `Walker` interface, class, or trait, and then anything that can walk explicitly implement/extend it.

With structural typing (let's use TS as an example), you can say, `type Walker = {legs: Number}`. Then any object with a property called `legs` with a `Number` type value will satisfy the first argument of my `walk` function.

This is a trivial example, but it becomes extremely powerful and useful in large code bases. The compiler can start telling you exactly how you need to refactor your code to make it work again. And you don't have to keep every data structure in your head when you're designing your code. You can just say, "OK, this object is a cat. It has fur, legs, and a tail." It can be a simple data structure, without worrying about how it relates to other objects and methods.

Alternative title: "It's strange what some people expect of C#"

The author seems to be trying to fit a C#-shaped peg into an unsuitably academic F# hole.

C# is one of the best languages I've ever used, it's amazing how well it holds up in my (admittedly large) niche. But one language can't be everything to everyone.

For example, the complaint that something doesn't print in a C# notebook if a semicolon is appended seems so far out there in the margins of usefulness I'm having trouble finding back the page.

C# is pretty great, I use it everyday. But if you've even spent significant time in F#, you return to C# land asking some of the same questions Don does.

A great example is C# 8's Nullable Reference Types feature, touted as (partial) solution to the Null object references. But it isn't air tight, or even water tight. Even if you annotate everything correctly, you can still slip null refs past the compiler.

But F#'s solution to the Null ref issue is much simpler, don't allow nulls at all. Force the developer to use Option<T> to express the absence of value. F# did it correctly the first time and has benefitted ever since.

Could someone please elaborate his #1 a little bit (as I'm not familiar with C#)?

For the first example, I don't see how it has anything to do with C#.

For the second example, I assume it works like Matlab? By "discard" it just suspends the print, but if you use something like `a = 1;` it still assign 1 to a? If so, I think it's perfect fine for notebook-like interactive shell.

Same question. It’s two entirely different pieces of code, even returning different types. Is the author upset that C# allows side effect mutations?
Compiler didn't balk at unused return value. (Also by the sound of Format() the function is typed as returning a string, but the author didn't raise that issue.)
I see... seems a rather subjective take (IMO)
Being able to discard the return value in an interactive shell is not just ,,fine'', but actually crucial feature (the exact syntax for discarding is less important).

Quite often the rendering of an object would take so much time, that the interactive shell would hang, or so much space that it's not worth to show it.

i think we should cut fingers of humans because some of us uses them to pick their noses or fingers themselves and then smells their fingers. this is disgusting.
There are real advanced issues here, not just syntax issues. I'd summarize the first four items like this:

A. (1,4) C# immutable collection support is a step-child, without full support for the required behaviours around it from the compiler and the community. That's why VS allows "immutableList.Add(item)" as if it were mutable, when it should be an error.

Yea, C# isn't just a functional language, but if there's support for immutable collections it should be done right.

B. (2,3) There is "no way to implement interfaces or abstract classes using an expression". The author's example is simple, but one could imagine a comparison involving captured variables where the separate class complicates things, because any captured variable needs to be shoved in a constructor.

I've hit 1 and 2 directly, 3 comes naturally from 2 (when you can define an object expression, you want a simple syntax for the constructor), as well as 4 from 1. I've had sufficient annoyances with 1 I've decided to not use immutable collections until support is improved.

As for dynamic, it's good for what it does, people who use it know the risks. Instead, I'd complain about nuget, when it's too easy to use outdated/obsolete packages and keep them that way. npm actually has better package management options. The saving grace is that the typical C# solution has far less dependencies than the JS/TS projects, but it's best to act prophylactically, and help the programmer to keep dependencies updated.

(1) I discussed here and I do not even know what's going on there: https://news.ycombinator.com/item?id=24168274

Tho, your own example of immutable collections is actually a good one. Rather specific, but still good. Immutable collections really could profit from compuiler-level warnings or even errors. It would be an easy enough fix for the language/compiler team: have some attribute that you/the immutable library authors would decorate functions with, something like [NoDiscardReturn] and the compiler would check that and emit warnings/errors.

(2) Implementing something like IComparer<> is something you rarely do, and more or less never the way the author does.

   var enumerator = array.OrderBy(e => e.Foo).ThenBy(e => something.CheckProperty(e.Bar)).ThenByDescending(e => e.Name, StringComparer.OridinalIgnoreCase);
   array = enumerator.ToArray();
   // Tho you often do not need to call .ToArray() if you're only gonna traverse the enumerator once
Using Linq like this is more idiomatic and also kinda "functional", which appears to be what the author is after.

Now, this was specific to the comparer stuff, but in general a sane API these days would use Func<> or Action<> instead of single-method interfaces. There are still some leftovers in the C# stdlib (and probably more in third party libraries) but again, you can write a wrapper once - not the horrible mess the author proposed either, but an interface implementation that would use single Func<> parameter in the constructor and then call that in the method implementation.

    internal class Foo
    {
      public string Bar;
      public int Baz;

      public override string ToString() => $"{Bar}: {Baz}";
    }

    internal class DontAnnoyMeComparer<T> : IComparer<T>
    {
      private readonly Func<T, T, int> cmpFunc;

      internal DontAnnoyMeComparer(Func<T, T, int> cmpFunc)
      {
        this.cmpFunc = cmpFunc;
      }

      public int Compare(T x, T y) => cmpFunc(x, y);
    }

    static void Main(string[] args)
    {
      var foos = new[] {
         new Foo { Bar = "World", Baz = 1 },
         new Foo { Bar = "Hello", Baz = 2 },
         new Foo { Bar = "Hello", Baz = 1 },
       };

      // First with some ideomatic Linq
      Console.WriteLine(string.Join(", ", foos.OrderBy(f => f.Bar).ThenBy(f => f.Baz)));

      // Then with our helper interface implementation
      Array.Sort(foos, new DontAnnoyMeComparer<Foo>((x, y) => {
        var res = x.Bar.CompareTo(y.Bar);
        return res != 0 ? res : x.Baz.CompareTo(y.Baz);
      }));
      Console.WriteLine(string.Join(", ", (object[])foos));
    }

    // Output
    // Hello: 1, Hello: 2, World: 1
    // Hello: 1, Hello: 2, World: 1

(3) related directly to (2). The only real reason this is a real-world problem for the author is that they do not actually write idiomatic code.

(4) Sure, something like that would be nice sometimes. But the lack of it is hardly the end of the world. Tho on the flipside, in my opinion languages that offer such pseudo-DSLs or even real embdeddable DSLs quite often suffer from people writing DSLs for every freaking thing, thus reducing readability of their code.

(5) dynamic is hell for robust code, that's why you avoid it. I only ever used it when I was too lazy to do the extra typing to create proper types in run-once code dealing with JSON. And even then I should have probably just:

    class MyType {
      [DataMemeber]
      public int Foo;
      
      [DataMemeber(Name="baz")]
      public int Baz;
    }
1) Immutable collections is a bit of a pain in C# right now. No LINQ To() methods, some nugets don't actually work with them, constructor methods can be nicer...

His item 4 is to some extent a result of the limited immutable support (he mentions "a lightweight way of building immutable list/collection expression"). Fix that support and 1,4 out of his list are not much of a problem anymore or at least ameliorated.

2) I would be glad to never need hear of IComparer or IEqualityComparer again, which I only need due to the stdlib leftovers... Admittedly not much, but enough to be annoying. His solution is more general, but your idea is very nice. Why didn't I think of that?

5) Dynamic is there for the Dynamic Language Runtime and for dynamic code far more general than your example. It should be kept in that silo, but it does do its job there.

IMHO, the author tries to write C# with the current trend of functional+immutable, and that's why he ended up hitting these issues. It's true that the OOP way isn't bad (at least with those examples), but it's also true C# could offer better support for doing things the other way without much effort.

I thought about creating a small library that would expose a static IComparer<T> ToComparer<T>(this Func<T, T, int>) extension method (and a similar one for IEqualityComparer<T>). But I so far just was too lazy. If somebody from the C# API team reads this tho.... :P

And while you're at it dear C# team, add .Distinct<TSource, TDest>(Func<TSource, TDest>, IEqualityComparer<TDest>) similar to OrderBy and friends, and various constructors to the generic collection types taking Func<T, T, int> instead of IComparer<T>. Yeah, API bloat, but good one in this case IMHO.

C# and the .NET runtime and the compiler(s) are still evolving indeed, and I would guess a lot of issues will be addressed and fixed in the future.

PS: the most annoying bit to me about the immutable collection library is that they are dead slow whenever I tried them.

Just to pick on your response to 2: there’s plenty more complex interfaces than Func possible that have single-use implementations. The fact that they’re rare in C# is precisely because they’re too painful.to the extent that C# developers will instinctively attack the problem a different way to avoid the painful syntax.
These days I would use ReadOnlyMemory<T> to express an immutable view of a collection. If you need LINQ on it, there's MemoryMarshal.ToEnumerable.
"Implicitly discarding information" is just outright wrong.

All calls can have side effects, even calls that return a value. And while C# did recently add the discard operator _ to throw away returned values out of an e.g. Tuple, if you did as this critique suggests and flag every "discarded information" the language would become verbose/obnoxious.

For example:

      try 
      {   
            [....]
            myFirstDatabaseTransaction.Commit(); // Compiler warning: "Discarded Information" (it returns the transaction ID, but has the side effect of committing your changes to the database).   
      } 
      catch([...])
      { [...] }

Basic programmer errors are easily caught during development, because they're immediately wrong. Unlike null reference exceptions which sometimes can only be found after extensive usage of the system (due to different states, or underlying data changing).
"All calls can have side effects" - this isn't the case for a "pure" function. What the author wants would make some code worse but is very useful for anything working with immutable collections [IMHO, reading #4 that's what's behind his complaints - working in a functional/immutable style where C# isn't entirely comfortable]. There could be a middle ground where certain function calls do warn/error on such use and most others do not.
F# solves this by making it a compile error to fail to consume an expression that returns anything other than unit. If you really must do this, you normally use `ignore : 'a -> unit`. It's unidiomatic F# to structure your APIs in such a way that you will routinely throw away the information you're given.
I think really what people want are strict and sleazy compile options. Or better the ability to tell the compiler that a return value is an error. Or not. If it's not an error and it gets tossed who cares really. Sometimes you don't care if every i is dotted and every t crossed.
And nullable helps with null references as well as intellisense warnings.

Moreover that examples the author did are likely both oversimplified and wrong.

In the first one, a bool then a string is returned which should raise a type error in most cases (unless you do an anonymous function and feed the result back into something that accepts an object-typed parameter/use an object-typed variable to store the result).

I don't even know what's up with the second one; that's simply not valid code - if it was a normal function you cannot omit the trailing semicolon, and if it was a anonymous lambda you cannot have a trailing semicolon.

I created an account just to comment on this.

So I do not agree with OP at all. C# is a language capable of creating really beautiful and easy to understand code. It has generally very few quirks. Please don't turn it into JavaScript or F#.

Have to agree! I mainly code Kotlin at the moment, however I still love C# and actually F# too.

I have yet to see a 'best of everything' language and C# has proven to be an amazingly versatile language. Yes, it has plenty of warts but so does everything else.

Go use C++ and you will come back running to C# scarred for life.

Have you ever tried to implement itterators in C++?

> C# already by default warns on discarded return values.

> If you did as this critique suggests and flag every "discarded information" the language would become verbose/obnoxious.

I accept these propositions and their implication.

At first, I couldn't have agreed less with the author, but when I started to write down my complaints, I realized we agreed on more than what we disagreed about. This is why I read HN. Thanks!
This may be a bit of a tangent, but does anyone know how to implement a redux-like pattern in C#?

I think i've seen clean implementations in F#, but not in C#. The only thing that prevents me from fully embracing Unity is not knowing how to do pure function reducers with a single store. That, and an aversion for everything being so class-heavy. (My favourite languages these days are elixir and typescript)

He could try:

- satisfying contracts using delegates/lambdas rather than interfaces

- using higher order fn's rather than indexed loops, etc.

which he's obviously used to doing in F#, so he can do it from C# as well. If he rants on C# like this, I'd hate to say what he has to say about Java which, as someone said is not object-oriented but "class-orientated".

It's hard to believe that D.S. wrote this -- esp. since C# has borrowed so much of F#'s ideas, and you can just write F# if you like, or use both. I've been using F# since it was first released, and it's great, but certainly not perfect either. IMHO, C# is one of the best all-around general-purpose languages with "familiar" syntax around with 1st class tooling and ecosystem. All it needs is records w/structural equality semantics, DU's and implicit "new" in object creation. F# on the other hand hasn't moved the it's needle much in a long time. (Like using Rosyln as a back end, supporting higher-kinded modules, etc.)

If C# had these features it would be hard not to use it.
This guy is the designer of F# complaining that C# is not more like F#.

"Don Syme: My main responsibility is the design and implementation of the F# programming language. I've also worked on the design of virtual machines, the C# language (being co-responsible for C# and .NET generics, and one of many contributors to C# asynchronous programming) and, indirectly, Visual Basic and other .NET languages."

https://www.microsoft.com/en-us/research/people/dsyme/

> This guy is the designer of F# complaining that C# is not more like F#.

History, by way of the C# design team, has validated his opinions. Don Syme has had a massive influence on C# even when one degree removed from it's design. His complaints should be considered carefully.

Mark Seemann has a good write up about his contributions: https://blog.ploeh.dk/2015/04/15/c-will-eventually-get-all-f...

Sure, mostly because the C# team is great and has been able to pick and choose the best features among several languages and ecosystems. We now have most of those features listed in the article since 2015 and the pace is only getting faster with point releases. Even the comment saying non-nullable reference types would never appear has already happened a year ago.

That being said, not everything is a fit (otherwise what's the point of F#?) and much of the post here seems to be edge cases or personal preference than C# defects.

1) Yeah.

2) This is a bigger deal than a lot of people here seem to think.

3) As Don knows, this is getting addressed in C#9 (finally!)

4) I mean, you could do pretty much all of this with standard LINQ operators and AddToFront (which I end up adding into every project). So I think the example is garbage. What it's really missing is F#'s yield!

5) Yeah, but it's not like anyone uses it.

> No language in the 21st Century should have implicit information loss. There are likely C# analyzers to prevent this kind of bug, or you have other choices.

I like the Rust feature of semicolon discarding information. Is that not what is happening here? Really contrived example that he accidentally wrote "return true".

Yes, but he's talking about an equivalent of Rust's [must_use].

I actually made a post on the Rust subreddit inquiring why Rust made must_use opt-in instead of opt-out like Swift and, apparently, F#. The reaction was OVERWHELMINGLY negative toward the idea of it being opt-out, which I find very surprising considering the kind of code one typically sees in Rust.

I put up with it because that's mostly where the work is that I want to work on.