Hacker News new | past | comments | ask | show | jobs | submit login
How many keywords I can fit into a single C# expression? (tabsoverspaces.com)
110 points by abbeyj on Feb 7, 2019 | hide | past | favorite | 91 comments

I got to 14 (now 15!) in javascript so far:

    async function* foo() {
        return yield delete void await null in this instanceof typeof new class extends async function () {} {}
(not sure whether the code needs to successfully run? But it at least parses)

What's really fun is seeing how babel transpiled the above function:


EDIT: here's an explanation https://medium.com/@bluepnume/how-many-keywords-can-you-plac...

Well, you are correct that that parses. But how? I'm with it right up to around "new class extends"; `new` I'm okay with, but doesn't `new` require a symbol (the name of the type to allocate) after it? I tried parsing "class extends async function" as an expression that boiled down to a type that we could new, but I get lost at "class extends" — don't we need a class name there? (Also, I didn't think `class` could be used in an expression…)

Here it is with line breaks and as many extra legal parens as I can add without redundancy, showing all the unary and binary operators involved:

  return (
    yield (
      delete (
        void (
          await (
            (null) in (
              (this) instanceof (
                typeof (
                  new (
                    class extends (
                      async function () {}
                    ) {}
`class extends X {}` is an anonymous class extending X. Syntactically, X is an arbitrary expression, which `async function () {}` is. At runtime, X must be a constructor on pain of TypeError, and an async function isn’t; this could be considered grounds for disqualification, as under no circumstances will it execute without exceptions.

`new` does not require an identifier or path to follow it: it can be an expression. So `new (foo)` is fine. It’s just kinda greedy as regards an open paren of a function call: `new foo.bar(baz)(quux)` is equivalent to `(new (foo.bar)(baz))(quux)` rather than any alternative grouping. Remember also that `new foo` is legal, and equivalent to `new foo()`.

TIL class expressions and anonymous classes are a thing that exists in JavaScript.

It’s the same thing as function expressions and anonymous functions, but with classes instead of functions.

Does it mean that you're creating an anonymous new class that inherits from an anonymous async function, since it's really prototype objects all the way down in JS?

In Chrome:

  > (new class {}).__proto__
  {constructor: ƒ}

I think it works out to:

  return yield delete void await(
    (null in this) instanceof typeof(
      new (class /*anon*/ extends (async function () {}) {})
Maybe? Is `await foo in bar` a special case in javascript?

null is technically not a keyword in JS[1]. And fun fact, neither are true, false, undefined, NaN, let, of[2], static[3], as[4], from[4], get[5] and set[5].

[1] https://www.ecma-international.org/ecma-262/9.0/index.html#p...

[2] as in `for (foo of bar) {}`

[3] as in `class { static foo() {} }`

[4] as in `import {foo as bar} from 'baz'`

[5] as in `{get foo() {}, set foo() {}}`

It runs successfully in Chrome returning:

  foo {<suspended>}
That's the craziest statement I've ever seen!

That's because a generator is being returned from `foo` (the yield syntax gives you this).

Try calling next() on the return value of foo.

I get:

  Promise {<rejected>: TypeError: Class extends value async function () {} is not a constructor or null
    at foo

You can slip `void` in after delete an indefinite number of times. You can also do an indefinite number of `new class extends class extends class extends class {} {} {} {} {}`.

Good point! Let me update.

I was going for unique keywords with no repetitions.

Yes, according to the OP (in a comment) it has to be unique keywords.

This looks like something a fuzzer would come up with.

If you are in a module context, you could add "export" in front of the whole thing.

The count is from `return` on; the `async function* foo() {` part is excluded due to the `* foo() {`. Thus `export` isn’t applicable.

That's only 4, can't be broken by parentheses or braces

I assume you are on mobile - keep scrolling right on the code, some reason HN doesn't wrap text on mobile.

> fit into a single C# expression

Mostly unrelated - but this reminds me of a project I did years ago to put an entire raytracer in a single C# LINQ expression.


This is awesome!

I have been warned that LINQ is often inefficient compared to raw C# with the same functionality -- but I have my doubts as the MS C#/.net put quite a lot into LINQ efficiency under the hood. Did you test this against any raytracing setups that don't use LINQ?

LINQ isn't really inefficient as such -- that's not really how I would put it. When used correctly, the performance is pretty good and it buys you elegance and a massive amount of expressive power with relatively little performance tradeoff. I use LINQ everywhere in my code and the maintainability gains are palpable.

It does have overhead, but it shouldn't be avoided for that reason unless you're really trying to squeeze performance gains out of your code. Eric Lippert, the designer of LINQ, has this to say [1].

[1] https://stackoverflow.com/questions/3769989/should-linq-be-a...

Though my understanding is that if you do something like this:

var a = new double[10000];

var b = a.Select(x=>x*x).ToArray();

I'd expect LINQ to create a List or a similar structure when executing the select, then copy the output to the final array. If you were operating on two arrays directly, I would expect less memory allocations (unless these things get optimized away).

No. The Select-statement is not evaluated until necessary, i.e. until ToArray() is called.

While your comment is true, I'm not sure how it's relevant. `ToArray` is still operating on the `IEnumerable` produced by Select, which doesn't tell you that the Select is itself operating on an array.

The CLR does do some type checking to try to propagate the size if it can, but that's by no means guaranteed.

Edit: In skimming the source available on sourceof.net, it looks to me like the array doesn't actually get propagated far enough, so the construct in GP will in fact incur several unnecessary array copies.

I'm curious what you saw. My understanding was that it's mostly generators, i.e. it's done through yields, not extra allocated arrays.

It's `ToArray` itself that's the problem. Since it doesn't know how big the array will be in the first place, it has to allocate a small array, start iterating, copy everything into a bigger array each time it runs out of space, and finally allocate an array of exactly the right size and copy everything into that.

You can see the `ToArray` implementation at [0], which defers the implementation to [1]. The implementation checks for ICollection to get the exact size, but the type [2] that `Select` returns doesn't implement ICollection, so `ToArray` has to fall back to the less efficient algorithm.

[0] https://referencesource.microsoft.com/#System.Core/System/Li...

[1] https://referencesource.microsoft.com/#System.Core/System/Li...

[2] https://referencesource.microsoft.com/#System.Core/System/Li...

That's more or less what I had in mind without being certain of the details.

Was trying to open your links but I am served an expired tls certificate from Microsoft!

Oddly enough it seems that the certificate just happened to expire today in between the posting of my first and second comment. I didn't mention it only because I'm at work and couldn't be 100% certain that it's not my organizations security service messing with things.

Ugh I made a mistake -- Eric Lippert isn't the designer of LINQ. That would be Erik Meijer [1].

[1] https://en.wikipedia.org/wiki/Erik_Meijer_(computer_scientis...

Of course I'd never use LINQ inside a tight loop in the rendering path of my Unity3D projects, that's not what it is built for, but in my setup and project management code it is a god-send for expressiveness and maintainability.

LINQ doesn't optimize that much. If you're using LINQ on a database, you may benefit from the underlying database and driver's optimizations. However, if you're just operating on lists and in-memory data structures, you are probably eating the overhead.

As an example, a .Where(...) on a List<...> object is exactly what it claims to be: a linear pass through the list. Sometimes that's exactly what you need. Other times, it's more elegant and fast enough to represent an operation as a list comprehension. Finally, sometimes you really do need to eke out more performance. It's only the last case where LINQ is a bad idea.

> As an example, a .Where(...) on a List<...> object is exactly what it claims to be: a linear pass through the list.

Yes, but it uses deferred execution, which means that it returns enough information to perform the filter, it doesn't really do much until you start to enumerate the list. If, for instance, you continue to do a Take(5) on the result, the filter is only applied until it finds 5 elements that satisfy the filter (or until there are no more elements).

The thing about Linq efficiency is that under the hood it’s setting up a state machine with enumerators and continuations to be fully general. It’s about as fully-optimized as it could possibly be given the design constraints, but it will always be less efficient than just iterating over a collection directly.

But the worst performance hits in Linq come when people don’t know how to use it. The biggest culprit is unnecessary materializations, e.g. ToList() or anything that requires traversing the entire enumerator like All(), so if you’re careful to avoid the obvious pitfalls you should be fine.

That's... not really true of LINQ, specifically. I have seen this assumption from developers before, with regard to LINQ-to-objects, that 'the compiler does clever stuff to optimize LINQ expressions', but... no, not really. It compiles a series of method calls. The JIT might do some optimization. But it's not a query optimizer - it just does what you tell it, and underneath it all there's nothing but an enumerator.MoveNext() getting called repeatedly.

Setting up state machines and continuations is something C# does when you write generators (with yield) and async methods (with await), but LINQ doesn't do that, particularly.

LINQ the language feature in particular doesn't do any of that sort of thing. As a language feature, LINQ is just the 'query comprehension' syntax, which is the 'from x in y where bar select z' coding form. And from the language's point of view that is just a different way of writing y.Where(x => bar).Select(x => z), which it will then try to compile. If it happens to wind up calling generator methods which implement state machines and continuations, or just simple methods that return ordinary objects, LINQ doesn't care, so long as the types all line up.

The only other thing that confuses matters with LINQ is that those lambdas it wants to compile can match method signatures that have delegate types, or Expression<> types, in which case the lambda doesn't get compiled as executable code, but rather as an expression tree literal, which is how Linq to SQL and friends were built (but again, that's not a 'LINQ' feature - you can assign or pass a lambda as an Expression<> yourself without involving 'LINQ'). And THAT is where LINQ gets a lot of its worst reputation, because that turns out to introduce a ton of complexity and failure modes that aren't the fault of the C# language, except in as much as it made it possible for a library to get involved in interpreting your code's syntax tree at runtime, which may have been a mistake.

You can always use LinqOptimizer to improve performance: http://nessos.github.io/LinqOptimizer/index.html

Not with keywords but this reminded me of Black Perl : https://en.m.wikipedia.org/wiki/Black_Perl

Is there a formal grammar? You could probably plug that into a SAT solver and not have to guess.

How would you use a SAT solver? Do you have a cleverer way than generating an expression in a breadth first search?

Here's one with 8 keywords:

    abstract class A
        internal protected abstract ref int X();

    class B : A
        unsafe extern internal protected override sealed ref int X();


    case x of _ -> True 
is a valid expression of type boolean for all values of `x`.

    if b then True else False
is a valid expression if `b` is a boolean.

These can be nested recursively infinitely, but that's not very interesting, so let's move on.

    let x = z in y
this is another valid expression for any y and z values.

    do e
this is a valid expression if e is a type that implements the Monad typeclass.

Basically every other keyword is for type declarations or module importing, and those both aren't expressions, and seem much harder.

So, my final expression is:

    let x = x in do case if let x = x in True then False else False of _ -> return False
where `in do case if let` is my longest expression.

You can add `infixl` or `infixr` to that:

    let x = x in do case if let infixr 7 `x`; x = x in True then False else False of _ -> return False
And with the RecursiveDo extension, you can throw `mdo` in there:

    let x = x in mdo do case if let infixr 7 `x`; x = x in True then False else False of _ -> return False
That brings us to `in mdo do case if let infixr` as the longest string of consecutive (distinct) keywords that I can come up with in Haskell.

Given the rule:

  expr := 'if' expr 'then' expr 'else' expr
An expression may start with a sequence 'if if if if ...' of arbitrary length (and will contain that same number of 'then' and 'else' keywords, although not as a sequence).

A simpler variation of the same concept is to use the fact that for every expression `expr`, `do expr` is a valid expression, allowing you to start a sequence with any number of `do`s.

But the challenge as I see it is to stack as many distinct keywords in a row. In javascript you can do the same thing with `await`: for every expression `expr`, `await expr` is a valid expression, so you can start with a sequence of any number of `await`s. That's great, but ultimately not a challenge: the real puzzle is in stacking as many unique keywords as possible.

> do e > this is a valid expression if e is a type that implements the Monad typeclass.

That is not necessary,

  do do do ()

> where `in do case if let` is my longest expression.

Not every string of tokens is an expression.

I can do 14 using parenthesis:

    case default(string) when await unchecked(nameof(value) as dynamic) is checked(sizeof(decimal)):

Missed the rule about nothing else but spaces in between. Can be brought up to 8 in the latest C# 7.x by adding another `is true` after `is false`.

`case null when await this is false is true:`

Actually, unless you count only distinct keywords, this can be extended indefinitely.

He did not even try too hard.

You can, obviously, add all of the standard type names and both true and false to your expression easily via ||. Add an out and ref too.

Also, if you put a local function into an async lambda (not sure if that's possible), you can add all the control flow operators, and using too.

The only things you won't be able to fit there, IMHO, are type keywords (class, interface, etc), namespace, visibility modifiers, static, abstract, virtual, override and operator.

Actually, it might be easier to tell which keywords you can't fit into an expression.

The challenge the post sets is to line up as many keywords as possible in sequence, without separating commas, semicolons, parens, braces, symbolic operators, etc.

So yes, of course if you ignore the constraints on the problem it becomes much easier.

The challenge description has an example using parentheses, and doesn't explicitly prohibit them:

> yield return sizeof(...)

The part submitted for scoring is just "yield return sizeof", that's why he says it scores 3. The keywords can be surrounded with more code to make them compile, but none of it counts for the puzzle.

I agree. I wonder whether he consider an "expression" up to the place where another expression can be placed e.g. && combines two expressions into a third one.

As someone who briefly used C# back in the days of .NET 2.0 (and found it was not much different from Java at the time), it's surprising how much has been added to the language, and my main question is thus (assuming this is a valid expression) "what does it mean?" Especially the non-constant in a switch case.

C# 7 introduced pattern matching in a number of contexts, including switch statements. It's not as elegant or powerful as F# or some other functional languages but still quite useful.

By my count that's only six keywords as case is a keyword, but not part of an expression.

Giving Python a try was fun~

>>> not None and True or [False for _ in dict() if print(lambda: int is str)]

But [] isn't really a keyword, and `dict`, `print`, `int` and `str` (and `_`) are definitely not keywords (they're just function/type names/symbols; they can be shadowed, for example).

"How many keywords can I fit into a single expression" and "How many 'blue words' can I put consecutively with only spaces between them" (as described in the post) are two pretty different challenges. With only distinct keywords only separated by spaces, best I could do was 10:

    def _():
      yield from None if not True is False else lambda :_

up to 11! (parses on 3.7.2)

  async def _():
    async with await None if not True is False else lambda: _:
it's a shame that Python has so few expression keywords... `yield` can be used as an expression, but it seems to need parentheses :(

C# is a mess. I'd love a C# light.

First follow Great Scott's instructions for making an Arduino capacitative switch light.


Then enable it to be integrated with C# code:


Voila! A C# light!

What for? You can write code C# 2.0 style if you want to.

Really? C# seems very balanced and coherent to me. What seems messy to you?

The messy part of C# is the way it does function overloading. (Speaking as someone who likes C#.)

Why do you think function overloading is messy?

I’m not making any claims about function overloading in general. Only C#.

It interacts in surprisingly complicated ways with a surprising number of other parts of the language. It causes problems with prospective new features. IIRC there have been a number of bugs in the spec and in the compiler wrt overloading. Given the overall high level of expertise of people working on the spec I would say it’s a bit of a minefield, compared to the rest of the language.

Edit: Found a Q&A by John Skeet, https://youtu.be/8UvTdobOiJk?t=1292

“Overload resolution is the nexus that every feature ends up contributing to. […] All of these things make overload resolution and type interface really really really complex and it’s always a bit of the C# specification that I found hardest to understand […] and it turns out so did the spec designers, because they got it wrong, and we’ve been trying to fix it.”

It might be tricky from a standards point of view, but as a user of the language, I don't think I've ever had an issue with how it handles overload resolution. Every once in a while you get an "call is ambiguous" compiler error, but I've always found the "ambiguity" to be reasonable and easy enough to fix (by making generics explicit or using an explicit cast). It's also a very minor bummer that you can't overload on return type, but that is A LOT to ask of a language.

Generally speaking, I've found C# to be a very pleasant language to work in, by far my favorite in that particular "lane" of computer languages (i.e. annoyingly object-oriented enterprise languages, like C#, Java or C++).

Thanks for the detailed reply. There’s lots here I had not considered. I’ve realized my overloading implementations have been rather simple cases.

I sort of agree, it's OK if you have a history with it because all these features are incremental. However if you are a beginner then it could look overwhelming.

JavaScript has the same problem but is much worse.

It seems we don't know when to stop with this stuff.

You can set the language version and go back to pre linq, pre async days.

C natural? (I tried to type U+266E but it didn't save)

Why not C-flat? We already have a C--, to pair with C++.


(Note to the pedants: Although C-flat and B are enharmonically equivalent they are not the same.)

I'd just like to throw C#m into the mix!

A mess in what way?

So many features have been added over time, I think it's beginning to show that the features are piled on top of a language that weren't designed for them.

Things that I think feel awkward are the tuples handling, switch case pattern matching. As a go developer, when I see the Func, Action, Expression, and Predicate objects... oof I shudder. When you layer function overloading on top of those, you can get in a mess.

Obviously it's an unpopular opinion, but I would like to see what c# would look like if it was designed from scratch again.

I tend to think F# is what C# would look like if designed from scratch these days. My take is that C# started out not very good because it copied a not very good language (Java) but they improved on Java a bit and have continued to add things that tend to improve the language within the constraints of remaining backward compatible.

Choosing just on the language I wouldn't choose C# but when working in an ecosystem built around C# (like Unity for me currently) I appreciate many of the features as making things a bit nicer.

I disagree. First of all, F# is functional and therefore has another design. C# also intentional followed C style languages to pick up developers. C# was built for the masses while functional programming is for the elites (F# is has 10k devs using it, VB.NET has 100k and C# more than a million active users). I agree that there are some syntax they would have designed differently when they would have understand the functional programming influence they have today a bit earlier.

When they would design it today from scratch, I think they would look at JavaScript.

C# comes from a statically typed language tradition (the dynamic keyword aside) and has very little in common with Javascript beyond the surface level of curly braces. The trend in C# features seems very much influenced by functional programming, adapted to be more familiar and palatable to non functional programmers.

Some features of C# that seem influenced by functional programming in general or F# in particular include lambdas, type inference, Enumerable (F# pipelines), pattern matching, tuples, null coalescing (a Maybe if you squint), expression bodied members, record types and Range expressions. There is also what looks like an 'aesthetic' influence of F# in language features that reduce ceremony and make for more concise code like auto properties as well as a functional inspired recommended practice of making immutable types.

Fair assessment and I agree.

My comment regards JavaScript was more about to pick up existing developers. C# was always meant for productivity. A rapid learning curve (as opposed to learning functional programming first) is very important here.

F# wasn't created from scratch at all, it's the last descendant of the ML language family. It was originally developed with some compatibility in mind. So no, a hypothetic today's C# wouldn't look like F# if is wasn't intentionally functional.

I didn't say that F# was created from scratch, I'm familiar with its heritage. My point was that C# started as a slightly improved Java but the trend in new language features, particularly in recent years, has shown a strong influence of functional programming in general and F# in particular. Hence the idea that if in some hypothetical universe C# was making changes not constrained by backwards compatibility I would expect it to look more like F# and less like Java.

In that hypotetical universe, without Java history, Microsoft would be focusing on VB and C++, with COM Runtime having replaced COM+.

Quite the contrary, as someone who has been doing C# since it was invented, I find all of the new features to be implemented in a very coherent, even respectful way. It can seem like things are arbitrarily tacked on, but once you start using the features in earnest, it becomes quite clear that they are very well designed, very well thought out. I have much, much respect for the C# language committee. They've done amazing work in adding features and keeping the language clean and forwards compatible!

Well said. I am also on that journey and compared to C++ the complexity is still very low and readability very high. I think the only C# feature I have not cracked yet is co/contravariance. And that is more due to the fact, that you rarely need to declare it.

I generally like the new features of C# but I still find C++ to be a much more coherently designed language. C# can't escape its poisoned roots in Java despite its best efforts.

Out of curiosity, what do you find poorly designed about them? In my opinion many of these things are elegant to the point of self documentation. Switch case pattern matching in particular is just great:

        void foo()
            object pi = 3.14d;

            switch (pi)
                case int i:
                    Console.WriteLine("pi is an int of value " + i);

                case float f:
                    Console.WriteLine("pi is a float of value " + f);

                case double d:
                    Console.WriteLine("pi is a double of value " + d);

                    Console.WriteLine("pi is of an unknown type.");
Elegance is of course in the eye of the beholder, so what would you propose as an improvement to something like this?

Well, C# 8 will be having something much nicer syntax actually:

        void foo()
            object pi = 3.14d;

            Console.WriteLine(pi switch 
                int i => "pi is an int of value " + i,
                float f => "pi is a float of value " + f,
                double d => "pi is a double of value " + d,
                _ => "pi is of an unknown type.";
Check-out https://neelbhatt.com/2018/05/19/c-8-0-expected-features-par... Cheers!

To some degree I agree but I am pretty impressed how much they have been able to add while still keeping C# a pretty clean language. I am sure most language designers dream about starting from scratch....

Like any product or human languages, programming languages either evolve to make it easier to deal with the complexity of the problems their users need to tackle, or they fade away.

Every programming language that advertises itself as simple, it isn't any longer simple if it actually is adopted by the market at large.

Go is the newest example, already with a few quirks, needed to revamp their dependecy story, the team finally cave in to look into better solutions for error handling and generic programming.

> Go is the newest example, already with a few quirks

Here's a fun game for people who use Go: why does this error?


Applications are open for YC Summer 2021

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact