Idiomatic or idiosyncratic? by Mark Seemann
A strange programming construct may just be a friendly construct you haven't yet met.
Take a look at this fragment of JavaScript, and reflect on how you feel about it.
var nerdCapsToKebabCase = function (text) { var result = ""; for (var i = 0; i < text.length; i++) { var c = text.charAt(i); if (i > 0 && c == c.toUpperCase()) { result = result + "-"; } result = result + c.toLowerCase(); } return result; };
Do you understand what it does? Does it feel appropriate for the language? Does it communicate its purpose?
Most likely, you answered yes to all three questions - at least if you know what NerdCaps and kebab-case means.
Would your father, or your 12-year old daughter, or your English teacher, also answer yes to all of these questions? Probably not, assuming they don't know programming.
Everything is easy once you know how to do it; everything is hard if you don't.
Beauty is in the eye of the beholder #
Writing software is hard. Writing maintainable software - code that you and your team can keep working on year after year - is extraordinarily hard. The most prominent sources of problems seem to be accidental complexity and coupling.
During my career, I've advised thousands of people on how to write maintainable code: how to reduce coupling, how to simplify, how to express code that a human can easily reason about.
The most common reaction to my suggestions?
"But, that's even more unreadable than the code we already had!"What that really means is usually: "I don't understand this, and I feel very uncomfortable about that."
That's perfectly natural, but it doesn't mean that my suggestions are really unreadable. It just means that you may have to work a little before it becomes readable, but once that happens, you will understand why it's not only readable, but also better.
Let's take a step back and examine what it means when source code is readable.
Imperative code for beginners #
Imagine that you're a beginner programmer, and that you have to implement the nerd caps to kebab case converter from the introduction.
This converter should take as input a string in NerdCaps, and convert it to kebab-case. Here's some sample data:
Input | Expected output |
---|---|
Foo | foo |
Bar | bar |
FooBar | foo-bar |
barBaz | bar-baz |
barBazQuux | bar-baz-quux |
garplyGorgeFoo | garply-gorge-foo |
Since (in this imaginary example) you're a beginner, you should choose to use a beginner's language, so perhaps you select Visual Basic .NET. That seems appropriate, as BASIC was originally an acronym for Beginner's All-purpose Symbolic Instruction Code.
Perhaps you write the function like this:
Function Convert(text As String) As String Dim result = "" For index = 0 To text.Length - 1 Dim c = text(index) If index > 0 And Char.IsUpper(c) Then result = result + "-" End If result = result + c.ToString().ToLower() Next Return result End Function
Is this appropriate Visual Basic code? Yes, it is.
Is it readable? That completely depends on who's doing the reading. You may find it readable, but does your father, or your 12-year old daughter?
I asked my 12-year old daughter if she understood, and she didn't. However, she understands this:
Funktionen omformer en stump tekst ved at først at dele den op i stumper hver gang der optræder et stort bogstav. Derefter sætter den bindestreg mellem hver tekststump, og laver hele teksten om til små bogstaver - altså nu med bindestreger mellem hver tekststump.
Did you understand that? If you understand Danish (or another Scandinavian language), you probably did; otherwise, you probably didn't.
Readability is evaluated based on what you already know. However, what you know isn't constant.
Imperative code for seasoned programmers #
Once you've gained some experience, you may find Visual Basic's syntax too verbose. For instance, once you understand the idea about scope, you may find End If, End Function etc. to be noisy rather than helpful.
Perhaps you're ready to replace those keywords with curly braces. Here's the same function in C#:
public string Convert(string text) { var result = ""; for (int i = 0; i < text.Length; i++) { var c = text[i]; if (i > 0 && Char.IsUpper(c)) result = result + "-"; result = result + c.ToString().ToLower(); } return result; }
Is this proper C# code? Yes. Is it readable? Again, it really depends on what you already know, but most programmers familiar with the C language family would probably find it fairly approachable. Notice how close it is to the original JavaScript example.
Imperative code for experts #
Once you've gained lots of experience, you will have learned that although the curly braces formally delimit scopes, in order to make the code readable, you have to make sure to indent each scope appropriately. That seems to violate the DRY principle. Why not let the indentation make the code readable, as well as indicate the scope?
Various languages have so-called significant whitespace. The most widely known may be Python, but since we've already looked at two .NET languages, why not look at a third?
Here's the above function in F#:
let nerdCapsToKebabCase (text : string) = let mutable result = "" for i = 0 to text.Length - 1 do let c = text.Chars i if (i > 0 && Char.IsUpper c) then result <- result + "-" result <- result + c.ToString().ToLower() result
Is this appropriate F# code?
Not really. While it compiles and works, it goes against the grain of the language. F# is a Functional First language, but the above implementation is as imperative as all the previous examples.
Functional refactoring #
A more Functional approach in F# could be this:
let nerdCapsToKebabCase (text : string) = let addSkewer index c = let s = c.ToString().ToLower() match index, Char.IsUpper c with | 0, _ -> s | _, true -> "-" + s | _, false -> s text |> Seq.mapi addSkewer |> String.concat ""
This implementation uses a private, inner function called addSkewer
to convert each character to a string, based on the value of the character, as well as the position it appears. This conversion is applied to each character, and the resulting sequence of strings is finally concatenated and returned.
Is this good F#? Yes, I would say that it is. There are lots of different ways you could have approached this problem. This is only one of them, but I find it quite passable.
Is it readable? Again, if you don't know F#, you probably don't think that it is. However, the F# programmers I asked found it readable.
This solution has the advantage that it doesn't rely on mutation. Since the conversion problem in this article is a bit of a toy problem, the use of imperative code with heavy use of mutation probably isn't a problem, but as the size of your procedures grow, mutation makes it harder to understand what happens in the code.
Another improvement over the imperative version is that this implementation uses a higher level of abstraction. Instead of stating how to arrive at the desired result, it states what to do. This means that it becomes easier to change the composition of it in order to change other characteristics. As an example, this implementation is what is known as embarrassingly parallel, although it probably wouldn't pay to parallelise this implementation (it depends on how big you expect the input to be).
Back-porting the functional approach #
While F# is a multi-paradigmatic language with an emphasis on Functional Programming, C# and Visual Basic are multi-paradigmatic languages with emphasis on Object-Oriented Programming. However, they still have some Functional capabilities, so you can back-port the F# approach. Here's one way to do it in C# using LINQ:
public string Convert(string text) { return String.Concat(text.Select(AddSkewer)); } private static string AddSkewer(char c, int index) { var s = c.ToString().ToLower(); if (index == 0) return s; if (Char.IsUpper(c)) return "-" + s; return s; }
The LINQ Select method is equivalent to F#'s mapi function, and the AddSkewer function must rely on individual conditionals instead of pattern matching, but apart from that, it's structurally the same implementation.
You can do the same in Visual Basic:
Function Convert(text As String) As String Return String.Concat(text.Select(AddressOf AddSkewer)) End Function Private Function AddSkewer(c As Char, index As Integer) Dim s = c.ToString().ToLower() If index = 0 Then Return s End If If Char.IsUpper(c) Then Return "-" + s End If Return s End Function
Are these back-ported implementations examples of appropriate C# or Visual Basic code? This is where the issue becomes more controversial.
Idiomatic or idiosyncratic #
Apart from reactions such as "that's unreadable," one of the most common reactions I get to such suggestions is:
"That's not idiomatic C#" (or Visual Basic).
Perhaps, but could it become idiomatic?
Think about what an idiom is. In language, it just means a figure of speech, like "jumping the shark". Once upon a time, no-one said "jump the shark". Then Jon Hein came up with it, other people adopted it, and it became an idiom.
It's a bit like that with idiomatic C#, idiomatic Visual Basic, idiomatic Ruby, idiomatic JavaScript, etc. Idioms are adopted because they're deemed beneficial. It doesn't mean that the set of idioms for any language is finite or complete.
That something isn't idiomatic C# may only mean that it isn't idiomatic yet.
Or perhaps it is idiomatic, but it's just an idiom you haven't seen yet. We all have idiosyncrasies, but we should try to see past them. If a language construct is strange, it may be a friendly construct you just haven't met.
Constant learning #
One of my friends once told me that he'd been giving a week-long programming course to a group of professional software developers, and as the week was winding down, one of the participants asked my friend how he knew so much.
My friend answered that he constantly studied and practised programming, mostly in his spare time.
The course participant incredulously replied: "You mean that there's more to learn?"
As my friend told me, he really wanted to reply: “If that's a problem for you, then you've picked the wrong profession." However, he found something more bland, but polite, to answer.
There's no way around it: if you want to be (and stay) a programmer, you'll have to keep learning - not only new languages and technologies, but also new ways to use the subjects you already think you know.
If you ask me about advice about a particular problem, I will often suggest something that seems foreign to you. That doesn't make it bad, or unreadable. Give it a chance even if it makes you uncomfortable right now. Being uncomfortable often means that you're learning.
Luckily, in these days of internet, finding materials that will help you learn is easier and more accessible than ever before in history. There are tutorials, blog posts, books, and video services like Pluralsight.
Comments
I would have done this...
Craig, fair enough :)
In addition to Craig's proposal (which I immediately though of too hehe), think of this one:
It probably does not follow the whole conventions of the language, but the great thing about it is that it can be easily understood without knowledge of the language. This brings it to another complete level of maintainability.
Of course, the meta-information that you convey through the code is subject to another complete analysis and discussion, since there are way too many options on how to approach that, and while most of us will lean on "readable code", it all comes down to what the code itself is telling you about itself.
Finally, as a side note, something that I found quite interesting is that most of the introductory guides to Ruby as a language will talk about how Ruby was design to read like english.
(based on Why's Poignant Guide to Ruby.)
While I found that pretty appealing when learning Ruby, some other constructs will take it far away from how English works, pretty much what happened to your example with F#. Not that they cannot be made to read easier, but that extra effort is not always performed.
My conclusion is: readability is not about the code or the algorithm (which definitely can help), but about the information that the code itself gives you about what it does. Prefer to use pre-built language functions rather than implementing them yourself.
In under than 30 seconds of seeing the code blocks, I can't follow any of the process. But I know what the operation is doing thanks to the operation name, nerdCapsToKebabCase. Surely that the terms nerdCaps and KebabCase is uncommon, but I only need some minutes to know what are those using internet.
So for me, no matter how good you write your code, you can't make it commonly readable by using the code itself. Won't the readability can be enhanced by using comments / documentation? Ex: /* convert thisKindOfPhrase to this-kind-of-phrase */. I've also used unit tests to improve the readability to some extent too.
To end the loop it may be fair to give a JavaScript function sample which became more idiomatic since 2015 :)
ES5 syntax to be consistent with the first example.
I know you can use String.prototype.replace with a regex in this case but it don't illustrate well the idiom from Imperative to functional (even it's a High-Order Function).