Skip to content

Instantly share code, notes, and snippets.

@medigor
Forked from dsyme/rant.md
Created August 14, 2020 19:30
Show Gist options
  • Save medigor/0e902e55ed145b6e30297cdbe990dd40 to your computer and use it in GitHub Desktop.
Save medigor/0e902e55ed145b6e30297cdbe990dd40 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)

    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.

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