Of all things, a sci-fi novel recently got me thinking about the importance of notation and how it influences our thinking. In short, syntax length impacts the kinds and sophistication of ideas.

The sci-fi book was Canticle for Leibowitz. Some scholars of a regressed humanity discover an ancient document with sum notation. They marvel at the concise representation of so much information and wonder at all the ideas they hadn’t even considered that could be explored.

I’ve long learned that we should program into langauges in not in them. In other words, create your design based on your needs and figure out how the language can achieve it instead of basing your design on available language features. I also have long loved static language. Popular static languages like Java and C# have developed a reputation of lots of “ceremony” or unnecessary syntax to express an idea. Many have turned to dynamic languages because of this. This is a false dichotomy, however. Static languages can also have low ceremony. F# is a great example.

Still, the concern over ceremony is valid.

The longer or more effortful an idea is to express, the less likely we are to express it. Just look at natural languages. Most anything we say often quickly gets a shortened form. However, programmers can’t abbreviate syntax like we do with words. Instead, ideas with inconvenient syntax don’t get expressed as often. Longer syntax can also take more time to understand, and understandability is one of the most important factors for long-term maintainability of a system.

The Sapir-Whorf hypothesis suggest that the language we use influences and may be limit the thoughts humans have.

This certainly seems to be important in math and scientific fields. The effectiveness of notation is often related to the growth of ideas. Ideas can grow more complex by reducing current ideas into more terse communication. I’m thinking of examples like chemical notation. Using prose to describe the states and relationships of atoms is much less efficient than the notation we use to show elements and bonds. This itself requires the terse element notation of the periodic table. We’ve also taken chemical notation further with special representations for common combinations, like Benzine rings. Each of these steps in notation cemented a shared concept in a way that enables more efficient communication, which in turn enables us to reason about more complex ideas.

To clarify, I don’t think shorter is always better. Many code understandability violations are made in the name of conciseness. Understandability is of first importance. Conciseness can be an aid understandability, and clear concise notation can lead to new ideas and expressions. However, never be more concise than clear.

Programming Samples

Here are a few examples where short syntax has effected how I program.

Unions

F# has union types, which represent a set of alternatives that don’t necessarily have the same data.

1
2
3
4
type PaymentTypes = 
| CreditCard of CardNumber * SecurityCode * Expiration * NameOnCard
| ACH of (AccountNumber * RoutingNumber)
| Paypal of IntentToken

C# has no concept of unions, and the intent of the alternatives is not very clear with traditional class syntax. Thus, I rarely used union types in my C# even though I like using them in F#.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class PaymentType{}

class CreditCard : PaymentType{
    public readonly CardNumber CardNumber {get; init;}
    public readonly SecurityCode CVV {get; init;}
    public readonly Expiration ExpirationDate {get; init;}
    public readonly NameOnCard Name {get; init;}

    // constructor isn't strictly necessary, but it helps with concise initialization later on
    public CreditCard(CardNumber cardNumber, SecurityCode cvv, Expiration expirationDate, NameOnCard name){
      //...
    }
}

class ACH : PaymentType{
    public readonly AccountNumber AccountNumber {get; init;}
    public readonly RoutingNumber RoutingNumber {get; init;}
    
    public ACH(AccountNumber accountNumber, RoutingNumber routingNumber){
      //...
    }
}
class Paypal : PaymentType{
    public readonly IntentToken Token {get; init;}

    public Paypal(IntentToken token){
      Token = token;
    }
}

However, C# introduced positional records which allow a concise approximation of union types. Now I often use union-like records in C#.

1
2
3
4
5
6
7
record PaymentType{
    public record CreditCard(CardNumber CardNumber, SecurityCode CVV, Expiration ExpirationDate, NameOnCard Name) : PaymentType();
    public record ACH(AccountNumber AccountNumber, RoutingNumber RoutingNumber) : PaymentType();
    public record Paypal(IntentToken Token) : PaymentType();

    private PaymentType(){} // private constructor can prevent derived cases from being defined elsewhere
}

Effective pattern matching is also key to concisely consuming union-likes. C# previous would have required a bunch of type checks and casting. Now it can be done like this

1
2
3
4
5
6
7
public void HandlePayment(PaymentType paymentInfo){
    paymentInfo switch {
        CreditCard cardInfo => //...
        ACH checkInfo => //...
        Paypal paypalInfo => //...
    };
}

I have a whole post about unions in C#.

Function Composition

In F#, there is an operator for composing two functions. That is, to create one new function that does the same work as the two original functions.

In F# we can compose three functions together like this.

1
2
3
4
5
let aToB (param: A) : B = //...
let bToC (param: B) : C = //...
let cToD (param: C) : D = //...

let aToD = aToB >> bToC >> cToD

In C#, achieving just that last line of F# looks like

1
2
3
public D AToD(A param){
  return CToD(BToC(AToB(param));
}

Here the difference is not just density, but also order. F#’s composition operator allows the composition to be read left to right with no scope tracking. C#’s approach requires the user to read from the inside out of a series of nested scopes.

If we compose multi-argument functions the difference becomes even more significant. F# can partially apply information known beforehand

1
2
3
let aToB (config: Config) (param: A) : B = //...
let config = //...
let aToD = aToB config >> bToC >> cToD

C# would have to create additional functions to do the same. Something like

1
2
3
4
5
6
7
8
9
public B AToBWithConfig(Config config, A param){
    //...
}

var config = /*...*/;
var AToB = (a) => AToBWithConfig(config, a);
public D AToD(A param){
  return CToD(BToC(AToB(param));
}

Passing functions

F# uses a sophisticated type inference system. As such, function parameters often don’t need any explicit typing but remain strongly typed.

This makes it very simple to write functions that compose other functions.

1
2
3
4
5
6
let tryWithFallback fSuccess fError input = 
    try 
        fSuccess input
    with 
    | e ->
        fError input e

C# can pass functions as parameters, but infers far fewer types. As such, functions that take other functions tend to be verbose and difficult to read. It takes a much more compelling case for me to write higher-order functions in C#.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public TOut TryWithFallback<TIn,TOut>(Func<TIn,TOut> fSuccess, Func<TIn, Exception, TOut> fError, TIn input)
{
    try
    {
        return fSuccess(input);
    }
    catch (Exception e)
    {
        return fError(input, e,);
    }
}

This function doesn’t make much sense in C#. It’s not really better than directly using a try-catch.

However in F#, functions like these can be composed into very clean and readable pipelines.

Conclusion

In summary, syntax length matters. Ideas that take more effort to express are less likely to be expressed. This directly impacts what design concepts we leverage. Advances in clear and concise notation also level up our ability to explore new and even more complex ideas.