The Path to Learning a Programming Language

· 14 min read

“I’ve learnt Ruby in a weekend/ It’s a simple language” boasted one developer who had had no prior experience of the Ruby programming language.

This particular developer had many years of C# programming experience and could probably draw a few parallels between the languages. However, their statement prompted three questions for me.

  1. Can you spend 48 hours and be effective in a programming language?
  2. When can you claim to be proficient in a programming language?
  3. What does it mean to ’know’ a programming language?

Research on learning patterns discussed in this Developer Learning Curve article shows that the learning to code path ends up being a curve with a predictable shape - a steep initial incline, a steady rise, and a plateau along the top.

    |                      xxxxxxxxxxxxx
  E |               xxxxxxx 
  X |           xxxx
  P |        xxx
  E |       x
  R |     xx
  T |    x
  I |   x
  S |  x
  E | x
    |x
    +----------------------------------->
             E X P E R I E N C E

The learning curve, charted between two axes of an unknowable scale, is not a diagonal line and it mirrors my own experience. I have programmed in many languages including Delphi, C, Java, and Ruby, to name a few and the learning curve was always steep and bumpy at the start and tapered off as my experience grew.

So what do you need to do to quickly climb to the top?

There are some essential steps that everybody learning a language needs to take such as studying syntax, semantics and error handling. But is this enough or is there more to learn?

As you gain an understanding of the more basic ideas, you need to start to adapt to a language’s paradigm, its idiomatic usage, and then begin exploring all the available standard libraries. Only then can you start to appreciate the unique advantages and capabilities of a language over others. However, this is still not the end of your journey.

Languages are not created in a vacuum. They are created by people for the people. The language tooling ecosystem and community provide the resources for you to be the most effective in a language but these take time to master.

All together, learning a programming language is a complex process.

Let’s break down each step of this process.

The Beginning of The Ascent

One thing is certain, a new language requires learning the syntax. This is a rudimentary but essential step. Every programming language has a set of rules that describe which combinations of symbols can be used to create a valid program. If you have an invalid syntax, the compiler will scream at you by spewing a stack of offending lines of code and your program will die in agony. So without knowing the language valid statements and constructs, you won’t go very far.

After you’ve learned the syntax, the next concept to wrap your head around is semantics. Do you know what the code you wrote actually means? For example, let’s take a look at a classic first program in Java that many books throw at a newcomer to the language:

public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, World");
  }
}

These few lines pack a powerful punch. I’ve typed the public static void main line thousands of times, without fully understanding what I was doing initially and then I slowly learned about the meaning of each word. You don’t have to understand every line of this program to understand what it does. But with time, you internalise all the language’s magical incantations and start to see the purpose of all the statements.

On a superficial level, if you know the basic data structures like an array or a list, flow control expressions such as an if or case statement, and understand the concept of a function, you can accomplish a lot in many different languages. However, this may prove to be more of a hindrance than a help. For example, how an array is viewed and used really depends on the paradigm the language uses.

At this point, having studied the syntax and semantics, the language starts to feel familiar. But, of course, in the real world, programs have bugs.

When Things Turn Sour

A lot of developer time is spent debugging software and recovering from errors. What is an error anyway? Generally, an error can occur under these conditions:

  • Syntax: a wrong construct used.
  • Type checking: the types don’t match.
  • Evaluation (as Runtime): program runs but produces a wrong result.

The syntax and type checking errors can be verified by the interpreter or compiler. The syntax errors are usually relatively easy to fix. Depending on the language the type system may be complex and resolving type errors may prove hard. However, modern languages such as Elm provide a powerful help system to resolve even the thorniest type errors.

When the compiler is helpful enough to instruct you on how to fix syntax and type errors, the bane of existence for most programmers is runtime errors - when the code does something the programmer didn’t intend it to do. Such unpredictable errors may be caused by network timeouts, out of memory issues but most often they are the result of logical errors, where a programmer’s mental model of a problem fails to match actual code behaviour. These are the worst errors! It’s embarrassing to admit how many times I used the assignment operator when what I needed was a comparison operator. One extra character made all the difference!

Hand in hand with errors goes a programming language’s safety. What does it mean to be a safe language? The Rust programming language provides a succinct definition which means safe from undefined behaviour. Safe language will prevent the most common mistakes and significantly reduce chances of runtime errors during your program’s lifetime.

For example, Rust improves the situation over C programming language by automatically handling memory allocation and deallocation helping inexperienced programmers avoid memory leaks. On the other hand, in Ruby you don’t even have to worry about memory, it takes care of memory allocation for you, but it does it at the cost of less safety. The most infamous error is the NoMethodError which is raised when you try to call a method on nil value.

# => NoMethodError: undefined method `client' for nil:NilClass

These type of errors are common in Ruby programs and can be extremely difficult to fix. The developer’s skill in creating robust software, the strict testing habits and the experience are the only way to prevent and fix such generic errors.

All the fundamental constructs, errors and safety are the direct result of the underlying ideas and concepts that lead to a programming language creation.

What the Language Wants

There are myriad ways to think about a programming language based on the features it supports. However, I will focus on two major and most common ways for classifying modern programming languages.

Based on how information is accessed and transformed, a language can support Object Oriented or Functional paradigm. The other way to look at the language can be its type system, whether it is a Static or Dynamic. Here are a few modern languages that best fit each of the paradigm and type system combinations.

Object Oriented Functional
Static Java, C++ Haskell, ML
Dynamic Ruby, Python Racket, Elixir

It is important to note that languages often don’t belong strictly to just one category. The Ruby language, though classified as Object Oriented due to everything in it being an object, includes many features that enable the Functional style.

Each paradigm creates a unique view of the world and encourages a specific approach to programming. Understanding this approach is essential to fully benefiting from the language design. For example, Java and Ruby are fundamentally different in how they handle interfaces. Ruby doesn’t provide any compile-time safety for its interfaces. However, in Java, if an object implements an interface it has to fulfill the contract by implementing all of its methods.

To make it more concrete, let’s look at Comparable interface in Java. In our example, we make the Car class adhere to Comparable contract which gives the class the ability to create objects that can be compared with one another based on the productionYear attribute.

public class Car implements Comparable<Car> {
  public String name;
  public int productionYear;  

  public Car(String name, Integer productionYear) {
    this.name = name;
    this.productionYear = productionYear;
  }

  @Override
  public int compareTo(final car Car) {
    return Integer.compare(this.productionYear, car.productionYear) 
  }
} 

The Java implementation will force you to implement the compareTo method and ensure that only Car type can be compared to. The Ruby implementation is very similar bar the syntax. A Comparable module is included that will automatically trigger a comparison call <=> when needed:

class Car
  include Comparable

  attr_reader :name, :production_year

  def initialize(name, production_year)
    @name = name
    @production_year = production_year
  end

  def <=>(car)
    self.production_year <=> car.production_year
  end
end

Due to Ruby’s duck type system, the above implementation will allow Car instance to be compared with any object as long as it implements the production_year method. Ruby doesn’t even enforce implementation of <=> method! If absent, it will very happily use one in Object class that is pretty much useless in our scenario as it compares the object’s identity.

But what about the style of programming?

More Than One Way to Code

As you climb up the programming language learning curve, you start to pick up the idioms of the language. The idioms allow you to express the intention behind your code to fellow language programmers in the most intuitive way.

Everyone who has coded for some time in more than one language is probably familiar with a phenomenon where a newcomer to a language writes code that is correct but just doesn’t feel right to people experienced in that language and often reminds you of another language.

In Ruby, if you want to iterate over a collection of 10 numbers you can use a for loop that is common in many other languages.

for num in 0..10 do
  print num
end

You can also use a while loop to perform a similar computation.

num = 0
while num < 10 do
  print num
  num += 1
end

However, a more natural fit would be to use the times method.

10.times do |num|
  print num
end

