Error Comparison (Python 3 vs. Scala 3)
In my last post, I looked at some of the ways in which dynamically typed languages can cause students to struggle with a number of examples in Python. I wanted to follow up on that by explicitly showing comparisons with what happens in those examples in a statically typed language. To do this, I wrote Scala versions of each of the scripts using Scala 3. I tried to make fairly minimal changes to keep things similar. This included using the “quiet” syntax in Scala 3 that doesn’t include curly braces. I also updated a few aspects of the Python code from the last post.
File Example
The first example is one that reads three values from a file and uses them later in some other functions. There is an error on line 4 in the Python code where we forgot to convert one of the lines to be an int
. In Python, this manifests as a logic error where that line is printed multiple times. This happens because *
is defined to duplicate a string when used with an int
. Note that some people don’t like the fact that you can do “multiplication” between a string and an integer. I find it handy, so that doesn’t bother me so much. The problem is that when that isn’t the desired functionality this code produces a logic error that is seen in a print in a very different part of the code from where the actual error occurs.
So what happens in the Scala version? When we try to run this as a script we get the following.
-- [E007] Type Mismatch Error: /home/mlewis/Test/sfileTest.scala:8:7 -----------
8 | (v1, v2, v3)
| ^^
| Found: (v2 : String)
| Required: Intlonger explanation available when compiling with `-explain`
1 error found
Error: Errors encountered during compilation
This states fairly directly that on line 8 the variable v2
was found to be a String
and it needed to be an Int
. If I use VS Code as my editor with the Metals plugin, I get a red squiggly underline below v2
on line 8, and hovering over it shows the part of the error saying what was found and what was expected. As a general rule, tools for statically typed languages can provide significantly more help than those for dynamically typed languages because the types provide additional information for them to use.
Another thing that I want to call the reader's attention to here is the fact that the Scala code really is remarkably similar to the Python code in many ways, including length. Because of experience with languages like Java, C++, and C#, many people who say they prefer dynamically typed languages say that a big part of the reason is that those languages require less code to do things. This isn’t true in general. It just happens that the statically typed languages most people are familiar with are rather verbose. That verbosity isn’t really a requirement of static typing.
Dice Example
The second example was based on the idea of a student writing a text-based game with a D&D flavor. The error in the Python code occurs on line 8 where we accidentally typed n
instead of m
. Because that line occurs with rather low frequency, this program runs successfully in Python most of the time. Every so often it crashes on line 15 saying that m
isn’t defined.
Deciding how to convert this to Scala is challenging and I could have done it in several ways. The key is that all of them would have produced syntax errors. If we try to run the Scala code shown above we get the following.
-- [E007] Type Mismatch Error: /home/mlewis/Test/sdice.scala:6:8 ---------------
6 | n = "critical miss!"
| ^^^^^^^^^^^^^^^^
| Found: ("critical miss!" : String)
| Required: Intlonger explanation available when compiling with `-explain`
-- [E006] Not Found Error: /home/mlewis/Test/sdice.scala:13:6 ------------------
13 | (n, m)
| ^
| Not found: mlonger explanation available when compiling with `-explain`
2 errors found
Error: Errors encountered during compilation
It points out that there are really two problems here. One is that we used n
instead of m
. The other is that in the Scala code, m
is defined in the nested scopes of 8, 10, and 12 so it doesn’t exist at the end of the function when we try to return it. As before, if you write this in an IDE you don’t even get to the point of trying to run the code, instead, you will see errors in the IDE and get immediate feedback that you need to fix things as opposed to needing multiple runs to even find out there is an error.
Class Typo Example
The third example focuses on how typos in dynamically typed languages can go silently unnoticed and produce interesting logic errors. For the code, I included the redefinition of sqrt
as well. In this example, lines 16 and 20 are problematic, but both run just fine in Python 3 with no complaints. Line 16 adds a new field to the object and line 20 successfully turns sqrt
into multiplication by two.
Trying to run the Scala code produces helpful error messages that will tell whoever made these mistakes exactly what they did.
-- [E008] Not Found Error: /home/mlewis/Test/sclass.scala:15:4 -----------------
15 | p.names = "there"
| ^^^^^^^
| value names is not a member of Person - did you mean p.name?
-- [E052] Type Error: /home/mlewis/Test/sclass.scala:19:12 ---------------------
19 | math.sqrt = (n: Double) => 2 * n
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| Reassignment to val sqrtlonger explanation available when compiling with `-explain`
2 errors found
Error: Errors encountered during compilation
Lambda/Scoping Example
My last example was more Python-specific, though it can happen in other languages, it is particularly bad in languages that don’t provide block scope. The problem occurs when a lambda closes over a variable in a loop. Generally, the author expects that variable to have different values in each iteration, but if it is the same variable because of scope issues, all the lambdas will use the value at the last iteration. That is what happens in the Python code shown here. It is a subtle bug in this case that could go a very long time before anyone notices it.
This doesn’t happen at all in the Scala code. The declaration of d
on line 8 is unique for each iteration of the loop. It is worth noting that in Scala, it is even safe to close over i
in this code as every iteration gets a unique instance of that value as well.
This is the one example where I did opt to write a second version of the Scala code that uses a more idiomatic Scala style. Instead of using a var
and a for-do
, it is more idiomatic to use a for-yield
to create the collection directly. This style could be done in Python as well with list comprehensions, though in my testing that did not fix the bug.
General Philosophy
I will note that my general philosophy in my own choice of tools, as well as what I teach students, is to prefer tools that move typos and other mistakes up the “hierarchy” as much as possible. The more things that become syntax errors instead of runtime errors or logic errors, the better. The best way that I have found to do this is to use statically typed functional languages. There is some evidence to support this, though the effect is not that strong. My favorite, simple example of this is the following line of Scala code.
val adults = people.filter(_.age >= 18)
There are very few typos one can make on this line that don’t become syntax errors. The only things one can mess up are the comparison and the numeric value and those mistakes become logic errors in all languages. I would argue that the rise of the Rust language at the time of my writing this blog is one of the strongest lines of evidence of industry seeing the value of a language that turns what would be logic or runtime errors in other languages, especially in C/C++, into syntax errors. Personally, I hope that this trend continues.
Going back to teaching, I think that there is value in teaching students to write good code and use tools that we feel help us produce quality software that is free of bugs. Personally, I like to start that from the very beginning.