Feedback on the lecture's use of Java

Hello,

this is essentially an extended feedback on the use of Java as a programming language for this course which obviously doesn’t fit into the comment box of the evaluation. I as a student will not be able to profit from any changes to this but I would still like to argue the case. This started out as a post about the usage of instanceof in this project, but I realized midway that the problem wasn’t instanceof, but rather Java. I kept that in, because it highlights the problem I perceive.


Usage of instanceof in the Compiler Project

I couldn’t help but notice some comments on the forum (by distinct tutors), claiming that instanceof is bad style and that the visitor pattern isn’t really needed for this project.

(Throughout I refer to instanceof with pattern matching, NOT instanceof combined with a cast)

I already completed the Software Engineering Lab (the lecture formerly known as Software-Praktikum) and instanceof was maligned similarly. Since I was learning Java at the time I didn’t think to argue against it, because maybe the alternatives provided were really better, but alas.

I searched a bit on stack overflow and most of the questions regarding this topic strike a similar tone. The answers are usually a combination of:

  • “It’s a code smell”. (= “Hell, if I know!”)
  • “Use inheritance instead.”
  • “Use the visitor pattern instead.”
  • “It violates the Open-Closed principle.”
  • “It violates the Liskov-Substitution principle.”

Basically from my understanding the two principles urge us to:

  • minimize the amount of changes you have to make to existing code in order to add new classes/functionality. (Open-Closed principle), and
  • make sure that new sub classes do not break the contracts of old classes (Liskov-Substitution principle)

Now if I have a functionality which behaves differently on different subtypes of a class and I use instanceof to essentially switch on these behaviors (i.e. a potentially cascaded if then else construct) that violates the above principles in the sense that:

  • If I add a new class/functionality then I have to go everywhere I discriminate on type and check that I don’t need to add a new case. If I used inheritance instead the static analysis would complain that a method is not overridden. (See the next list for a problem with this)
  • Similarly I have to check whether an existing check subsumes the new type I added and make sure that the behavior in that case is compatible.

Note that inheritance and the visitor pattern come with their own set of problems:

  • If a method in a superclass has a default implementation (such as simply doing nothing, or returning false) then I will have to remember to override that behavior, because semantic analysis doesn’t inform me about it.
  • Visitor forces me to create a new class hierarchy just for adding a single piece of functionality. Every member of the target hierarchy requires me to copy and paste boilerplate accept methods as well.
  • A single behavior is scattered across many files (inheritance) or many methods in a different file. This is less painful if the behavior is complex but becomes completely ridiculous when the behavior is small.

I do not see a clear winner here,

In the compiler project you might want to implement the toString method for primary expressions (abstract class PrimaryExpression) by either:

  • Using inheritance and overwriting the method in each case, which will always be of the form return "Const" + getToken() or return "Var" + getToken(). For the three constant primary expressions I could add a new class (yay!) just to bundle that ONE behavior. This means 5 classes and #unrelated_methods + 5 methods (including prototype) OR if I bundle constants 6 classes and #unrelated_methods + 3 + 1 methods (of course you need a constructor for your fancy new constant class, even if it has NO fields :upside_down_face:)
  • Using instanceof in the abstract class produces a one liner that is easy to read and bundles this very simple functionality in a single place. It needs 5 classes and #unrelated_methods + 1.

This same reasoning applies to my methods producing a formula out of an expression and to a degree to type checking. Essentially if I pulled those into the PrimaryExpression class the subclasses would merely be a way of enforcing that variants of PrimaryExpression exist. Essentially I could give PrimaryExpression an enum that determines which one we are dealing with and switch over that and not much flexibility would be lost. (Now people wouldn’t complain about instanceof, because that would be replaced by switch. Rather they would say that it is not good OOP style…)

Essentially the natural way to implement something so simple is frowned upon, because… this is Java and your program is bad if it doesn’t have 500 classes at least.


Shortcomings of Java

Everytime I ask myself whether there is a simple way to do something in Java without huge drawbacks the answer is to use a complicated and costly workaround that doesn’t behave like I would expect. All of the following things are useful and efficient ways to solve simple problems in other languages and they suck in Java:

  • Optionals: Tacked on, forces boxing of primitives. I could find no guarantees that it doesn’t use up more space and time than simply dealing with null. Since Java lacks pattern matching you have to use methods to unpack them which makes the code more verbose.
  • Generics: Tacked on, forces boxing of primitives. Due to (premature) type erasure you can’t do some of the most interesting things with those. Since primitives must be boxed (= performance hit) for the use of generics you are forced to essentially write a non-generic class for every primitive you need in addition to the generic implementation you provide. A prime example of this is the existence of Stream and IntStream.
  • Streams: Tacked on. list.stream().map(...).collect(Collectors.toList()) vs. map f l. There is hardly anything more natural than applying some function to each element of a list/array/whatever, but why make it clear and concise when you could use 10 times as many symbols to say the same thing.
  • Monads: 404 not found (I realize that streams and optionals and all of that jazz are technically monads, but they are handcrafted by JVM experts and are still bad. Anything that I “handcraft” will be a million times worse)
  • Pattern Matching: It’s coming this fall! As a preview! And then it becomes a full feature in a year, maybe! Of course you won’t be able to switch on multiple things at once. And until it’s released you can use visitors and inheritance to bloat your codebase instead.
  • Let Expressions: I guess this is already here, but they don’t really advertise it. let x = (a, b) in vs. x instanceof Pair, kinda.

I am aware that this is a huge wish list and implementing those features correctly and at no cost to the programmer using them, requires expert knowledge on language and compiler design as well as a historic outlook on what was already done and how it panned out. (Incidentally I realize how little expertise I have in those fields, thanks to the compiler project)
Of course the people maintaining Java and the JVM have those prerequisites but what they work on is outdated and was never meant to provide any of these features in the first place. This is why all of the features above that are in the pipeline (or were in the pipeline) take forever to arrive. Once they do arrive it turns out that some compromise was necessary to make them work and thus they ended up worse than their counterparts in other languages.


Alternatives

Sadly, Java is the only language I really know (except the token OCaml and C I got in Prog1 and Prog2 and some Python I learned on my own: import ...) so maybe this is an instance of the grass being greener on the other side. In any case I can’t make a strong case for any other language, but I will try.

Basically the idea seems to be to introduce people to a functional language and then to show them some of the lower level stuff that goes on (MIPS, C, SysArch) and after that you show them Java.

After Prog2 students don’t hear much about functional programming again (not as far as I can tell from the curriculum) unless they choose the logic/coq path. In Prog2 functional concepts appear in the kNobel tutorial mostly. So less than ten people get useful information about functional features in Java (or the lack thereof) and everyone else gets to not know about it, unless they stumble over it by chance.

As for alternatives to this I think there are definitely some candidates, like C++, Python or Rust. I don’t think any of them has all of the features mentioned above but all of them have a good portion of them. I realize that Python doesn’t have the same typing standards as Java, Rust and C++ but there seems to be a type annotation functionality which fixes this problem. What these three languages have in common is that they are actually multiparadigm, not just single paradigm + some other features.

Java enforces a single paradigm, and if you want to do something in a more elegant/efficient fashion you are reprimanded for it. The same goes for OCaml in Prog1, where the mere mention of a for-loop makes you a pariah.

Why not teach people a modern multiparadigm language over a year (culminating in the Software Engineering Lab) so they know all the tools they have at their disposal in the real world, instead of thinking that there are a bunch of distinct toolboxes and for each problem only one toolbox is applicable.


Conclusion

I know this might not change anything, but I am sufficiently frustrated with Java to write this much and I doubt I am the only one. If no one ever complains (and how would we know if that happens, the evaluations are anonymous!) then whoever is in charge might think everything is fine.

Thanks for reading!

5 Likes

Hi!
Of course, I cannot say much about future iterations of the course, but I wanted to make a few comments anyway.

A lot of your criticism is valid, some of it concerns object-oriented programming in general, and some (generics) are Java-specific problems.
Of course, you’re also right that the single-paradigm approach is often a drawback.

However, I would like to point out that there are no alternatives that are clearly better. To comment on the ones you used, pointing out some flaws:

  • C++: basically inherits all the undefined behaviour issues from C. A very common language, but can be ugly to debug, especially for beginners.
  • Rust: has some fancy features that are really cool, but unfortunately, they can make some things pretty hard. A famous example is doubly linked lists. Furthermore, many are of the opinion that you need to have dealt with other languages before to understand the advantages of Rust.
  • Python: there is the issue with types that you already mentioned, as well as some other weird things. Importantly, object-oriented programming in python feels tacked-on.

So, here are a few arguments for Java:

  • you’re supposed to learn OOP. Java forces you to do it “properly”.
  • Java is still a very widespread programming language.
  • Knowledge of Java is expected for later courses as well as in the industry.

In the end, you’ll find that all languages have their downsides and you’ll find that some are better, some worse for specific tasks. No one language clearly comes out on top, and most of it just comes down to personal preference.

2 Likes

Thank you for the feedback.
We are always trying to improve the course.
We are aware that Java has some (or maybe many) drawbacks.
An alternative would be nice.
But as you mentioned it is not so easy to find a suitable replacement.

Why one might want to use a language that leads to one / a few paradigms
instead of an all-rounder:
We want to show you multiple paradigms during the first semesters.
If the language allows for everything the same, it is easy to stay in
already known paradigms and not venture out.
For instance, python supports classes but many python programmers will not use them
because “it’s easier without the boilerplate code”.

In the end, nearly all modern languages are multiparadigm.
For instance, all modern imperative languages introduce functional design patterns, and
all functional languages have some form of modules/structures and imperative control flow using monads.

A problem with Rust for instance is that the learning curve is quite steep.
Ideally, the high-level language should be easy to learn and use, allow for everything with good notation, has strong safety guarantees, clean semantics, support all paradigms, be relevant, and be imperative (because we want to teach imperative programming in this course).
Some languages fulfill more criteria than others. But I am not aware of a language that fulfills all of them.

It might also be advantageous to show many programming languages with their own strength and weaknesses
for a full picture.
The hope is that you learn what you like, what is out there, and that in the end, a language is just a tool that does not matter so much. Some things become easier some harder but as a programmer, you will be able to handle all programming languages.
There is a large toolbox and you use and combine tools and use the programming language that fits best.
(But there are pitfalls. For instance, you should not rewrite the whole code because a new language looks better (again there are exceptions but that goes on and on).)
Hopefully, the courses do not let the impression arise that the worlds are separate and that the world is like imperative vs. function. But rather that there is imperative programming, functional programming, and a whole lot more (declarative programming, array programming, logic programming, …), and we use ideas from everywhere and incorporate them into your programming style.

4 Likes

First, thank you for your feedback. (Although I’m not sure whether you actually want to change things or just vent about Java)

Aside from the other criticism, you really can not. At least, I would consider this much less beautiful. You Identifier already has different fields compared to the other classes, since you will want to annotate it with the declaration. I guess you want a Constant class hierarchy / BNF / ADT.

Some more thoughts:

It’s actually not that bad. The optimizer is quite good at removing this, and even then Java never made the promise to be “really fast”. It’s already orders of magnitudes faster than Python. Yet still, all your optimization can do nothing against an untimely GC hit.

We’ve already talked about this – it’s not type erasure but rather that you do not have Traits, because this is against the Java design philosophy (“name things”), since they are non-local, which I can understand.

You can write code that basically compiles to the same assembly as code the code you would have gotten with Traits.

Related:

That’s not even a problem, I would say. We have advanced past the 1980 where you could only use 8 chars in your variable names and 80 chars in your lines.

Code is read much more often than it is written. A bit of verbosity makes code more clear. Also, your code snippet tells me a lot more that I would not know from just using map: In what order do we map? Is it in parallel? What happens to the original list? What kind of list is the new list?

Also, it’s not even that bad to write. You type the first few letters and hit auto-complete. Since the main problem when writing code is thinking and not typing, it’s really not an issue in practice.

Also, I think it makes your code more readable. You won’t miss any important functionality, just because someone compressed it into a 5 char code snippet that hides somewhere in plain sight (*pain*)

Let’s not miss the elephant in the room: Java, like all imperative languages, have very good support for the State/IO monad. In practice, this removes 95% of the need for other monads.

Some more notes on the curriculum:

Well, yes, except if they want to. After Prog2, I used the following languages for University:

  • C (OS)
  • Rust (Verification, NP (nowadays), Competetive Programming)
  • C++ (Compiler Construction, Competetive Programming)
  • R (a HiWi job, also Statistics with R but I did not take that)
  • Java (SoPra, NP)
  • LaTeX (which is a functional language!)
  • Coq/Gallina (ICL, Semantics)
  • That toy functional language you build in Semantics
  • Python (BDE, Security, playing CTFs)
  • Kotlin (another HiWi job)
  • OCaml (for research)
  • Z3 (also for research)

After Prog1 and Prog2, you know enough about several paradigms to choose which paths you want to take. Many people really find imperative programming much more intuitive, and many people like writing low-level code for some reason.

That would be an interesting approach, but you can already see several problems with that (which you know already):

  • You want students to understand how computers work bottom-up, so you need to talk about assembly
  • Languages historically evolved by stealing things from their predecessors, so it’s didactically worthwhile to teach them that way
  • No language is multi-paradigm enough for you to do this. When you try functional programming in Rust, you quickly run into ownership issues. If you try it in Java, you miss ADTs, and clash with the “name all the things” policy. If you try Python, you miss types (yet they are very important, and CS should definitely teach that. No, typing annotations are not a solution, more tacked on than anything you’ve ever seen before, and by default not even enforced by some type checker). Try C++ and you get undefined behavior. Also, “functional language is when immutable” and none of those are so by default.

Since you are already teaching two languages (assembly + the one hypothetical language to rule them all), you can bite the bullet and teach several since in the end, you expect CS degree holders to be able to pick up any programming language in a reasonably small time frame.

Teaching is hard, and it’s also very easy to forget how long it took you to learn all the things you already know. I was already rather good at programming before I started university, and I guess most “good” students are (Both in the sense that they are good because they have experience, but also they started early since they enjoy writing code). However, this does not hold in general, and you want to design your curriculum such that students starting with zero knowledge about programming can succeed.

Well, different programming languages solve diffenrent problems. As an engineer, you are expected to know the best tool for your job. This is not only due to language design, but also depends on the set of libraries available in the ecosystem. Thus you often make choices like
OS? C, assembly, Rust (maybe?)
Systems programming? C, C++, maybe Rust
Glue code? Bash, or python!
Heavy number crunching? C, Fortran, or maybe Python with optimized numpy…
Interacting with databases? Java is rather good at this…
Web stuff? :scream: only bad choices

Here’s a fun exercise: Try writing the Wordle project in Rust. Do not use any of the more fancy std features (like maps or sets), but implement the trie yourself. Then consider that you are likely in the top 10% of your class and estimate how good this project would work for the average student.

With all due respect, this might be the case. Many of the mentioned problems also exist in C++ – where lambada are even more of a horrible bodge, you got five different constructors for copying, moving, … Spend five minutes on r/programmerhumor and you will find that programmers like to spend their spare time hating on all but their favourite programming language.

Rust will give you similar problems once you break the unsafe escape hatch: Try writing unsafe code that does not have undefined behavior (use miri to check). You will find (try it with Wordle) it’s much harder than C since you have all the implicit safety and validity invariants of your fancy reference types to worry about. So using unsafe Rust as a C replacement is not really an option.

Once you learn enough languages, you find that almost all are horrible. You then come up with Rust, which is not horrible, but also very hard to learn without knowledge of the prior art. Or you stick to your well-behaved niche of Haskell with all it’s nice syntactic sugar for monads, and then also use a language no one really understands. Alternatively, you stick to a language with constructs closer to hardware, bite the bullet, but actually write maintainable software others can modify.

That’s my two cents on your two cents. Again, thanks for the feedback. I think we should continue this discussion in person, if at all.

2 Likes

Thanks for all the detailed answers!

I wanted to see whether what I said would hold up, so I actually tried to do Wordle in rust and in 6 hours I didn’t even get past the Trie implementation:

The dict.rs file doesn’t report a single error, only the call to lookup in main.rs complains because the parameter t doesn’t have the right type. What happens if I add a & before the t?

I get 9 (!!!) errors in the previously error free dict.rs :upside_down_face: :upside_down_face: :upside_down_face:

Apparently changing a function call in main causes an unrelated module to break. I have never seen any behavior like this before.

I don’t understand how borrowing works at all (didn’t read that part yet) but it must have very strong non-local effects on the program. I have tried all kinds of symbols in front of the variables (*, &, &mut, etc) but usually I can only remove a few errors. And when dict.rs is clean then main.rs breaks and vice versa.

I suspect that the Box< ... > part in the definition of a trie is to blame but if I replace that with &mut I have to specify a lifetime, but it doesn’t say where.

Since it is apparently impossible to learn rust within 6 hours without already having a lot of programming experience I concede that it would be a bad fit for this course.

Additional note: rust doesn’t support strings like a normal language… it is all utf-8 which means that you can’t even index them and accessing characters becomes cumbersome.


I also concede that Java is probably the best compromise for a learning language. However it runs the risk of letting people get away with not learning how to program complex systems and understand what they are doing (which is what I want to learn). There is also no value to working in an interpreter or a Jupyter notebook to me. It is unappealing (small scale) and at the end you have nothing to show for your effort.


I didn’t consider a lot of things that came up in these three answers, and it goes to show that my knowledge (and hence agency) as a programmer is still too limited, I acknowledge that.

Ultimately I think there are too few avenues to get this kind of meta information (party opinion based) about cs topics, as provided by the kNobel tutorial and the many helpful answers by tutors here. The lecture notes are great, but a lot of actually interesting things are mere side notes (problems with generics was an example that came up).

If I try to look on the internet I find a lot of bad information (outdated or simply wrong), especially on stack exchange. These sites are interesting because of the potential to get expert opinions and experience and that is why MathOverflow works so incredibly well. In contrast the cs version features a lot of bad posts by non-experts. I have to basically validate that they know what they are talking about before using the information.

1 Like

We try to teach people how to approach and work with complex systems.
Interpreter (Prog 1) and compiler (Prog 2) are some of the largest projects that come to mind (that are still manageable).
Our projects are medium-sized in these lectures as we want to teach a lot more.
There are dedicated lectures for large-scale projects (for instance Compiler Construction if you want to write a more full-fletched compiler or the Software-Praktikum for another larger piece of software).

It is a quick way to try and play with a programming language. You type, modify, and directly run some code.
It helps especially at the beginning of learning.
Another advantage is that it does not require first setting up a whole development environment (which depending on the language can be quite tedious).

We try to give such information at many points.
But it is impossible to teach all this:

  • It is hard to teach as it is unstructured
  • Not interesting to everyone
  • way too much information
  • heavily depends on experience
  • way too little time to go into every topic

As you noticed, we try to give such information to interested students in many ways:

  • remarks in lectures
  • side notes in the lecture notes
  • further reading mentioned in the lecture notes
  • the kNobel tutorial in Prog1 where experienced students reported on their favorite aspects of computer science
  • the kNobel tutorials in Prog2 on extended subjects.

And you are doing everything right by asking many questions in the forum.
In the end, communication with older students and your peers is one of the best ways to learn interesting non-standard things.

1 Like

You might want to read Introduction - Learning Rust With Entirely Too Many Linked Lists

You not knowing ownership is going to make writing Rust hard. For example, your insert currently takes ownership of the trie and thus frees it at the end. You want a mutable reference there. You likely also will have fun with lifetimes, insofar as that the lifetime of the children is the lifetime of its parent, but I’m unsure about this since I also don’t know that much about Rust.