Skip to content

Instantly share code, notes, and snippets.

@dsyme
Last active December 31, 2022 05:54
Show Gist options
  • 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.

@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