In general, you will be hard pressed to find Ruby code that uses procedural for loop. It is more common to send each message to a collection and return computation back to a block that will evaluate the expressions included in it.

array.each do |el|
  print el
end

The need to understand how to use the language’s concepts intuitively will often require you to explore the whole spectrum of what a modern language can offer.

The Universe Is Much Bigger Than Expected

Most programming languages are packaged and distributed with a standard library. Standard packages often provide support for networking operations, files processing or building graphical user interfaces. The Java language standard library even provides many alternatives to the core data structures to facilitate, for example, concurrent programming. So to become adept at writing concurrent code in Java you need to familiarise yourself with the java.util.concurrent package and its numerous thread-safe collections.

Knowing the fundamental components that come with the language is an important prerequisite to fully benefiting from the language. Failing to do so, you’re prone to invent solutions to solved problems. Familiarity with the standard packages is often not enough though. You will frequently need to install additional libraries and tools to be effective in a new programming language. Here are a few questions you may need to consider:

  • When you have an error, do you know how to effectively debug it?
  • Does the language come with REPL(Read and Eval Print Loop) and do you know how to use it?
  • Do you know how to set up automatic code formatting, linting, and testing?

The Ruby community created Bundler to automate management of project dependencies. Understanding how Bundler works is the key to knowing how to develop new libraries and work in large projects, for example, web applications written in Ruby on Rails framework. The knowledge of such tools is a must-have.

There are also IDEs(Integrated Development Environments) that can hugely enhance programmers’ productivity with powerful shortcuts, code autocompletion and interactive debugging. One such editor is the Visual Studio Code that integrates easily via extensions with many modern languages.

Learning about the libraries will bring you a lot closer to knowing the language. But everyone comes on this learning path with a different bag of experiences.

Does the Past Matter?

Learning a new language has a lot do with what you are already familiar with. If you spent many years, for example, writing imperative code and suddenly had to switch to a new object-oriented paradigm, it may feel like crossing a chasm.

I remember when travelling in Germany, I met a developer who had spent nearly 20 years programming in Fortran, an imperative language, to suddenly find himself unemployed and in need of a new job. He needed to quickly pick up Java and learn many object-oriented concepts which took a long time to master due to many years of different habits and conceptual models.

It is much easier to move to a language that displays many similarities with what you already know. When I started learning regular expressions in Ruby, I found that they exactly matched my prior experience coding in Perl. Similarly, I could see many parallels between Ruby and Python approach to using dynamic type system.

Your past circumstances will differ and so will the motivation.

What Prompts You to Learn?

Why are you actually learning a new programming language? It can be because your boss asked you to do a job in an unfamiliar language and you have to quickly execute on a project. You’re just a person on a mission to get the job done.

Perhaps you like programming and have a growth mindset. The classic book The Pragmatic Programmer by Andy Hunt and Dave Thomas encourages readers to study a new programming language every year to open themselves up to new ideas and approaches. Following this advice will surely expose you to different views on the art of programming.

There is no escaping the fact that motivation in learning a language plays a big role in how well you’re going to master it. Is this a long-term or short-term investment? If you wish to be using the language for a long time then it benefits to learn it deeply.

But languages are not created equal in the minds of programmers.

Is It Hard or Is It Easy?

Urban legends, mythical tales, and crude jokes give rise to all sorts of perceptions about the programming languages. These perceptions often colour your attitude towards the language and the speed with which you learn it.

If you see syntax, naming conventions or paradigm that match your own experience, you’re more likely to see the language as familiar and what follows easy to learn. At the extreme, a programmer may skip learning a language because of its label as highly difficult to master. Or on the contrary, studying a hard language may help convince others of your intellectual prowess and gain adulation of your peers after they hear that you have finally mastered the impossible!

Over the years listening to the stories from ’experienced’ programmers and reading blog articles, I came to visualise a simplified scale of a programming languages difficulty. The scale starts with JavaScript viewed as ‘easy’ to learn and ends at Haskell whose scary sounding monads and applicative functors contribute to its perception as 'hard’.

EASY  |----------x----------x----------x-----------x-----------| HARD
  JavaScript    PHP        Lisp       Ruby      C++/Java     Haskell

Any programming language has enough dark corners to trip a newcomer and can strike horror in the hearts of many 'experienced’ programmers. For example, the JavaScript’s overall size is small compared to many other languages and the lack of standard library creates a perception of an easy language to learn. However, the language is riddled with strange and unexpected behaviours. It takes time to master the good parts and stay clear of bad ones.

Dealing with perceptions is not easy, they are the byproduct of the programming communities.

Who Are My People?

A programming language has a culture that influences people within its ranks. Almost cult-like social constructs, where all 'believers’ see the language as the true one. We as humans are social animals and like to feel that we belong somewhere, and if the language community meshes with our ideas and makes us feel accepted then it is much easier to sustain motivation and learn.

Though not often thought as an important ingredient of learning a new programming language, the culture has a significant impact on how quickly you will progress and find enjoyment in a new language. Finding mentors is key in order to be effective in any programming language, especially if you don’t want to spend decades rediscovering all the challenges anew.

Access to community and experienced programmers can foster the learning process. User groups, conferences and open source communities play a vital role in onboarding new programmers. Not only can you directly ask questions about the advantages and disadvantage of the language you’re hoping to learn but also what is the best path to take to become proficient.

And this brings me back to the original question.

How Long Do I Need to Learn a Language?

So yes, you can probably learn the syntax of a language in 48 hours. However, I hope that I have convinced you that it takes a lot more than the syntax to learn a programming language. Among many things, you have to get good at idiomatic usage, understand the language paradigm, know built-in libraries and get familiar with the tooling and the language ecosystem. Your past experience has a large say in how fast will you go and so does the community of the like-minded programmers.

You didn’t learn your language of choice overnight. It takes time. But at some point, things just click! You start to feel and think about problems in that language. You learn how to effectively use your IDE and the tools. The language becomes the extension of you that translates ideas into working software.

I would go as far as to say that programming in a language is another view of the world. The author of the language wants you to approach software creation using language features and paradigms. Spending time in understanding a language 'philosophy’ leads to proficiency.

You can learn a language by attending a class, watching a video, or reading a book. If the community resources are ample, it makes the process so much easier. Granted community members and mentors will show you the way, but nothing matches the coding experience you gain working on actual projects that someone needs. This is where you really learn! Alone at your keyboard bashing your head against problems! A mentor can instruct you but they cannot implant the concepts in your head. This can only be done by yourself over many tries and failures.

There will never be a level when you are done learning a language. You may feel you know it but there is always something new to learn. The language itself goes through evolution and requires constant attention — practices change and tools get updated or replaced.

It seems natural to want to say how long something will take. Knowing this we can plan ahead. Of course, nobody can even on a superficial level expect to learn a new language in a short period of 48 hours. Learning a language is very context and environment dependent. This may take six months, one year, or a decade according to Peter Norvig’s Teach Yourself Programming in Ten Years.

Back to our developer and his exclamation. I think it is fair to say that he hasn’t learnt the language over the weekend! All he did is translate his prior experience and understanding on a very superficial level to a new language. He doesn’t yet know how to design software in a new language or anything about its idiomatic usage, standard library, the available tooling and ecosystem. He is bound to have a hard time. As I recall he did struggle with understanding the error messages and debugging his code.

Alexander Pope expressed this article sentiment aptly: “A little learning is a dangerous thing”.

Next time when you hear similar statements of someone learning a new programming language in a weekend, please be kind to that person, and if you want to help, you can always send them this article.


With thanks to Felienne Hermans, Nadia Odunayo and Thorsten Ball for their comments, reviews and contributions.

I'm Piotr Murach. I document my programming journey and share my coding experiences with practical examples to improve your day-to-day work. If you enjoy my articles or open source projects, please consider supporting what I do. Be sure to also subscribe to my newsletter and feed.