Ways programming languages got better (mostly)

I was talking with an old friend the other day about how programming has changed in our lifetimes. There’s plenty of room for old people grousing about how tool chains are too complicated, abstractions are too deep, or kids today don’t know how to do X. There are however a lot of positive changes in programming languages since I started programming in the 80s. In 40 years something has to have been good right?

What follows is just one old man’s opinion and nothing is that revolutionary here but maybe it will interest someone. This is not about software engineering but more just about the smaller things that have changed in programming languages for the better and a few I’m not so sure about.

Things which have gotten better

Nullability in the Type System

A common issue in coding is that often variables can have no current value. In the old days you had a pointer in C that either was NULL or hopefully pointed to a valid address for something of the type of the pointer. There were lots of cool tricks you could do by referencing things as types other than how they were written etc but mostly we’ve moved away from this “raw” access in modern programming. Still, we are left with the problem of NULL. ie.

int *x;

X is now a variable which might point to an integer or it might point to nothing. This results in a lot of code having to be careful everytime it accesses x.

If (x != NULL) { *x = 3 } 

Etc

Keeping track of when x could be NULL and when we know that is not going to be is tedious and results in all kinds of errors and security issues. How can the language and compiler help here? 

We add nullability into the type system so that there are effectively two kinds of pointers: one that might be null and one that is never null.  It’s easy enough to show how this works in Swift

var x : Int

Here x is a reference to an integer and it’s never nil (Swift’s version of NULL). The only things which can be assigned to it are also actual values of Int.  ie.  x = nil would be an error. If we stay in this world we can’t have a null pointer exception. Unfortunately, we can’t live there all the time since sometimes we do need to deal with cases of things not existing. For example if we have an array of Int like:

Var x : [Int]

And we access the first element using .first

Var y = x.first

Y is now possibly nil since x could be empty and have no first element. The type of first is Element? Which means an Optional ( denoted by the trailing ?) of the array’s element type.

Without type inference this looks like:

Var y : Int? = x.first

So now Y is something we need to be careful with. We are back to if (y != nil) checks, but since the type system and the compiler is smart we can also use some cool language features to slip back into the safe world of non nullability.

As an example, Swift provides this nice if/let construct to check if nil and assign to a var that will be non optional for use in the block without worry.

If let y = x.first  { 
// Now Y is Int and not an optional Int?
// We know we are safe in here.
}

Swift also provides ways to easily break this safety. You can use ! to force unwrap or convert an optional to the concrete version. If you do this on an optional that is currently nil you get a runtime error just like you would with C but this option is there if you feel you need it.

Overall these changes make programming better because they allow the code to better capture what is going on so that it’s explicit both to the compiler and more importantly to any future readers of the code.

Support for Immutability by Default

In the old C days essentially all state was kept in variables. Int x held an integer called x. You could allocate space on the heap or in the stack but it was also considered to be something you were going to want to update. X = 3 then X = 5 etc. There wasn’t much thought given to values that didn’t change. A constant like PI was defined outside of the core language via a preprocessor directive. Memory was scarce and needed to be re-used if possible.  Over time it became more popular to have state that you compute once and didn’t change. This has value in threaded systems since a thread can be sure that someone else isn’t going to change the value which is being worked on.

The const directive can be added to a declaration in C to tell the compiler to not allow any changes to this value once set.

const double PI = 3.14;

Now PI won’t be able to be changed in subsequent code. This is a great step forward but modern languages tweak this in a subtle way that makes it even better. In C if you see code without the const added like:

double PI = 3.14

And you will see this. You don’t know if someone just forgot to say const or if they intend to modify PI later.

Swift and other languages do something nicer by having different keywords for declaring immutable and mutable state.

Let x : Int = 3   // x is an integer with value 3 and will change
Var y : Int = 3  // y is a mutable integer with a current value of 3.

The compiler generally tells you that you’ve used the wrong one and the code is much clearer and obvious about what is going on. If you don’t mutate y you get a message saying essentially:  “How about using let and not var”

Immutability is more common and mutation is now more explicit. Wins all around.

Small Lambdas

I’m not so much a fan of deep functional programming. I’m probably just not smart enough to program large things in Haskell and I’ve been more than lost by a long chain of functions in Scala code. It’s too hard for me to track the execution or poke inside to see what’s happening but It’s almost certainly a case where I just haven’t learned the right skills.

However the small use of comprehensions, lambdas etc enabled in python or swift have felt like a big win to me and seem to be a cleaner way to express intent than a clumsy for loop.

X = [ y*3 for y in list_of_numbers if y > 0 ]

Seems nicer than a for loop of

X = []
For y in list_of_numbers  {
   If y > 0 {
      x.append(y*3)
   } 
}

And doesn’t take too long to get used to.

Similarly

Let bar = foo.map( {$0.name + “ done!”} ) 

Is a nice way to transform all the elements of a collection into something else that makes the transformation more explicit than the boilerplate of iterating through the collection itself. The focus is on the important part.

Reduce() and other friends work similarly but you have to be careful. The key is making the code clearer,  not more clever.  You want to highlight the important part, not bury it in an implicit chain of values flowing through a complex set of functions (yes I am looking at you Scala)

One way to format

I’m really happy about things like PEP8 or the go philosophy on formatting. Not much to say here but honestly so much time has been wasted on garbage like what line you should put the { on.

Things I am less sure about

Now let’s go over a couple of things I am less convinced about. I use these things and they work but they don’t always make me feel good.

Type Inference

I am not sure I could stand coding in some languages without this but if I’m honest when I go back to read the code I really wish more of the types were listed explicitly. I know there is tooling that can make some of this visible but it still frustrates me sometimes.

Property Wrappers

While I am a big fan of concision, I am less a fan of hiding things. Abstractions are necessary when dealing with complexity but we don’t have to keep inventing new methods of hiding what’s really going on.  If I see a function 

Let a = dosomestuff(b, c, d)

I have no idea what’s going to happen inside of dosomestuff, how long it will take or what side effects it might have, but If I know the language well enough I can at least assume something in there is going to be based on b c and d and whether or not those values will be changed by it. 

Sure there might be some crazy dependency injection or global state editing going on that has a lot of side effects to state not in the caller params but that’s just another example of bad patterns

I can also usually quickly find the definition of dosomestuff with a keystroke or two in any modern IDE. 

Let’s contrast this with property wrappers in Swift. These allow me to add functionality to a variable without it being at all apparent this functionality exists when I’m using it. That could be considered cool and concise. By adding a Property Wrapper I can add fancy properties to this var by the way SwiftUI does with @State.  That makes the code cleaner and shorter but it also makes what’s really going on more hidden. If x.w = 3 can do almost anything when w is annotated with a property wrapper, how do I even know to consider looking for this?

Dependency Injection

I’m just going to say this upfront. I hate most dependency injection frameworks because they introduce too much magic into how the code runs and how it is understood. I’ve heard all the arguments about how DI can make things simpler, how it facilitates better testing etc. but every large project I’ve worked on which made significant use of DI frameworks has resulted in seeing developers spend a lot of time trying to understand and or debug these frameworks.

It’s harder to know what’s happening as your code is starting up. There’s not a clear line through the code of where everything is coming from.


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *