Skip to content

Instantly share code, notes, and snippets.

@dsyme
Last active December 31, 2022 05:54
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save dsyme/32de0d1bb0799ca438477c34205c3531 to your computer and use it in GitHub Desktop.
Save dsyme/32de0d1bb0799ca438477c34205c3531 to your computer and use it in GitHub Desktop.

Quick notes on a rant

Some examples related to my tweet rant https://twitter.com/dsymetweets/status/1294276915260522496

1. Implicitly discarding information is so 20th Century

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" and is almost as corrosive to accurate programming as null pointers. Similarly in a C# notebook there is a subtle difference between

System.AppDomain.GetCurrentThreadId()

and

System.AppDomain.GetCurrentThreadId();

The first evaluates an expression and displays it, the second evaluates the expression and discards 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.

2. "No object expressions" (was "no way to implement interfaces or abstract classes using an expression, meaning stupid extra classes")

Link: F# Object expressions

Let's say you want to implement IComparer<T> (or some other interface where there's no existing way of doing it).

You have to do this:

    Array.Sort(candidates, new SortByRelevanceAndOrder<(Type type, string mimeType, int index)>(tup => tup.type, tup => tup.index));

    private class SortByRelevanceAndOrder<T> : IComparer<T>
    {
        Func<T, Type> _typeKey;
        Func<T, int> _indexKey;
        public SortByRelevanceAndOrder(Func<T, Type> typeKey, Func<T, int> indexKey)
        {
            _typeKey = typeKey;
            _indexKey = indexKey;
        }
        public int Compare(T inp1, T inp2)
        {
            var type1 = _typeKey(inp1);
            var type2 = _typeKey(inp2);
            var index1 = _indexKey(inp1);
            var index2 = _indexKey(inp2);
            if (type1.IsRelevantFormatterFor(type2) && type2.IsRelevantFormatterFor(type1))
                return Comparer<int>.Default.Compare(index1, index2);
            else if (type1.IsRelevantFormatterFor(type2)) 
                return 1;
            else 
                return -1;
        }

    }

The class is pointless and shouts out "C# can't create object instances in expressions!". Instead you should be able to implement an interface using an expression, e.g. like F# object expressions

    let comparer = 
        { new IComparer<_> with 
            member x.Compare((type1: Type, index1: string), (type2, index2)) =
              if (type1.IsRelevantFormatterFor(type2) && type2.IsRelevantFormatterFor(type1))
                  return compare index1 index2
              else if (type1.IsRelevantFormatterFor(type2)) 
                  return 1
              else 
                  return -1
        }
   
    Array.Sort(candidates, comparer)

There are hundreds of examples of this (many of them involving significant variable capture too) and the absence of this feature makes C# very problematic as a mixed functional-object language. You can be functional with delegates, or write classes the old way, but implementing functional objects in functional code is very contorted, verbose and error prone.

Note this is not "inner classes" of Java which is a complexifying, not simplifying beast.

You should not do functional-object programming in a language without this feature.

3. "No implicit construction for classes"

