You want to learn about programming paradigms, and maybe also design patterns.
Programming paradigms are usually described as traits of programming languages, and sometimes lead to flame wars about which programming language is best. I recommend seeing programming paradigms as obligations you accept, and get some benefits in return. Some languages make those obligations mandatory, but sometimes you can decide to voluntary accept the obligation of some programming paradigm even in a language that doesn’t require it naturally. For example, even if you want to program in Java, I would recommend learning some functional languages just to see what advantages those languages offer and how do they achieve it, so that when you return to writing Java code, you will have an option to write it in a way that provides you some of those advantages.
For example: Even in Java you can decide to write functions that don’t use global variables. I mean functions that only use the values of their explicit input parameters, and only call other functions which follow the same rule. At first sight, this may seem like an pointless obligation, inconvenient in some situations. However, if you carefully write a part of program like this, you will not have to worry about multithreading: because those functions only use their explicit inputs, you can run them in multiple parallel threads, and there will be no bugs. (Which is not a small thing, because multithreading bugs are sometimes really annoying to find.) People who are not familiar with functional languages and frequently use global variables usually produce multithreaded code full of bugs, which in turn makes them afraid of multithreading. By understanding the trade-offs, you will be able to write code that works well in multithreaded applications, and you will not even have to think too much about it. You will just follow the rules.
The big picture in my opinion is this: You have an idea of a program you want to make. Let’s suppose your specification is unambiguous—if you would tell me, a human, what you want to do, I would be able to do exactly that program without giving you further question. Why can’t a sufficiently smart compiler do this? I mean, this would be the ultimate form of programming. It would still be a programmer’s duty to give an unambiguous description, but beyond that, anything you do, is merely compensating for imperfections of today’s programming languages. (Also, if your description is not unambiguous, a good compiler should tell you that.)
Think about it. Not just like “yeah, I also wish programming was easier”, but instead as someone who decided to create such perfect programming language; someone who is ready to do that hard job in the following decades. What problems would you have to solve? Let’s ignore parsing natural language; suppose there is a formal syntax and the programmers have to follow it. What you want to avoid is forcing programmers to write unnecessary code; the code that does not contribute to their goals, but is required to solve some technical problem imposed by your language.
For example, let’s suppose that you have a list of numbers, and you want to get a list of squares of these numbers. From {10, 20, 30} you want {100, 400, 900}. Compare the following solutions in pseudocode:
1)
a list of (X * X) for X in {10, 20, 30}
2)
a new empty list of integers called L
for each X in {10, 20, 30}:
... append X * X to L
3)
for brevity, {10, 20, 30} will be called Z
an integer called N, initially zero
a new list of integers called L, with the same size as Z
while N is smaller than size of Z:
... X is the N-th element of Z
... put X * X into N-th position of L
... increase N by one
The first approach is Python or Perl, the second one is Java, and the third one is C or Basic. All of the pseudocodes were written to accomplish the same goal, but what a difference there is in legibility! (JavaScript would be somewhere between the first and second one; it supports both styles.)
The first one says what needs to be done. The second one must first create an empty list, and then process the values in the existing list, because it lacks brief idioms for converting lists to lists. It’s actually worse than it seems: for example, in the second example, the items in the list must be processed sequentially. Imagine that instead of “X X” you would have a difficult function that takes a whole minute to compute. Luckily, you also have three processors available. The first program, hypothetically, could let each processor compute the function for one item (the calculations for individual items are independent), and then put the results together, so the whole computation would only take one minute. But the second program must process the items sequentially, otherwise the “append” commands could be executed in a wrong order. The third program specifies so much it is difficult to see what exactly was the intention* of the programmer—imagine reading thousands of lines like this.
So, if you want to make a perfect programming language, you should have good idioms for list processing (how about other data structures? which ones? could you devise a fully general approach?), and it would be nice if it could recognize where to use multithreading, so you don’t have to write it specifically. This is already rather difficult and we have only touched the surface.
You want to learn about programming paradigms, and maybe also design patterns.
Programming paradigms are usually described as traits of programming languages, and sometimes lead to flame wars about which programming language is best. I recommend seeing programming paradigms as obligations you accept, and get some benefits in return. Some languages make those obligations mandatory, but sometimes you can decide to voluntary accept the obligation of some programming paradigm even in a language that doesn’t require it naturally. For example, even if you want to program in Java, I would recommend learning some functional languages just to see what advantages those languages offer and how do they achieve it, so that when you return to writing Java code, you will have an option to write it in a way that provides you some of those advantages.
For example: Even in Java you can decide to write functions that don’t use global variables. I mean functions that only use the values of their explicit input parameters, and only call other functions which follow the same rule. At first sight, this may seem like an pointless obligation, inconvenient in some situations. However, if you carefully write a part of program like this, you will not have to worry about multithreading: because those functions only use their explicit inputs, you can run them in multiple parallel threads, and there will be no bugs. (Which is not a small thing, because multithreading bugs are sometimes really annoying to find.) People who are not familiar with functional languages and frequently use global variables usually produce multithreaded code full of bugs, which in turn makes them afraid of multithreading. By understanding the trade-offs, you will be able to write code that works well in multithreaded applications, and you will not even have to think too much about it. You will just follow the rules.
The big picture in my opinion is this: You have an idea of a program you want to make. Let’s suppose your specification is unambiguous—if you would tell me, a human, what you want to do, I would be able to do exactly that program without giving you further question. Why can’t a sufficiently smart compiler do this? I mean, this would be the ultimate form of programming. It would still be a programmer’s duty to give an unambiguous description, but beyond that, anything you do, is merely compensating for imperfections of today’s programming languages. (Also, if your description is not unambiguous, a good compiler should tell you that.)
Think about it. Not just like “yeah, I also wish programming was easier”, but instead as someone who decided to create such perfect programming language; someone who is ready to do that hard job in the following decades. What problems would you have to solve? Let’s ignore parsing natural language; suppose there is a formal syntax and the programmers have to follow it. What you want to avoid is forcing programmers to write unnecessary code; the code that does not contribute to their goals, but is required to solve some technical problem imposed by your language.
For example, let’s suppose that you have a list of numbers, and you want to get a list of squares of these numbers. From {10, 20, 30} you want {100, 400, 900}. Compare the following solutions in pseudocode:
1)
2)
3)
The first approach is Python or Perl, the second one is Java, and the third one is C or Basic. All of the pseudocodes were written to accomplish the same goal, but what a difference there is in legibility! (JavaScript would be somewhere between the first and second one; it supports both styles.)
The first one says what needs to be done. The second one must first create an empty list, and then process the values in the existing list, because it lacks brief idioms for converting lists to lists. It’s actually worse than it seems: for example, in the second example, the items in the list must be processed sequentially. Imagine that instead of “X X” you would have a difficult function that takes a whole minute to compute. Luckily, you also have three processors available. The first program, hypothetically, could let each processor compute the function for one item (the calculations for individual items are independent), and then put the results together, so the whole computation would only take one minute. But the second program must process the items sequentially, otherwise the “append” commands could be executed in a wrong order. The third program specifies so much it is difficult to see what exactly was the intention* of the programmer—imagine reading thousands of lines like this.
So, if you want to make a perfect programming language, you should have good idioms for list processing (how about other data structures? which ones? could you devise a fully general approach?), and it would be nice if it could recognize where to use multithreading, so you don’t have to write it specifically. This is already rather difficult and we have only touched the surface.