Compilers have gotten good at optimizing code. So good, in fact, that we programmers take optimizations as granted. I don't object to optimizations, but I do think we need to re-think the opaqueness of them.
Optimizations are, in general, good things. They are changes to the code to make it faster and more efficient, while keeping the same functionality. Many times, they are changes that seem small.
For example, the code:
a = f(r * 4 + k)
b = g(r * 4 + k)
can be optimized to
t1 = r * 4 + k
a = f(t1)
b = g(t1)
The common expression r * 4 + k can be performed once, not twice, which reduces the time to execute. (It does require space to store the result, so this optimization is really a trade-off between time and space. Also, it assumes that r and k remain unchanged between calls to f() and g().)
Another example:
for i = 1 to 40
a[i] = r * 4 + k
which can be optimized to:
t1 = r * 4 + k
for i = 1 to 40
a[i] = t1
In this example, the operation r * 4 + k is repeated inside the loop, yet it does not change from iteration to iteration. (It is invariant during the loop.) The optimization moves the calculation outside the loop, which means it is calculated only once.
These are two simple examples. Compilers have made these optimizations for years, if not decades. Today's compilers are much better at optimizing code.
I am less concerned with the number of optimizations, and the types of optimizations, and more concerned with the optimizations themselves.
I would like to see the optimizations.
Optimizations are changes, and I would like to see the changes that the compiler makes to the code.
I would like to see how my code is revised to improve performance.
I know of no compiler that reports the optimizations it makes. Not Microsoft's compilers, not Intel's, not open source. None. And I am not satisfied with that. I want to see the optimizations.
Why do I want to see the optimizations?
First, I want to see how to improve my code. The above examples are trivial, yet instructive. (And I have, at times, written the un-optimized versions of those programs, although on a larger scale and with more variables and calculations to worry about.) Seeing the improvements to the code helps me become a better developer.
Second, I want to see what the compiler is doing. It may be making assumptions that are not true, possibly due to my failure to annotate variables and functions properly. I want to correct those failures.
Third, when reviewing code with other developers, I think we should review not only the original code but also the optimized code. The optimizations may give us insight into our code and data.
It is quite possible that future compilers will provide information about their optimizations. Compilers are sophisticated tools, and they do more than simply convert source code into executable bytes. It is time for them to provide more information to us, the programmers.
Wednesday, August 28, 2019
Monday, August 19, 2019
The Museum Principle for programming
Programming languages have, as one of their features, variables. A variable is a thing that holds a value and that value can vary over time. A simple example:
The statement
a = 1
defines a variable named 'a' and assigns a value of 1. Later, the program may contain the statement
a = 2
which changes the value from 1 to 2.
The exact operations vary from language to language. In C and C++, the name is closely associated with the underlying memory for the value. Python and Ruby separate the name from the underlying memory, which means that the name can be re-assigned to point to a different underlying value. In C and C++, the names cannot be changed in that manner. But that distinction has little to do with this discussion. Read on.
Some languages have the notion of constants. A constant is a thing that holds a value and that value cannot change over time. It remains constant. C, C++, and Pascal have this notion. In C, a program can contain the statement
const int a = 1;
A later statement that attempts to change the value of 'a' will cause a compiler error. Python and Ruby have no such notion.
Note that I am referring to constants, not literals such as '1' or '3.14' that appear in the code. These are truly constant and cannot be assigned new values. Some early language implementations did allow such behavior. It was never popular.
The notion of 'constness' is useful. It allows the compiler to optimize the code for certain operations. When applied to a parameter of a function, it informs the programmer that he cannot change the value. In C++, a function of a class can be declared 'const' and then that function cannot modify member variables. (I find this capability helpful to organize code and separate functions that change an object from functions that do not.)
The notion of 'constness' is a specific form of a more general concept, one that we programmers tend to not think about. That concept is 'read but don't write', or 'look but don't touch'. Or as I like to think of it, the "Museum Principle".
The Museum Principle states that you can observe the value of a variable, but you cannot change it. This principle is different from 'constness', which states that the value of a variable cannot (and will not) change. The two are close but not identical. The Museum Principle allows the variable to change; but you (or your code) are not making the change.
It may surprise readers to learn that the Museum Principle has been used already, and for quite a long time.
The idea of "look but don't touch" is implemented in Fortran and Pascal, in loop constructs. In these languages, a loop has an index value. The index value is set to an initial value and later modified for each iteration of the loop. Here are some examples that print the numbers from 1 to 10:
An example in Fortran:
do 100 i = 1, 10
write(*,*) 'i =', i
100 continue
An example in Pascal:
for i:= 1 to 10 do
begin
writeln('i =', i)
end;
In both of these loops, the variable i is initialized to the value 1 and incremented by 1 until it reaches the value 10. The body of each loop prints the value of i.
Now here is where the Museum Principle comes into play: In both Fortran and Pascal, you cannot change the value of i within the loop.
That is, the following code is illegal and will not compile:
In Fortran:
do 100 i = 1, 10
i = 20
write(*,*) 'i =', i
100 continue
In Pascal:
for i:= 1 to 10 do
begin
i := 20
writeln('i =', i)
end;
The highlighted lines are not permitted. It is part of the specification for both Fortran and Pascal that the loop index is not to be assigned. (Early versions of Fortran and Pascal guaranteed this behavior. Later versions of the languages, which allowed aliases via pointers, could not.)
Compare this to a similar loop in C or C++:
for (unsigned int i = 1; i <= 10; i++)
{
printf("%d\n", i);
}
The specifications for the C and C++ languages have no such restriction on loop indexes. (In fact, C and C++ do not have the notion of a loop index; they merely allow a variable to be declared and assigned at the beginning of the loop.)
The following code is legal in C and C++ (and does what you expect):
for (unsigned int i = 1; i <= 10; i++)
{
i = 20;
printf("%d\n", i);
}
My point here is not to say that Fortran and Pascal are superior to C and C++ (or that C and C++ are superior to Fortran and Pascal). My point is to show that the Museum Principle is useful.
Preventing changes to a loop index variable is the Museum Principle. The programmer can see the value of the variable, and the value does change, but the programmer cannot change the value. The programmer is constrained.
Some might chafe at the idea of such a restraint. Many have complained about the restrictions of Pascal and lauded the freedom of C. Yet over time, modern languages have implemented the restraints of Pascal, such as bounds-checking and type conversion.
Modern languages often eliminate loop index variables, by providing "for-each" loops that iterate over a collection. This feature is a stronger form of the "look but don't touch" restriction on loop index variables. One cannot complain about Fortran's limitations of loop index variables, unless one also dislikes the 'for-each' construct. A for-each iterator has a loop index, invisible (and untouchable!) inside.
For the "normal" loop (in which the index variable is not modified), there is no benefit from a prohibition of change to the index variable. (The programmer makes no attempt to change it.) It is the unusual loops, the loops which have extra logic for special cases, that benefit. Changing the loop index value is a shortcut, often serving a purpose that is not clear (and many times not documented). Preventing that short-cut forces the programmer to use code that is more explicit. A hassle in the short term, but better in the long term.
Constraints -- the right type of constraints -- are useful to programmers. The "structured programming" method was all about constraints for control structures (loops and conditionals) and the prohibition of "goto" operations. Programmers at the time complained, but looking back we can see that it was the right thing to do.
Constraints on loop index variables are also the right thing to do. Applying the Museum Principle to loop index variables will improve code and reduce errors.
The statement
a = 1
defines a variable named 'a' and assigns a value of 1. Later, the program may contain the statement
a = 2
which changes the value from 1 to 2.
The exact operations vary from language to language. In C and C++, the name is closely associated with the underlying memory for the value. Python and Ruby separate the name from the underlying memory, which means that the name can be re-assigned to point to a different underlying value. In C and C++, the names cannot be changed in that manner. But that distinction has little to do with this discussion. Read on.
Some languages have the notion of constants. A constant is a thing that holds a value and that value cannot change over time. It remains constant. C, C++, and Pascal have this notion. In C, a program can contain the statement
const int a = 1;
A later statement that attempts to change the value of 'a' will cause a compiler error. Python and Ruby have no such notion.
Note that I am referring to constants, not literals such as '1' or '3.14' that appear in the code. These are truly constant and cannot be assigned new values. Some early language implementations did allow such behavior. It was never popular.
The notion of 'constness' is useful. It allows the compiler to optimize the code for certain operations. When applied to a parameter of a function, it informs the programmer that he cannot change the value. In C++, a function of a class can be declared 'const' and then that function cannot modify member variables. (I find this capability helpful to organize code and separate functions that change an object from functions that do not.)
The notion of 'constness' is a specific form of a more general concept, one that we programmers tend to not think about. That concept is 'read but don't write', or 'look but don't touch'. Or as I like to think of it, the "Museum Principle".
The Museum Principle states that you can observe the value of a variable, but you cannot change it. This principle is different from 'constness', which states that the value of a variable cannot (and will not) change. The two are close but not identical. The Museum Principle allows the variable to change; but you (or your code) are not making the change.
It may surprise readers to learn that the Museum Principle has been used already, and for quite a long time.
The idea of "look but don't touch" is implemented in Fortran and Pascal, in loop constructs. In these languages, a loop has an index value. The index value is set to an initial value and later modified for each iteration of the loop. Here are some examples that print the numbers from 1 to 10:
An example in Fortran:
do 100 i = 1, 10
write(*,*) 'i =', i
100 continue
An example in Pascal:
for i:= 1 to 10 do
begin
writeln('i =', i)
end;
In both of these loops, the variable i is initialized to the value 1 and incremented by 1 until it reaches the value 10. The body of each loop prints the value of i.
Now here is where the Museum Principle comes into play: In both Fortran and Pascal, you cannot change the value of i within the loop.
That is, the following code is illegal and will not compile:
In Fortran:
do 100 i = 1, 10
i = 20
write(*,*) 'i =', i
100 continue
In Pascal:
for i:= 1 to 10 do
begin
i := 20
writeln('i =', i)
end;
The highlighted lines are not permitted. It is part of the specification for both Fortran and Pascal that the loop index is not to be assigned. (Early versions of Fortran and Pascal guaranteed this behavior. Later versions of the languages, which allowed aliases via pointers, could not.)
Compare this to a similar loop in C or C++:
for (unsigned int i = 1; i <= 10; i++)
{
printf("%d\n", i);
}
The specifications for the C and C++ languages have no such restriction on loop indexes. (In fact, C and C++ do not have the notion of a loop index; they merely allow a variable to be declared and assigned at the beginning of the loop.)
The following code is legal in C and C++ (and does what you expect):
for (unsigned int i = 1; i <= 10; i++)
{
i = 20;
printf("%d\n", i);
}
My point here is not to say that Fortran and Pascal are superior to C and C++ (or that C and C++ are superior to Fortran and Pascal). My point is to show that the Museum Principle is useful.
Preventing changes to a loop index variable is the Museum Principle. The programmer can see the value of the variable, and the value does change, but the programmer cannot change the value. The programmer is constrained.
Some might chafe at the idea of such a restraint. Many have complained about the restrictions of Pascal and lauded the freedom of C. Yet over time, modern languages have implemented the restraints of Pascal, such as bounds-checking and type conversion.
Modern languages often eliminate loop index variables, by providing "for-each" loops that iterate over a collection. This feature is a stronger form of the "look but don't touch" restriction on loop index variables. One cannot complain about Fortran's limitations of loop index variables, unless one also dislikes the 'for-each' construct. A for-each iterator has a loop index, invisible (and untouchable!) inside.
For the "normal" loop (in which the index variable is not modified), there is no benefit from a prohibition of change to the index variable. (The programmer makes no attempt to change it.) It is the unusual loops, the loops which have extra logic for special cases, that benefit. Changing the loop index value is a shortcut, often serving a purpose that is not clear (and many times not documented). Preventing that short-cut forces the programmer to use code that is more explicit. A hassle in the short term, but better in the long term.
Constraints -- the right type of constraints -- are useful to programmers. The "structured programming" method was all about constraints for control structures (loops and conditionals) and the prohibition of "goto" operations. Programmers at the time complained, but looking back we can see that it was the right thing to do.
Constraints on loop index variables are also the right thing to do. Applying the Museum Principle to loop index variables will improve code and reduce errors.
Labels:
constraints,
Fortran,
loops,
Museum Principle,
Pascal,
structured code
Wednesday, July 31, 2019
Programming languages, structured or not, immediate or not
I had some spare time on my hands, and any of my friends will tell you that when I have spare time, I think about things. This time, I thought about programming languages.
That's not a surprise. I often think about programming languages. This time I thought about two aspects of programming languages that I call structuredness and immediacy. Immediacy is simply the rapidity in which a program can respond. The languages Perl, Python, and Ruby all have high immediacy, as one can start a REPL (for read-evaluate-print-loop) that takes input and provides the result right away. (In contrast, programs in the languages C#, Java, Go, and Rust must be compiled, so there is an extra step to get a response.
Structuredness, in a language, is how much organization was encouraged by the language. I say "encouraged" because many languages will allow unstructured code. Some languages do require careful thought and organization prior to coding. Functional programming languages require a great deal of thought. Object-oriented languages such as C++, C#, and Java provide some structure. Old-school BASIC did not provide structure at all, with only a GOTO and a simple IF statement to organize your code. (Visual Basic has much more structure than old-school BASIC, and it is closer to C# and Java, although it has a bit more immediacy than those languages.)
My thoughts on structuredness and immediacy led me to think about the combination of the two. Some languages are high in one aspect, and some languages mix the two aspects. Was there an overall pattern?
I built a simple grid with structure on one axis and immediacy on the other. Structure was on the vertical axis: languages with high structure were higher on the chart, languages with less structure were lower. Immediacy was on the horizontal axis, with languages with high immediacy to the right and languages that provided slower response were to the left.
Here's the grid:
structured
^
Go C++ Objective-C Swift |
C# Java VB.NET |
| Python Ruby
(Pascal) | Matlab
| Visual Basic
C | SQL Perl
COBOL Fortran | JavaScript (Forth)
slow <------------------------------------------------> <----------------------------------------------->immediate----------------------------------------------->------------------------------------------------>
(FORTRAN) | R
| (BASIC)
|
|
|
| spreadsheet
v
unstructured
Some notes on the grid:
- Languages in parentheses are older, less-used languages.
- Fortran appears twice: "Fortran" is the modern version and "(FORTRAN)" is the 1960s version
- I have included "spreadsheet" as a programming language
Compiled languages appear on the left (slow) side. This is not related to the performance of programs written in these languages, but the development experience. When programming in a compiled language, one must edit the code, stop and compile, and then run the program. Languages on the right-hand side (the "immediate" side) do not need the compile step and provide feedback faster.
Notice that, aside from the elder FORTRAN, there are no slow, unstructured languages. Also notice that the structured immediate languages (Python, Ruby, et al.) cluster away from the extreme corner of structured and immediate. They are closer to the center.
The result is (roughly) a "main sequence" of programming languages, similar to the main sequence astronomers see in the types of stars. Programming languages tend to a moderate zone, where trade-offs are made between structure and immediacy.
The unusual entry was the spreadsheet, which I consider a programming language for this exercise. It appears in the extreme corner for unstructured and immediate. The spreadsheet, as a programming environment, is the fastest thing we have. Enter a value or a formula in a cell and the change "goes live" immediately. ("Before your finger is off the ENTER key", as a colleague would say.) This is faster than any IDE or compiler or interpreter for any other language.
Spreadsheets are also unstructured. There are no structures in spreadsheets, other than multiple sheets for different sets of data. While it is possible to carefully organize data in a spreadsheet, there is nothing that mandates the organization or even encourages it. (I'm thinking about the formulas in cells. A sophisticated macro programming language is a different thing.)
I think spreadsheets took over a specific type of computing. They became the master of immediate, unstructured programming. BASIC and Forth could not compete with them, and no language since has tried to compete with the spreadsheet. The spreadsheet is the most effective form of this kind of computing, and I see nothing that will replace it.
Therefore, we can predict that spreadsheets will stay with us for some time. It may not be Microsoft Excel, but it will be a spreadsheet.
We can also predict that programming languages will stay within the main sequence of compromise between structure and immediacy.
In other words, BASIC is not going to make a comeback. Nor will Forth, regrettably.
That's not a surprise. I often think about programming languages. This time I thought about two aspects of programming languages that I call structuredness and immediacy. Immediacy is simply the rapidity in which a program can respond. The languages Perl, Python, and Ruby all have high immediacy, as one can start a REPL (for read-evaluate-print-loop) that takes input and provides the result right away. (In contrast, programs in the languages C#, Java, Go, and Rust must be compiled, so there is an extra step to get a response.
Structuredness, in a language, is how much organization was encouraged by the language. I say "encouraged" because many languages will allow unstructured code. Some languages do require careful thought and organization prior to coding. Functional programming languages require a great deal of thought. Object-oriented languages such as C++, C#, and Java provide some structure. Old-school BASIC did not provide structure at all, with only a GOTO and a simple IF statement to organize your code. (Visual Basic has much more structure than old-school BASIC, and it is closer to C# and Java, although it has a bit more immediacy than those languages.)
My thoughts on structuredness and immediacy led me to think about the combination of the two. Some languages are high in one aspect, and some languages mix the two aspects. Was there an overall pattern?
I built a simple grid with structure on one axis and immediacy on the other. Structure was on the vertical axis: languages with high structure were higher on the chart, languages with less structure were lower. Immediacy was on the horizontal axis, with languages with high immediacy to the right and languages that provided slower response were to the left.
Here's the grid:
structured
^
Go C++ Objective-C Swift |
C# Java VB.NET |
| Python Ruby
(Pascal) | Matlab
| Visual Basic
C | SQL Perl
COBOL Fortran | JavaScript (Forth)
slow <------------------------------------------------> <----------------------------------------------->immediate----------------------------------------------->------------------------------------------------>
(FORTRAN) | R
| (BASIC)
|
|
|
| spreadsheet
v
unstructured
Some notes on the grid:
- Languages in parentheses are older, less-used languages.
- Fortran appears twice: "Fortran" is the modern version and "(FORTRAN)" is the 1960s version
- I have included "spreadsheet" as a programming language
Compiled languages appear on the left (slow) side. This is not related to the performance of programs written in these languages, but the development experience. When programming in a compiled language, one must edit the code, stop and compile, and then run the program. Languages on the right-hand side (the "immediate" side) do not need the compile step and provide feedback faster.
Notice that, aside from the elder FORTRAN, there are no slow, unstructured languages. Also notice that the structured immediate languages (Python, Ruby, et al.) cluster away from the extreme corner of structured and immediate. They are closer to the center.
The result is (roughly) a "main sequence" of programming languages, similar to the main sequence astronomers see in the types of stars. Programming languages tend to a moderate zone, where trade-offs are made between structure and immediacy.
The unusual entry was the spreadsheet, which I consider a programming language for this exercise. It appears in the extreme corner for unstructured and immediate. The spreadsheet, as a programming environment, is the fastest thing we have. Enter a value or a formula in a cell and the change "goes live" immediately. ("Before your finger is off the ENTER key", as a colleague would say.) This is faster than any IDE or compiler or interpreter for any other language.
Spreadsheets are also unstructured. There are no structures in spreadsheets, other than multiple sheets for different sets of data. While it is possible to carefully organize data in a spreadsheet, there is nothing that mandates the organization or even encourages it. (I'm thinking about the formulas in cells. A sophisticated macro programming language is a different thing.)
I think spreadsheets took over a specific type of computing. They became the master of immediate, unstructured programming. BASIC and Forth could not compete with them, and no language since has tried to compete with the spreadsheet. The spreadsheet is the most effective form of this kind of computing, and I see nothing that will replace it.
Therefore, we can predict that spreadsheets will stay with us for some time. It may not be Microsoft Excel, but it will be a spreadsheet.
We can also predict that programming languages will stay within the main sequence of compromise between structure and immediacy.
In other words, BASIC is not going to make a comeback. Nor will Forth, regrettably.
Tuesday, July 16, 2019
Across and down
All programming languages have rules. These rules define what can be done and what cannot be done in a valid program. Some languages even have rules for certain things that must be done. (COBOL, for example, requires the four 'DIVISION' sections in each program.)
Beyond rules, there are styles. Styles are different from rules. Rules are firm. Styles are soft. Styles are guidelines: good to follow, but break them when necessary.
Different languages have different styles. Some style guidelines are common: Many languages have guidelines for indentation and the naming of classes, functions, and variables. Some style guidelines are unique to languages.
The Python programming language has a style which limits line length. (To 80 characters, if you are interested.)
Ruby has a style for line length, too. (That is, if you use Rubocop with its default configuration.)
They are not the first languages to care about line length. COBOL and FORTRAN limited line length to 72 characters. These were rules, not guidelines. The origin was in punch cards, and the language standards specified the column layout and specifies 72 as a limit. Compilers ignored anything past column 72, and woe to the programmer who let a line exceed that length.
The limit in Python is a guideline. One is free to write Python with lines that exceed 80 characters, and the Python interpreter will run the code. Similarly, Ruby's style checker, Rubocop, can be configured to warn about any line length. Ruby itself will run the long lines of code. But limits on line length make for code that is more readable.
Programs exist in two dimensions. Not just across, but also down. Code consist of lines of text.
While some languages limit the width of the code (the number of columns), no language limits the "height" of the code -- the number of lines in a program, or a module, or a class.
Some implementations of languages impose a limit on the number of lines. Microsoft BASIC, for example, limited line numbers to four digits, and since each line had to have a unique line number, that imposed an upper bound of 10,000 lines. Some compilers can handle as many lines as will fit in memory -- and no more. But these are limits imposed by the implementation. I am free, for example, to create an interpreter for BASIC that can handle more than 10,000 lines. (Or fewer, stopping at 1,000.) The language does not dictate the limit.
I don't want the harshly-enforced and unconfigurable limits of the days of early computing. But I think we could use with some guidelines for code length. Rubocop, to its credit, does warn about functions that exceed a configurable limit. There are tools for other languages that warn about the complexity of functions and classes. The idea of "the code is too long" has been bubbling in the development community for decades.
Perhaps it is time we gave it some serious thought.
One creative idea (I do not remember who posed it) was to use the IDE (or the editor) to limit program size. The idea was this: Don't allow scrolling in the window that holds the code. Instead of scrolling, as a programmer increased the length of a function, the editor reduced the font size. (The idea was to keep the entire function visible.) As the code grows in size, the text shrinks. Eventually, one reaches a point when the code becomes unreadable.
The idea of shrinking code on the screen is amusing, but the idea of limiting code size may have merit. Could we set style limits for the length of functions and classes? (Such limits and warnings already exist in Rubocop, so the answer is clearly 'yes'.)
The better question is: How do limits on code length (number of lines) help stakeholders? How do they help developers, and how do they help users?
The obvious response is that shorter functions (and shorter classes) are easier to read and comprehend, perform fewer tasks, and are easier to verify (and to correct). At least, that is what I want the answer to be -- I don't know that we have hard observations that confirm that point of view. I can say that my experience confirms this opinion; I have worked on several systems, in different languages, splitting large functions and classes into smaller ones, with the result being that the re-designed code is easier to maintain. Smaller functions are easier to read.
I believe that code should consist of small classes and small functions. Guidelines and tools that help us keep functions short and classes small will improve our code. Remember that code exists in two dimensions (across and down) and that it should be moderate in both.
Beyond rules, there are styles. Styles are different from rules. Rules are firm. Styles are soft. Styles are guidelines: good to follow, but break them when necessary.
Different languages have different styles. Some style guidelines are common: Many languages have guidelines for indentation and the naming of classes, functions, and variables. Some style guidelines are unique to languages.
The Python programming language has a style which limits line length. (To 80 characters, if you are interested.)
Ruby has a style for line length, too. (That is, if you use Rubocop with its default configuration.)
They are not the first languages to care about line length. COBOL and FORTRAN limited line length to 72 characters. These were rules, not guidelines. The origin was in punch cards, and the language standards specified the column layout and specifies 72 as a limit. Compilers ignored anything past column 72, and woe to the programmer who let a line exceed that length.
The limit in Python is a guideline. One is free to write Python with lines that exceed 80 characters, and the Python interpreter will run the code. Similarly, Ruby's style checker, Rubocop, can be configured to warn about any line length. Ruby itself will run the long lines of code. But limits on line length make for code that is more readable.
Programs exist in two dimensions. Not just across, but also down. Code consist of lines of text.
While some languages limit the width of the code (the number of columns), no language limits the "height" of the code -- the number of lines in a program, or a module, or a class.
Some implementations of languages impose a limit on the number of lines. Microsoft BASIC, for example, limited line numbers to four digits, and since each line had to have a unique line number, that imposed an upper bound of 10,000 lines. Some compilers can handle as many lines as will fit in memory -- and no more. But these are limits imposed by the implementation. I am free, for example, to create an interpreter for BASIC that can handle more than 10,000 lines. (Or fewer, stopping at 1,000.) The language does not dictate the limit.
I don't want the harshly-enforced and unconfigurable limits of the days of early computing. But I think we could use with some guidelines for code length. Rubocop, to its credit, does warn about functions that exceed a configurable limit. There are tools for other languages that warn about the complexity of functions and classes. The idea of "the code is too long" has been bubbling in the development community for decades.
Perhaps it is time we gave it some serious thought.
One creative idea (I do not remember who posed it) was to use the IDE (or the editor) to limit program size. The idea was this: Don't allow scrolling in the window that holds the code. Instead of scrolling, as a programmer increased the length of a function, the editor reduced the font size. (The idea was to keep the entire function visible.) As the code grows in size, the text shrinks. Eventually, one reaches a point when the code becomes unreadable.
The idea of shrinking code on the screen is amusing, but the idea of limiting code size may have merit. Could we set style limits for the length of functions and classes? (Such limits and warnings already exist in Rubocop, so the answer is clearly 'yes'.)
The better question is: How do limits on code length (number of lines) help stakeholders? How do they help developers, and how do they help users?
The obvious response is that shorter functions (and shorter classes) are easier to read and comprehend, perform fewer tasks, and are easier to verify (and to correct). At least, that is what I want the answer to be -- I don't know that we have hard observations that confirm that point of view. I can say that my experience confirms this opinion; I have worked on several systems, in different languages, splitting large functions and classes into smaller ones, with the result being that the re-designed code is easier to maintain. Smaller functions are easier to read.
I believe that code should consist of small classes and small functions. Guidelines and tools that help us keep functions short and classes small will improve our code. Remember that code exists in two dimensions (across and down) and that it should be moderate in both.
Monday, July 8, 2019
Lots of (obsolete) Chromebooks
We users of PCs are used to upgrades, for both hardware and software. We comfortably expect this year's PC to be faster than last year's PC, and this year's Windows (or macOS, or Linux) to be better than last year's Windows.
We're also used to obsolescence with hardware and software. Very few people use Windows XP these days, and the number of people using Windows 3.1 (or MS-DOS) is vanishingly small. The modern PC uses an Intel or AMD 64-bit processor.
Hardware and software both follow a pattern of introduction, acceptance, popularity, and eventual replacement. It should not surprise us that Chromebooks follow the same pattern. Google specifies hardware platforms and manufacturers build those platforms and install Chrome OS. After some time, Google drops support for a platform. (That period of time is a little over six years.)
For obsolete PCs (those not supported by Windows) and MacBooks (those not supported by macOS) the usual "upgrade" is to install Linux. There are several Linux distros that are suitable for older hardware. (I myself am running Ubuntu 16.04 on an old 32-bit Intel-based MacBook.)
Back to Chromebooks. What will happen with all of those Chromebooks that are marked as "obsolete" by Google?
There are a few paths forward.
The first (and least effort) path is to simply continue using the Chromebook and its version of Chrome. Chrome OS should continue to run, and Chrome should continue to run. The Chromebook won't receive updates, so Chrome will be "frozen in time" and gradually become older, compared to other browsers. There may come a time when its certificates expire, and it will be unable to initiate secure sessions with servers. At that point, Chrome (and the Chromebook) will have very few uses.
Another obvious path is to replace it. Chromebooks are typically less expensive than PCs, and one could easily buy a new Chromebook. (And since the Chromebook model of computing is to store everything on the server and nothing on the Chromebook, there is no data to migrate from the old Chromebook to the new one.)
Yet there is another option between "continue as is" and "replace".
One could replace the operating system (and the browser). The Chromebook is a PC, effectively, and there are ways to replace its operating system. Microsoft has instructions for installing Windows 10 on a Chromebook, and there are many sites that explain how to install Linux on a Chromebook.
Old Chromebooks will be fertile ground for tinkerers and hobbyists. Tinkerers and hobbyists are willing to open laptops (Chromebooks included), adjust hardware, and install operating systems. When Google drops support for a specific model of Chromebook, there is little to lose in replacing Chrome OS with something like Linux. (Windows 10 on a Chromebook is tempting, but many Chromebooks have minimal hardware, and Linux may be the better fit.)
I expect to see lots of Chromebooks on the used market, in stores and online, and lots of people experimenting with them. They are low-cost PCs suitable for small applications. The initial uses will be as web browsers or remote terminals to server-based applications (because that what we use Chromebooks for now). But tinkerers and hobbyists are clever and imaginative, and we may see new uses, such as low-end games or portable word processors.
Perhaps a new operating system will emerge, one that is specialized for low-end hardware. There are already Linux distros which support low-end PCs (Puppy Linux, for one) and we may see more interest in those.
Those Chromebooks that are converted to Linux will probably end up running a browser. It may be Firefox, or, in an ironic twist, they may run Chromium -- or even Chrome! The machine that Google says is "not good enough" may be just good enough to run Google's browser.
We're also used to obsolescence with hardware and software. Very few people use Windows XP these days, and the number of people using Windows 3.1 (or MS-DOS) is vanishingly small. The modern PC uses an Intel or AMD 64-bit processor.
Hardware and software both follow a pattern of introduction, acceptance, popularity, and eventual replacement. It should not surprise us that Chromebooks follow the same pattern. Google specifies hardware platforms and manufacturers build those platforms and install Chrome OS. After some time, Google drops support for a platform. (That period of time is a little over six years.)
For obsolete PCs (those not supported by Windows) and MacBooks (those not supported by macOS) the usual "upgrade" is to install Linux. There are several Linux distros that are suitable for older hardware. (I myself am running Ubuntu 16.04 on an old 32-bit Intel-based MacBook.)
Back to Chromebooks. What will happen with all of those Chromebooks that are marked as "obsolete" by Google?
There are a few paths forward.
The first (and least effort) path is to simply continue using the Chromebook and its version of Chrome. Chrome OS should continue to run, and Chrome should continue to run. The Chromebook won't receive updates, so Chrome will be "frozen in time" and gradually become older, compared to other browsers. There may come a time when its certificates expire, and it will be unable to initiate secure sessions with servers. At that point, Chrome (and the Chromebook) will have very few uses.
Another obvious path is to replace it. Chromebooks are typically less expensive than PCs, and one could easily buy a new Chromebook. (And since the Chromebook model of computing is to store everything on the server and nothing on the Chromebook, there is no data to migrate from the old Chromebook to the new one.)
Yet there is another option between "continue as is" and "replace".
One could replace the operating system (and the browser). The Chromebook is a PC, effectively, and there are ways to replace its operating system. Microsoft has instructions for installing Windows 10 on a Chromebook, and there are many sites that explain how to install Linux on a Chromebook.
Old Chromebooks will be fertile ground for tinkerers and hobbyists. Tinkerers and hobbyists are willing to open laptops (Chromebooks included), adjust hardware, and install operating systems. When Google drops support for a specific model of Chromebook, there is little to lose in replacing Chrome OS with something like Linux. (Windows 10 on a Chromebook is tempting, but many Chromebooks have minimal hardware, and Linux may be the better fit.)
I expect to see lots of Chromebooks on the used market, in stores and online, and lots of people experimenting with them. They are low-cost PCs suitable for small applications. The initial uses will be as web browsers or remote terminals to server-based applications (because that what we use Chromebooks for now). But tinkerers and hobbyists are clever and imaginative, and we may see new uses, such as low-end games or portable word processors.
Perhaps a new operating system will emerge, one that is specialized for low-end hardware. There are already Linux distros which support low-end PCs (Puppy Linux, for one) and we may see more interest in those.
Those Chromebooks that are converted to Linux will probably end up running a browser. It may be Firefox, or, in an ironic twist, they may run Chromium -- or even Chrome! The machine that Google says is "not good enough" may be just good enough to run Google's browser.
Friday, June 21, 2019
The complexity of programming languages
A recent project saw me examining and tokenizing code for different programming languages. The languages ranged from old languages (COBOL and FORTRAN, among others) to modern languages (Python and Go, among others). It was an interesting project, and I learned quite a bit about many different languages. (By 'tokenize', I mean to identify the type of each item in a program: Variables, identifiers, function names, operators, etc. I was not parsing the code, or building an abstract syntax tree, or compiling the code into op-codes. Tokenizing is the first step of compiling, but a far cry from actually compiling the code.)
One surprising result: newer languages are easier to tokenize than older languages. Python is easier to tokenize than COBOL, and Go is easier to tokenize than FORTRAN.
This is counterintuitive. One would think that older languages would be primitive (and therefore easy to tokenize) and modern languages sophisticated (and therefore difficult to tokenize). Yet my experience shows the opposite.
Why would this be? I can think of two -- no, three -- reasons.
First, the old languages (COBOL, FORTRAN, and PL/I) were designed in the age of punch cards, and punch cards impose limits on source code. COBOL, FORTRAN, and PL/I have few things in common, but one thing that they do have in common is line layout and the 'identification' field in columns 72 through 80.
When your program is stored on punch cards, a risk is that someone will drop the deck of cards and the cards will become out of order. Such a thing cannot happen with programs stored in disk files, but with punch cards such an event is a real risk. To recover from that event, the right-most columns were reserved for identification: a code, unique to each line, that would let a card sorter machine (there were such things) put the cards back into their proper order.
The need for an identification column is tied to the punch card medium, yet it became part of each language standard. COBOL, FORTRAN, and PL/I standards all refer to the columns 72 through 80 as reserved for identification, and they could not be used for "real" source code. Programs transferred from punch cards to disk files (when disks became available to programmers) kept the rule for the identification field -- probably to make conversion easy. Later versions of languages did drop the rule, but the damage had been done. The identification field was part of the language specification.
As part of the language specification, I had to tokenize the identification numbers. Mostly they were not a problem -- just another "thing" to tokenize -- but sometimes they occurred in the middle of a string literal or a comment, which are awkward situations.
Anyway, the tokenization of old languages has its challenges.
New languages don't suffer from such problems. Their source code was never stored on punch cards, and they never had identification fields. (Either within string literals or not.)
But the tokenization of modern languages is easier. Each language has a set of token types, but older languages have a larger set, and a more varied set. Most languages have identifiers, numeric literals, and operators; COBOL also has picture values and level indicators, and PL/I has attributes and conditions (among other token types).
Which brings me to the second reason for modern languages to have simpler tokenizing requirements: The languages are designed to be easy to tokenize.
It seems to me that, intentionally or not, the designers of modern languages have made design choices that reduce the work for tokenizers. They have built languages that are easy to tokenize, and therefore have simple logic for tokenizers. (All compilers and interpreters have tokenizers; it is a step in converting the source to executable bytes.)
So maybe the simplicity of language tokenization is the result of the "laziness" of language designers.
But I have a third reason, one that I believe is the true reason for the simplicity of modern language tokenizers.
Modern languages are easy to tokenize because they are easy to read (by humans).
A language that is easy to read (for a human) is also easy to tokenize. Language designers have been consciously designing languages to be easy to read. (Python is the leading example, but all designers claim their language is "easy to read".)
Languages that are easy to read are easy to tokenize. It's that simple. We've been designing languages are humans, and as a side effect we have made them easy for computers.
I, for one, welcome the change. Not only does it make my job easier (tokenizing all of those languages) but it makes every developer's job easier (reading code from other developers and writing new code).
So I say three cheers for simple* programming languages!
* Simple does not imply weak. A simple programming language may be easy to understand, yet it may also be powerful. The combination of the two is the real benefit here.
One surprising result: newer languages are easier to tokenize than older languages. Python is easier to tokenize than COBOL, and Go is easier to tokenize than FORTRAN.
This is counterintuitive. One would think that older languages would be primitive (and therefore easy to tokenize) and modern languages sophisticated (and therefore difficult to tokenize). Yet my experience shows the opposite.
Why would this be? I can think of two -- no, three -- reasons.
First, the old languages (COBOL, FORTRAN, and PL/I) were designed in the age of punch cards, and punch cards impose limits on source code. COBOL, FORTRAN, and PL/I have few things in common, but one thing that they do have in common is line layout and the 'identification' field in columns 72 through 80.
When your program is stored on punch cards, a risk is that someone will drop the deck of cards and the cards will become out of order. Such a thing cannot happen with programs stored in disk files, but with punch cards such an event is a real risk. To recover from that event, the right-most columns were reserved for identification: a code, unique to each line, that would let a card sorter machine (there were such things) put the cards back into their proper order.
The need for an identification column is tied to the punch card medium, yet it became part of each language standard. COBOL, FORTRAN, and PL/I standards all refer to the columns 72 through 80 as reserved for identification, and they could not be used for "real" source code. Programs transferred from punch cards to disk files (when disks became available to programmers) kept the rule for the identification field -- probably to make conversion easy. Later versions of languages did drop the rule, but the damage had been done. The identification field was part of the language specification.
As part of the language specification, I had to tokenize the identification numbers. Mostly they were not a problem -- just another "thing" to tokenize -- but sometimes they occurred in the middle of a string literal or a comment, which are awkward situations.
Anyway, the tokenization of old languages has its challenges.
New languages don't suffer from such problems. Their source code was never stored on punch cards, and they never had identification fields. (Either within string literals or not.)
But the tokenization of modern languages is easier. Each language has a set of token types, but older languages have a larger set, and a more varied set. Most languages have identifiers, numeric literals, and operators; COBOL also has picture values and level indicators, and PL/I has attributes and conditions (among other token types).
Which brings me to the second reason for modern languages to have simpler tokenizing requirements: The languages are designed to be easy to tokenize.
It seems to me that, intentionally or not, the designers of modern languages have made design choices that reduce the work for tokenizers. They have built languages that are easy to tokenize, and therefore have simple logic for tokenizers. (All compilers and interpreters have tokenizers; it is a step in converting the source to executable bytes.)
So maybe the simplicity of language tokenization is the result of the "laziness" of language designers.
But I have a third reason, one that I believe is the true reason for the simplicity of modern language tokenizers.
Modern languages are easy to tokenize because they are easy to read (by humans).
A language that is easy to read (for a human) is also easy to tokenize. Language designers have been consciously designing languages to be easy to read. (Python is the leading example, but all designers claim their language is "easy to read".)
Languages that are easy to read are easy to tokenize. It's that simple. We've been designing languages are humans, and as a side effect we have made them easy for computers.
I, for one, welcome the change. Not only does it make my job easier (tokenizing all of those languages) but it makes every developer's job easier (reading code from other developers and writing new code).
So I say three cheers for simple* programming languages!
* Simple does not imply weak. A simple programming language may be easy to understand, yet it may also be powerful. The combination of the two is the real benefit here.
Friday, May 17, 2019
Procedures and functions are two different things
The programming language Pascal had many good ideas. Many of those ideas have been adopted by modern programming languages. One idea that hasn't been adopted was the separation of functions and procedures.
Some definitions are in order. In Pascal, a function is a subroutine that accepts input parameters, can access variables in its scope (and containing scopes), performs some computations, and returns a value. A procedure is similar: it accepts input parameters, can access variables in its scope and containing scopes, performs calculations, and ... does not return a value.
Pascal has the notion of functions and a separate notion of procedures. A function is a function and a procedure is a procedure, and the two are different. A function can be used (in early Pascal, must be used) in an expression. It cannot stand alone.
A procedure, in contrast, is a computational step in a program. It cannot be part of an expression. It is a single statement, although it can be part of an 'if' or 'while' statement block.
Functions and procedures have different purposes, and I believe that the creators of Pascal envisioned functions to be unable to change variables outside of themselves. Procedures, I believe, were intended to change variables outside of their immediate scope. In C++, a Pascal-style function would be a function that is declared 'const', and a procedure would be a function that returns 'void'.
This arrangement is different from the C idea of functions. C combines the idea of function and procedure into a single 'function' construct. A function may be designed to return a value, or it may be designed to return nothing. A function may change variables outside of its scope, but it doesn't have to. (It may or may not have "side effects".)
In the competition among programming languages, C won big early on, and Pascal (or rather, the ideas in Pascal), have gained acceptance slowly. The C notion of function has been carried by other popular languages: C++, Java, C#, Python, Ruby, and even Go.
I remember quite clearly learning about Pascal (many years ago) and feeling that C was superior to Pascal due to its single approach. I sneered (mentally) at Pascal's split between functions and procedures.
I have come to regret those feelings, and now see the benefit of separating functions and procedures. When building (or maintaining) large-ish systems in modern languages (C++, C#, Java, Python), I have created functions that follow the function/procedure split. These languages force one to write functions -- there is no construct for a procedure -- yet I designed some functions to return values and others to not return values. The value-returning functions I made 'const' when possible, and avoided side effects. The functions with side effects I designed to not return values. In sum, I built functions and procedures, although the compiler uses only the 'function' construct.
The future may hold programming languages that provide functions and procedures as separate constructs. I'm confident that we will see languages that have these two ideas. Here's why:
First, there is a new class of programming languages called "functional languages". These include ML, Erlang, Haskell, and F#, to name a few. These functional languages use Pascal's original idea of functions as code blocks that perform a calculation with no side effects and return a value. Language designers have already re-discovered the idea of the "pure function".
Second, most ideas from Pascal have been implemented in modern languages. Bounds-checking for arrays. Structured programming. Limited conversion of values from one type to another. The separation of functions and procedures is one more of these ideas.
The distinction between functions and procedures is one more concept that Pascal got right. I expect to see it in newer languages, perhaps over the next decade. The enthusiasts of functional programming will realize that pure functions are not sufficient and that they need procedures. We'll then see variants of functional languages that include procedures, with purists holding on to procedure-less languages. I'm looking forward to the division of labor between functions and procedures; it has worked well for me in my efforts and a formal recognition will help me convey this division to other programmers.
Some definitions are in order. In Pascal, a function is a subroutine that accepts input parameters, can access variables in its scope (and containing scopes), performs some computations, and returns a value. A procedure is similar: it accepts input parameters, can access variables in its scope and containing scopes, performs calculations, and ... does not return a value.
Pascal has the notion of functions and a separate notion of procedures. A function is a function and a procedure is a procedure, and the two are different. A function can be used (in early Pascal, must be used) in an expression. It cannot stand alone.
A procedure, in contrast, is a computational step in a program. It cannot be part of an expression. It is a single statement, although it can be part of an 'if' or 'while' statement block.
Functions and procedures have different purposes, and I believe that the creators of Pascal envisioned functions to be unable to change variables outside of themselves. Procedures, I believe, were intended to change variables outside of their immediate scope. In C++, a Pascal-style function would be a function that is declared 'const', and a procedure would be a function that returns 'void'.
This arrangement is different from the C idea of functions. C combines the idea of function and procedure into a single 'function' construct. A function may be designed to return a value, or it may be designed to return nothing. A function may change variables outside of its scope, but it doesn't have to. (It may or may not have "side effects".)
In the competition among programming languages, C won big early on, and Pascal (or rather, the ideas in Pascal), have gained acceptance slowly. The C notion of function has been carried by other popular languages: C++, Java, C#, Python, Ruby, and even Go.
I remember quite clearly learning about Pascal (many years ago) and feeling that C was superior to Pascal due to its single approach. I sneered (mentally) at Pascal's split between functions and procedures.
I have come to regret those feelings, and now see the benefit of separating functions and procedures. When building (or maintaining) large-ish systems in modern languages (C++, C#, Java, Python), I have created functions that follow the function/procedure split. These languages force one to write functions -- there is no construct for a procedure -- yet I designed some functions to return values and others to not return values. The value-returning functions I made 'const' when possible, and avoided side effects. The functions with side effects I designed to not return values. In sum, I built functions and procedures, although the compiler uses only the 'function' construct.
The future may hold programming languages that provide functions and procedures as separate constructs. I'm confident that we will see languages that have these two ideas. Here's why:
First, there is a new class of programming languages called "functional languages". These include ML, Erlang, Haskell, and F#, to name a few. These functional languages use Pascal's original idea of functions as code blocks that perform a calculation with no side effects and return a value. Language designers have already re-discovered the idea of the "pure function".
Second, most ideas from Pascal have been implemented in modern languages. Bounds-checking for arrays. Structured programming. Limited conversion of values from one type to another. The separation of functions and procedures is one more of these ideas.
The distinction between functions and procedures is one more concept that Pascal got right. I expect to see it in newer languages, perhaps over the next decade. The enthusiasts of functional programming will realize that pure functions are not sufficient and that they need procedures. We'll then see variants of functional languages that include procedures, with purists holding on to procedure-less languages. I'm looking forward to the division of labor between functions and procedures; it has worked well for me in my efforts and a formal recognition will help me convey this division to other programmers.
Labels:
C,
functional programming,
new programming languages,
Pascal
Subscribe to:
Posts (Atom)