This is well known (they tried to get it in to C# 6) but basically why write all this

    private class SortByRelevanceAndOrder<T> : IComparer<T>
    {
        Func<T, Type> _typeKey;
        Func<T, int> _indexKey;
        public SortByRelevanceAndOrder(Func<T, Type> typeKey, Func<T, int> indexKey)
        {
            _typeKey = typeKey;
            _indexKey = indexKey;
        }
        public int Compare(T inp1, T inp2)
        {
            var type1 = _typeKey(inp1);
            var type2 = _typeKey(inp2);
            var index1 = _indexKey(inp1);
            var index2 = _indexKey(inp2);
            ...
        }
    }

when you could write this (defining a constructor implicitly)

    private class SortByRelevanceAndOrder<T>(Func<T, Type> typeKey, Func<T, int> indexKey) : IComparer<T>
    {
        public int Compare(T inp1, T inp2)
        {
            var type1 = typeKey(inp1);
            var type2 = typeKey(inp2);
            var index1 = indexKey(inp1);
            var index2 = indexKey(inp2);
            ...
        }

This means that when doing either object-programming or functional-object programming you have to continually manually populate the fields of a class and makes it harder to fluently move code from expressions to members.

This is a chronic problem for C# when writing any kind of object programming code. C# programmers are always field-converting by hand as a result.

This feature has been in many languages since the 90s (I'm not sure of the first - OCaml had it ~1994). In my opinion, any object-programming language should support this feature in the 21st Century.

4. "No list expressions, including generating lists" (making HTML DSLs a mess among other things)

Here's an example of a C# DSL for HTML generation. It's ok C# (the problem is the language not the code)

Note: to all you people saying "we don't need a HTML DSL - we've got Blazor!" please remember this rant occured in the context of a project that used a HTML DSL, and would not have remotely benefitted from using Blazor. Also, the question isn't whether C# needs a HTML DSL - the question is whether C# has sufficient features to be "good" at language-embedded data-generating DSLs. It really doesn't matter if this is HTML or a myriad od other data-generating constructs.

    var headers = new List<IHtmlContent>();
    headers.Add(th(i("index")));
    headers.AddRange(df.Columns.Select(c => (IHtmlContent) th(c.Name)));
    var rows = new List<List<IHtmlContent>>();
    var take = 20;
    for (var i = 0; i < df.Rows.Count; i++)
    {
        var cells = new List<IHtmlContent>();
        cells.Add(td(i));
        foreach (var obj in df.Rows[i])
        {
            cells.Add(td(obj));
        }
        rows.Add(cells);
    }
    
    var t = table(
        thead(
            headers),
        tbody(
            rows.Select(
                r => tr(r))));
    
    writer.Write(t);

Note that we've had to create mutable data for collections along the way. This is because C# doesn't have anything like F# list/sequence/array expressions beyond "C# iterator methods" which would be way overkill for this kind of generation. Compare with this:

table [] [
  thead [] [
    th [] [ str "Index" ]
    for c in df.Columns do
      th [] [ str c.Name]
  ]
  tbody [] [
    for i in 0 .. int df.Rows.Count do
      tr [] [
        td [] [ embed i ]
        for o in df.Rows.[int64 i] do
          td [] [ embed o ]
      ]
  ]
]

Note that

  1. this is an expression

  2. no mutation

  3. squint and it looks like HTML (used widely in F# server-sdie and client side code)

  4. irregular list generation is supported (you can emit a header, then do a loop)

  5. conditional fragments of list generation supported

  6. same syntax for computing arrays, lists, sequences

There is some syntax in C# using ParamArray (params) arguments extensively plus also LINQ, or collection initializers, or even LINQ queries. But

  1. this tends not to cope with truly generative situations where elements are in the collection conditionally, or loops are used, or collections are appended together.

  2. in practice this needs to use a really contorted mix of language features, and still won't look remotely like HTML.

The fundamental feature missing here is a lightweight way of building immutable list/collection expressions, including generatively.

Strong language features for succinct immutable data generation are essential for client and server programming today.

dynamic is hell

Well, it is.

@jcolebrand
Copy link

I agree on #3 that's kind of dumb that it doesn't have that yet. How do you handle multiple constructors? is this just for the default case and when you have more than one you just have to define them all? It also seems like this would solve #2 tbh

#4 is not how I would solve this problem anyways, I would use a templating language. Still gotta embed some loop constructs but something like mustache is pretty simple. It's a bit more object oriented but it's designed for rendering html from an object, so it's designed to solve this specific problem domain.

#1 you keep saying notebooks. Do you mean in Code? Cos that's not programs, that's more interactive right? That's not really a language failure in that case, just a really similar use-case that trips people up.

I agree about dynamic being .. something haha. The same concept is a problem in C, in JS, in most languages. Building an untyped language on the fly is always prone to risk.

@aviatrix
Copy link

Writing HTML over C# is like painting a picture over the phone.

@rubber-duck
Copy link

A thing that annoyed me previously (I haven't been in C# land for a while so maybe this was fixed) was that there's no way to express operator constraints on generics - so for example there is no way to say :

public int SomeGenericCalculation<T>(T a, T b) { return a + b; }

I'm guessing they could convert operator overloading to implicit implementations of IOperatorAdd<LHS, RHS, Result> or something like that. I encountered plenty of such small "ergonomic" annoyances - it's just a verbose language but for the most part it gets the job done and improves on things tremendously from Java ~6 days which was it's direct competitor.

@dsyme
Copy link
Author

dsyme commented Aug 15, 2020

#1 you keep saying notebooks. Do you mean in Code? Cos that's not programs, that's more interactive right? That's not really a language failure in that case, just a really similar use-case that trips people up.

See https://devblogs.microsoft.com/dotnet/net-interactive-is-here-net-notebooks-preview-2/ plus any Jupyter notebooks or C# scripting

@jcolebrand
Copy link

#1 you keep saying notebooks. Do you mean in Code? Cos that's not programs, that's more interactive right? That's not really a language failure in that case, just a really similar use-case that trips people up.

See https://devblogs.microsoft.com/dotnet/net-interactive-is-here-net-notebooks-preview-2/ plus any Jupyter notebooks or C# scripting

Yeah but you keep saying that that's a language problem that it consumes the content, but that appears to me as an operator error, albeit an almost impossible one to catch unless you're familiar. I'm not sure how that becomes a language problem that dropping the semicolon on the end changes behavior.

I agree that it's annoying.

Also, now I need to go back and revisit this and see if I can use this in lieu of Linqpad, cos if this replaces that then I truly am closer to a single IDE for all my day-to-day, which would be perf. Especially since it's cross-platform and I work as often on OSX as I do Windows. <3

@jcolebrand
Copy link

A thing that annoyed me previously (I haven't been in C# land for a while so maybe this was fixed) was that there's no way to express operator constraints on generics - so for example there is no way to say :

public int SomeGenericCalculation<T>(T a, T b) { return a + b; }

I'm guessing they could convert operator overloading to implicit implementations of IOperatorAdd<LHS, RHS, Result> or something like that. I encountered plenty of such small "ergonomic" annoyances - it's just a verbose language but for the most part it gets the job done and improves on things tremendously from Java ~6 days which was it's direct competitor.

I'm pretty sure that what you're suggesting is now perfectly doable. I'm not sure that in this case it would auto-convert to an Int32 return type, you would probably need to handle that somewhere, but some operator overloading on T would be easy enough. Probably take a little work for the operator definition. I'm not sure where generics comes into play on that.

Overall tho @rubber-duck, the language is leaps and bounds over where it and Java ~6 were years ago. It's durn near a new language to be honest. A lot of lessons-learned have gone in (esp since Typescript became a thing) and while it's getting easier to shoot yourself in the foot semantically, overall it's a nice and easy language. I basically only work in js/sql/c# and powershell these days, but I still dabble in other languages for fun.

@egregius313
Copy link

Yeah but you keep saying that that's a language problem that it consumes the content, but that appears to me as an operator error, albeit an almost impossible one to catch unless you're familiar. I'm not sure how that becomes a language problem that dropping the semicolon on the end changes behavior.

@jcolebrand I think the bigger issue here is that non-void expressions do not produce a warning when used as statements. This leads to the potential bug of accidentally ignoring doing anything with their results. Both allowing it and disallowing it is an annoyance. I've had C compilers complain when I don't use the return value of a function which produces an effects, which can be bothersome. But warnings like that help avoid a certain class of bugs.

@jcolebrand
Copy link

Oh, it's been a while but I thought there was a compiler warning on ignored return values, hence my question about if it was just an annoyance in Jupyter notebooks.

I've always thought forcing an assignment was bad practice and preferred being able to throw away values I don't need. It's handy for like if(!T.TryParse(val)) evaluating as truthy so I know I can't parse it instead of having to use three lines for the same thing.

I'm also not new to the language, so it's just old hat at this point. 🤷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment