A comparison between a few programming languages

Maurizio Tomasi <maurizio.tomasi@unimi.it>

Change history

Date Comment
2025-02-17 Update the text for C++ and Nim
2023-12-16 Update a few links, correct typos
2023-05-23 Add a few links, fix some typos
2023-01-02 Show a more interesting comparison between Ruby and Crystal
2022-12-05 Add a few more information about Pascal
2022-09-30 Add a section about Crystal, fix some references
2021-12-17 Add a description of how to «learn» a language
2021-12-08 Add a description of the D language, plus a few more fixes
2021-01-02 Fix a few typos
2020-12-29 First release

Introduction

The course Numerical tecniques for photorealistic image generation (prof. Maurizio Tomasi) aims to teach students how to develop complex computer codes to model the phenomenon of light propagation. The student will learn how large programs are structured, which are the best techniques to organize the code, and how does one ensure quality and reproducibility of the results.

As the course does not mandate the use of any official programming, students might feel uneasy at picking one language out of the many available: not only there are several well-known languages whose usage is well-established in universities and industries (e.g., C, C++, Fortran, Java, …), but a plethora of new languages are being created every year: Rust, Julia, Nim, Crystal, etc.

In this document I show the peculiarities of a few languages that are suitable for the course. I will start with C++, which is probably well known by the majority of the students, and then I will move to other languages. You are advised to read the section about C++ even if you do not plan to use this language, as I will often refer to it when describing other languages.

All the languages are presented following this schema:

The purpose of this document is to provide students with a general idea of some languages, but it is surely not meant to be exhaustive! Other languages that are not covered here might still be well suited for the course (e.g., Chapel, OCaml): contact the teacher if you are interested in any of those.

How to learn a new programming language

The opinion of the teacher is that a student willing to learn a new language should follow these steps:

  1. Find and read a good book, or blog posts, or YouTube video that show the basics of the language. Try to follow the exercises and type them on your own instead of using cut-and-paste.

  2. Watch a few YouTube videos that show how to actually use the language beyond the syntax. This is a valuable way of learning how to use editors and IDEs effectively, as these abilities are difficult to grasp while reading a book. The difference with the videos mentioned in Point 1 above is that here you should understand how the language works in a development environment. For instance, A Gentle Introduction to Julia is a great way to learn Julia’s syntax, but videos like Getting Started with Julia (for Experienced Programmers) are more effective at this stage, because the presenter shows how to set up Visual Studio Code and use plugins to develop in Julia more efficiently. (Please do not assume that Vim or Gedit are «all you need» for this course: this is a mistake that students have done in the past!)

  3. Go to the Project Euler website and use your newly-learned skills to implement solutions for the first 5−10 problems. The problems in Project Euler are increasingly complicated, but in a mathematical sense: you usually do not need anything else other than int/float variables and a few statements (if, while, for) to solve them. (But as you progress through the problems, they quickly become mathematically hard to solve, so please do not go beyond the first 10–20 problems.)

  4. Once you have successfully solved a few problems in Project Euler, try to solve a few problems from the past editions of Advent of code. These programs require more skills than Project Euler’s, as they are focused on coding rather than on mathematics. As Project Euler’s problems, these are listed in increasing order of difficulty. It is not needed that you do the exercises for this year, as the site keeps the archives of the past editions (e.g., 2020, 2019, etc.).

C++

There should be no need to present the C++ language. It is a widely used language, and it is ok for the kind of tasks required in the course. It was developed in the 1980s by Bjarne Stroustrup, which adopted Object-Oriented Programming (OOP) atop the C language. In the following years, C++ has progressively abandoned the OOP-centric approach and has adopted other programming styles as well; today is really a «multi-paradigm language».

To implement a «vector» data structure, one can use a struct:

#include <sstream>
#include <string>

struct Vec {
    double x, y, z;
    constexpr Vec(double ax = 0.0, double ay = 0.0, double az = 0.0)
        : x{ax}, y{ay}, z{az} {}

    std::string format() const {
        std::stringstream sstr;
        sstr << "<" << x << ", " << y << ", " << z << "\n";
        return sstr.str();
    }
};

[[nodiscard]] constexpr Vec operator*(double t, const Vec & v) noexcept {
    return Vec{t * v.x, t * v.y, t * v.z};
}

[[nodiscard]] constexpr double operator*(const Vec &v, const Vec & w) noexcept {
    return v.x * w.x + v.y * w.y + v.z * w.z;
}

As stated in the introduction, the purpose of the code examples shown in this document is to illustrate how to implement custom operators for the Vec class, namely sum, dot product, and scalar-vector product. Printing a Vec object is implemented through the Vec::format method, which uses a std::stringstream object; this is the standard approach in C++, although students developing their programs for the course can rely on more advanced libraries1 to format strings.

The code above makes use of a few features of recent C++ standards, like the [[nodiscard]] and constexpr specifiers. Students willing to use C++ in this course should become accustomed with the most recent releases of the C++ standard. (C++23, at the time of writing.)

The following code implements the Ray and Sphere data structures; the latter has the method intersect, which checks if a ray (oriented line) and a sphere intersect. It is not fundamental to understand the mathematics used here; it is enough to note that the code makes use of overloaded operators on the Vec class, like the ones I defined above (sum, dot product, scalar-vector product).

struct Ray {
  Vec m_o, m_d;
  double m_tmin, m_tmax;

  Ray(const Vec &origin, const Vec &dir)
      : m_o{origin}, m_d{dir}, m_tmin{DEFAULT_TMIN}, m_tmax{DEFAULT_TMAX} {}
};

struct Sphere {
  double m_r;
  Vec m_p;

  constexpr Sphere(double r, Vec p) : m_r{r}, m_p{p} {}

  [[nodiscard]] constexpr bool intersect(Ray &ray) const noexcept {
    const Vec op{m_p - ray.m_o};     // Vector difference
    const double dop{ray.m_d * op};  // Dot product
    const double D{dop * dop - op * op + m_r * m_r};

    if (0.0 > D) {
      return false;
    }

    const double sqrtD{sqrt(D)};

    const double tmin{dop - sqrtD};
    if (ray.m_tmin < tmin && tmin < ray.m_tmax) {
      return true;
    }

    const double tmax{dop + sqrtD};
    return ray.m_tmin < tmax && tmax < ray.m_tmax;
  }
};

Apart from the mathematical details, everything in the code above should look familiar to anybody who has written C++ programs in the past.

Let’s now list a few advantages of C++:

However, the language has its warts:

Past experience has shown that people that picked C++ because they felt the course would have been «easier» to follow have had to tackle several problems that were non-existent in other languages. If you want to pick an easy language to use, you should avoid C++ and choose something else; good choices are C#, Nim, or Crystal.

On the other hand, people that already had a strong interest in C++ were satisfied to have used it, as they felt they significantly improved their coding skills.

Nim

Nim (nim-lang.org) is not as widespread and used as C#, Kotlin, etc. However, it is extremely well designed and performant. It was created by Andreas Rumpf in 2008, and it is deeply inspired by the Pascal language. (In fact, the very first Nim compiler was written in FreePascal.)

Nim is the language I have used in the past to prepare both the HTML version of the course slides and the course notes. I picked it because at the time it was one of the very few languages that was able to produce a standalone executable or a Javascript code to be loaded in HTML pages3. This means that Nim programs can be run either from the terminal or within a web browser. (See below for an example.)

Nim’s syntax looks like a mixture of Python and Pascal, as the following implementation of the Vec data type shows:

type
    Vec = object
        x*, y*, z* : float64

# The $ procedure translates any value to a string
func `$`*(v : Vec) : string =
    result = "<" & $(v.x) & ", " & $(v.y) & ", " & $(v.z) & ">"

func `*`*(a, b : Vec) : float64 =
    result = a.x * b.x + a.y * b.y + a.z * b.z

func `*`*(t : float64; v : Vec) : Vec =
    result.x = t * v.x
    result.y = t * v.y
    result.z = t * v.z

In Nim, a object is like a struct in C++. The use of * at the end of functions, variables, and methods indicates that these should be made public, i.e., they should be exported whenever the file is imported by some script. It is quite like the public keyword in C++.

Nim uses the type keyword, much like Pascal. However, it avoids using {} or begin..end like in C++ or Pascal, because like Python it uses indentation to delimit the extents of functions.

Note that you can redefine operators like + and * using the func keyword, instead of using dedicated keywords like operator in C++: only, you must wrap them within backticks (`), like in func `$`*.

Returning values from procedures requires to use the implicit result variable. (This feature is shared with Pascal.)

The following code shows how to implement the Ray and Sphere data structures, as well as the ray/sphere intersection:

import math

type
    Ray = object
        m_o* : Vec
        m_d* : Vec
        m_tmin* : float64
        m_tmax* : float64

    Sphere = object
        m_p* : Vec
        m_r* : float64

func intersect(sphere: Sphere; ray: Ray) : bool =
    let op = sphere.m_p - ray.m_o
    let dop = ray.m_d * op
    let D = dop * dop - op * op + sphere.m_r * sphere.m_r

    if 0 > D:
        return false

    let sqrtD = sqrt(D)
    let tmin = dop - sqrtD
    if (ray.m_tmin < tmin) and (tmin < ray.m_tmax):
        return true

    let tmax = dop - sqrtD
    return (ray.m_tmin < tmax) and (tmax < ray.m_tmax)

Nim introduces three ways to declare variables:

The difference between const and let is subtle; typically the programmer does not know what the value used in a let will be, but once it is assigned it is not going to be changed. A typical case for let is for some input from the user (e.g., a file name containing some input data): the programmer cannot tell what is going to be the name of the file, but surely the variable file_name is not going to be changed once it is assigned. On the other hand, mathematical constants like π are known in advance and should be declared as const. This distinction improves a lot the clarity of the code, as most of the situations where one declares a variable, they usually mean to use a let, i.e., the variable is assigned but then never changed.

Nim has several advantages over other languages:

Disadvantages:

The opinion of the teacher is that Nim might be the best language for this course, from a technical standpoint. Unfortunately, the poor tooling and the scarce documentation have been a pain point for several students.

Crystal

The Crystal language is one of the youngest languages considered in this document. The history that leads to Crystal is interesting and worth to be told. Crystal is a reboot of the famous Ruby language, which in turns is a take over the old Perl language. Perl is a language created by Larry Wall, whose name was originally «Pearl» (a reference to the pearl of great price in Matthew’s Gospel) and indicated the aim to produce a useful tool to administer servers. Perl was used to create the autoconf tool, which is well known to any Linux programmer; moreover, it backs up websites like Booking.com. However, despite being very famous, Perl is also known for its lack of readability; consider this example, taken from a Perl tutorial:

# Save this in a file "test.pl" and run it using
#
#    perl test.pl

foreach ('hickory','dickory','doc') {
   print;
   print "\n";
}

The program is very simple to read and more lightweight than it would have been in C++ (with its #include <iostream>, and using namespace std, and stuff like that). However, the output of the program is a surprise:

hickory
dickory
doc

This happens because within the foreach loop the hidden variable $_ is set to the value of the three elements in the list, and the print command outputs the value of $_ if nothing else is passed. Perl is full of hidden parameters and shortcuts that can make source code very hard to understand.

For this reason, in the mid-1990s a Japanese programmer, Yukihiro Matsumoto, created Ruby, whose name is of course a reference to «pearl», Perl’s original name. Ruby’s aim is to be as versatile as Perl but far easier to read. It achieved huge success in early 2000s and was fundamental for the spread of modern websites, thanks to its famous web framework Ruby on rails, which powers sites like GitHub (we’ll use it a lot in this course!) and Airbnb. Ruby programs read like English and are easy to understand even by people that are not proficient with the language. Here is the Perl program shown above translated to Ruby; as you can see, there are no hidden variables, yet the program is very simple and free of noise:

# Save this in a file "test.rb" and run it using
#
#    ruby test.rb

['hickory','dickory','doc'].each do |x|
  puts x
end

The problem with Ruby is its slowness, as it is an interpreted language much like Perl and Python: programs written in Ruby can be orders of magnitude slower than C++ programs!

In 2014, a team of developers released the Crystal language: again, a pun to «pearls» and «rubies»! The syntax of Crystal programs is very similar to Ruby’s, but its creators added ahead-of-time compilation through the LLVM framework: this means that programs must be compiled into an executable before being executed. (It’s the same in C and C++, where you must call gcc or g++ to build an executable, but unlike Perl, Python, or Ruby.) The usage of LLVM enables Crystal programs to be as fast as programs written in C++, Julia, or Rust.

Let’s show how to define a new type, Vec, using Crystal:

# A "struct" is like a "class", but it has fewer features and enables
# the compiler to produce more efficient code
struct Vec
  # These would be called member variables; their type is
  # deduced by the constructor `initialize` (see below)
  property x, y, z

  # `Initialize` is the name of the constructor; in C++ you would
  # have written `Vec::Vec`. Note that using `@` in front of the
  # parameters mean that they must be used to initialize the
  # properties with the same name. Here we declare that `x`, `y`,
  # and `z` are 64-bit floating point values.
  def initialize(@x : Float64, @y : Float64, @z : Float64)
  end

  # Operator overloading is very simple to do in Crystal!
  #
  #                   +------+     This means that the result is of
  #                   |      |  <- the same type as the class, `Vec`
  def +(other : self) : self
    # Like in Julia, if it's the last line before a `end`,
    # you can avoid writing `return` explicitly
    Vec.new(@x + other.x, @y + other.y, @z + other.z)
  end

  def -(other : self) : self
    Vec.new(@x - other.x, @y - other.y, @z - other.z)
  end

  # Specifying the return type is not mandatory, the compiler is smart
  # enough to figure it out. If you want to be explicit, you can write
  #
  #     def *(other : self) : Float64
  #
  def *(other : self)
    @x * other.x + @y * other.y + @z * other.z
  end

  # `to_s` means: «convert this type into a string»
  def to_s : String
    "<#@x, #@y, #@z>"  # In strings, writing @variable means:
                       # «put here the value of the variable»
  end
end

The weird variable names with @ refer to member variables; in Python, you would write self.x instead of @x. As you can see, operator overloading is extremely easy in Crystal. The method initialize is the constructor; in C++ it would have been named Vec::Vec.

Defining Ray and Sphere is equally simple:

struct Ray
  property m_o, m_d, m_tmin, m_tmax

  def initialize(@m_o : Vec, @m_d : Vec, @m_tmin = 1e-10, @m_tmax = 1e+10)
  end
end

class Sphere
  property m_r, m_p
  def initialize(@m_r : Float64, @m_p : Vec)
  end

  def intersect(ray : Ray) : Bool
    op = @m_p - ray.m_o
    dop = ray.m_d * op
    d = dop ** 2 - op * op + @m_r ** 2

    # This kind of "if" syntax makes the code very readable like English!
    # But you can write plain "if" statements like Python and C++, if you want.
    return false if 0 > d

    sqrtd = Math.sqrt(d)
    tmin = dop - sqrtd
    return true if (ray.m_tmin < tmin) && (tmin < ray.m_tmax)

    tmax = dop + sqrtd

    ((ray.m_tmin < tmax) && (tmax < ray.m_tmax))
  end
end

The performance of the code is extremely good and perfectly comparable with C++ or Rust.

Here are a few highlights from the language:

The teacher believes that Crystal is a nearly perfect language to be used for this course. However, like Nim you must be prepared to cope with the parcity of documentation and the lack of a full IDE.

C#

The C# (pron. «C sharp») language has been developed by a Microsoft team lead by Anders Hejlsberg; its first release dates back to 2000. The aim of the team was to implement a «saner» version of the C and C++ languages, which explains the multiple puns in the name:

Unlike C++, C# adopts a OOP-centered approach. It does not aim to be 100% compatible with C++, which has enabled its designers to get rid of some of the complexities of the latter.

Under the hood, the way C# programs are executed differs significantly from C++, because they are run in a managed environment: the compiler translates the program to a low-level representation, which is then run by another program, called the Common Language Runtime (CLR). The purpose of the CLR is to translate the low-level representation into actual machine instructions that are sent to the CPU. This is in contrast with the way traditional compilers work: GCC and Clang translate the program directly to CPU instructions at compilation time.

Although the managed approach might seem slower to execute because of the on-the-fly translation made by the CLR to actually execute the code, the CLR implements a number of optimizations that make its performance similar to C++. In particular, the CLR measures the way the code is used during the execution and can re-generate CPU instructions according to what it has learnt6. Because of this, a program running in a managed environment requires some time to optimize itself (the so-called burn-in phase), but then after some time spent running it can be as performant as C++. In the tests I did while preparing the material for this course, the C# compiler was able to produce programs whose performance was always very close to C++’s.

Here is an example showing how to implement a basic Vec class in C#. Note that, despite a few differences, the syntax looks very similar to C++’s:

public struct Vec {
    public double x, y, z;

    public Vec(double _x, double _y, double _z) {
        this.x = _x;
        this.y = _y;
        this.z = _z;
    }

    public static implicit operator string (Vec v) {
        return string.Format("<{0}, {1}, {2}>", v.x, v.y, v.z);
    }

    public static Vec operator *(double t, Vec v) {
        return new Vec(t * v.x, t * v.y, t * v.z);
    }

    public static double operator *(Vec a, Vec b) {
        return a.x * b.x + a.y * b.y + a.z * b.z;
    }
}

C# provides a few high-level functionalities with respect to C++. One of these is first-class string support, which means that converting a vector into a string can make use of the handy string.Format class, instead of relying on the cumbersome std::stringstream class. Note that, unlike C++, C# does not force the programmer to remember when to use ; after a closing brace }.

To access the data members of a class, you must explicitly reference them using this; this is similar to the self parameter in the Python language.

Defining rays and spheres is similar:

public struct Ray {
    public Vec m_o, m_d;
    public double m_tmin, m_tmax;

    public Ray(Vec _o, Vec _d) {
        this.m_o = _o;
        this.m_d = _d;
        m_tmin = 1e-10;
        m_tmax = 1e10;
    }
}

public struct Sphere {
    public Vec m_p;
    public double m_r;

    public Sphere(Vec _p, double _r) { this.m_p = _p; this.m_r = _r; }

    public bool Intersect(Ray r) {
        Vec op = this.m_p - r.m_o;
        double dop = r.m_d * op;
        double D = dop * dop - op * op + m_r * m_r;

        if (0.0 > D)
            return false;

        double sqrtD = Math.Sqrt(D);
        double tmin = dop - sqrtD;
        if(r.m_tmin < tmin && tmin < r.m_tmax) {
            return true;
        }

        double tmax = dop + sqrtD;
        return r.m_tmin < tmax && tmax < r.m_tmax;
    }
}

Here are a few advantages of C# over C++:

The experience of students using C# for this course has been extremely positive so far.

D

The D language is the creation of Walter Bright, which authored one of the most famous C++ compilers available in the ’90: the Zortech C++ compiler (later called the Digital Mars C++ compiler). Bright was dissatisfied with the direction taken by the C++ committee, and he decided to develop a new language that was based on C but did not strive to maintain compatibility with it (unlike C++). The result is a more «modern» and elegant language, which however has not reached the same level of adoption as C++.

Unlike C++, D does not uses header files, as these are notoriously error-prone; rather, it use modules, which work similarly to Python:

import std.stdio;
import std.format;
import std.array;
import std.algorithm.searching;
import std.math;
import std.random;
import std.datetime.stopwatch;

Constants can be defined exactly like in C++:

const double DEFAULT_TMIN = 1e-10;
const double DEFAULT_TMAX = 1e+10;

Things get interesting when you define a new struct or class:

 Vec {
  double x;
  double y;
  double z;

  // This is a template over the type W
  void toString(W)(ref W writer, scope const ref FormatSpec!char f) const
       if (isOutputRange!(W, char)) // Template constraint
         {
           put(writer, "<");
           formatValue(writer, x, f);
           put(writer, ", ");
           formatValue(writer, y, f);
           put(writer, ", ");
           formatValue(writer, z, f);
           put(writer, ">");
         }

  // Another template
  double opBinary(string op)(const ref Vec other) const
       if(op == "*") {
         return x * other.x + y * other.y + z * other.z;
       }

  Vec opBinary(string op)(double t) const
       if(op == "*") {
         return Vec(t * x, t * y, t * z);
       }

  // Here we use a "mixin": a string that is interpreted at compile
  // time and substituted as a line of code. The "~" operator
  // concatenates strings.
  Vec opBinary(string op)(const ref Vec other) const
       if (op == "+" || op == "-") {
         return mixin("Vec(x " ~ op ~ " other.x, " ~
                      "y " ~ op ~ " other.y, " ~
                      "z " ~ op ~ " other.z)");
       }
}

The syntax struct Vec... is clearly derived from C++, but templates use a different syntax: instead of writing

template<typename W>
void toString(W & writer, const FormatSpec<char> & f) const

in D you write this

void toString(W)(ref W writer, scope const ref FormatSpec!char f) const

When you declare a template, you put the template names within parentheses, like (W), but when you use an existing template, you put the template parameters after the ! character. When we will explain the theory of compilers at the end of the course, we will learn why the D syntax is much more convenient to parse, but at the moment you should just accept this difference.

Operator overloading works differently from C++, because it does not force you to re-define several operators that all look alike:

// This is the same C++ code we saw before
[[nodiscard]] constexpr Vec operator+(const Vec &a, const Vec &b) noexcept {
  return Vec(a.x + b.x, a.y + b.y, a.z + b.z);
}

// This is almost the same as operator+: boring!
[[nodiscard]] constexpr Vec operator-(const Vec &a, const Vec &b) noexcept {
  return Vec(a.x - b.x, a.y - b.y, a.z - b.z);
}

In D, binary operators like + and are overloaded by the same template function opBinary(string op); in C++, this would probably look like the following:

template<std:string op>
constexpr Vec opBinary(const Vec &a, const Vec &b)

If C++ worked like D, the C++ compiler would translate a + b into opBinary<"+">(a, b) (still using the C++ syntax) instead of the usual operator+(a, b). The approach followed by D reveals its strengths when used with mixins, which is a way to tell the compiler to generate source code using string interpolation functions:

Vec opBinary(string op)(const ref Vec other) const
     if (op == "+" || op == "-") {
       return mixin("Vec(x " ~ op ~ " other.x, " ~
                    "y " ~ op ~ " other.y, " ~
                    "z " ~ op ~ " other.z)");
     }

The strange-looking if placed before the braces ({}, which is something not admitted in C++) is a template constraint: it tells that the template should not be applied for any string passed to op, but only if the string is either "+" or "-". The ~ operator means string concatenation (this is unlike C++, where you concatenate std::string objects using +), and the code

mixin("Vec(x " ~ op ~ " other.x, " ~
      "y " ~ op ~ " other.y, " ~
      "z " ~ op ~ " other.z)");

tells the compiler to do the following steps:

  1. Calculate the concatenation of the strings; if op == "+", the result is

    "Vec(x + other.x, y + other.y, z + other.z)"
  2. Put the string as if it were a true line of code at the spot where the mixin was used; again, if op == "+" then the result is

    Vec opBinary(string op)(const ref Vec other) const
         if (op == "+" || op == "-") { // We assume now that op == "+"
           return Vec(x + other.x, y + other.y, z + other.z);
         }

    but of course this is done in the same way if op == "-".

This combination of opBinary and mixins is extremely useful for the purpose of this course, as we will need to define a number of unary/binary operations on data types. Doing this in C++ is quite boring, but in D the task is much simplified.

The definition of Sphere, Ray, and intersect should be trivial to follow:

 Ray {
  Vec origin;
  Vec dir;
  double tmin;
  double tmax;

  // Constructors in D are named as "this"; in C++ this should
  // have been
  //
  //     Ray(Vec or, Vec d…)
  //
  this(Vec or, Vec d, double tmin = DEFAULT_TMIN, double tmax = DEFAULT_TMAX) {
    this.origin = or;
    this.dir = d;
    this.tmin = tmin;
    this.tmax = tmax;
  }
}

 Sphere {
  Vec center;
  double radius;
}

bool intersect(ref Sphere sphere, ref Ray ray) {
  const Vec op = sphere.center - ray.origin;
  const double dop = ray.dir * op;
  const double D = dop * dop - op * op + sphere.radius * sphere.radius;

  if (0 > D) {
    return false;
  }

  const double sqrtD = sqrt(D);

  const double tmin = dop - sqrtD;
  if (ray.tmin < tmin && tmin < ray.tmax) {
    return true;
  }

  const double tmax = dop + sqrtD;
  if (ray.tmin < tmax && tmax < ray.tmin) {
    return true;
  }

  return false;
}

Here are a few advantages of D:

There are a few disadvantages too:

A few good resources to learn D:

D could be the perfect language for this course, because it’s significantly better than C++ and easier to use. However, the scattered and sometimes contradictory documentation can be a barrier for students willing to use it.

Free Pascal

Pascal is one of the most venerable languages listed in this document. It was developed by prof. Niklaus Wirth in 1970, but it raised to widespread use only when in the 1980s Borland International produced and commercialized the Turbo Pascal7 compiler. Wirth developed the Pascal language as a tool to teach programming to students, which means that the language is meant to be easy to learn and teaches good programming practices. It was used to develop Apple Mac OS8 at least until version 6 (after that, Pascal fell out of fashion and Apple slowly rewrote parts of it in C).

Today one of the most used Pascal compilers is FreePascal (freepascal.org), which implements several new features in addition to the basic Pascal language proposed by Wirth.

Unlike C++, Pascal uses verbose keywords in place of symbols: for instance, instead of writing { and }, Pascal requires begin and end, and instead of && and || it uses and and or.

To define new data types, you must start with the type keyword; a C++ struct corresponds to a record in Pascal. Here is the implementation of the Vec class:

uses strutils;   (* This imports the "Format" function *)

type
   Vec = record
      x, y, z: Real;
   end;

function FormatVec(const v : Vec) : String;
begin
   result := Format('<%.2f, %.2f, %.2f>', [v.x, v.y, v.z]);
end;

operator * (t : Real; const v : Vec) : Vec; inline;
begin
   result.x := t * v.x;
   result.y := t * v.y;
   result.z := t * v.z;
end;

operator * (const a, b : Vec) : Real; inline;
begin
   result := a.x * b.x + a.y * b.y + a.z * b.z;
end;

operator * (a : Real; const v : Vec) : Real; inline;
begin
   result.x := a * v.x;
   result.y := a * v.y;
   result.z := a * v.z;
end;

Unlike C++, Pascal uses the := operator to mark an assignment; because of its didactic nature, Wirth wanted a language that marked the difference in roles between the left and right sides of expressions like a := b (where a gets modified but not b).

Other notable differences with respect to C++ are the following:

A peculiarity of Pascal is that the language is case-insensitive: you can either write BEGIN, begin, Begin, and the compiler will accept any of these. (This can be a feature to prevent sloppy students from defining variables named x and X in the same function, as the compiler will complain that the same variable is defined twice!)

The ability to define custom operators has been added by the FreePascal compiler; the original Pascal language did not have this feature.

The code to intersect rays and spheres is defined similarly:

type
   Ray = record
      m_o : Vec;
      m_d : Vec;
      m_tmin : Real;
      m_tmax : Real;
   end;

   Sphere = record
      m_p : Vec;
      m_r : Real;
   end;

function intersect(sphere: Sphere; ray: Ray) : Boolean;
var
   op : Vec;
   dop : Real;
   D : Real;
   sqrtD : Real;
   tmin, tmax : Real;

begin
   op := sphere.m_p - ray.m_o;
   dop := ray.m_d * op;
   D := dop * dop - op * op + sphere.m_r * sphere.m_r;

   if 0 > D then
      Exit(False);

   sqrtD := sqrt(D);
   tmin := dop - sqrtD;
   if (ray.m_tmin < tmin) and (tmin < ray.m_tmax) then
      Exit(True);

   tmax := dop + sqrtD;
   result := (ray.m_tmin < tmax) and (tmax < ray.m_tmax);
end;

Unlike C++, Pascal requires to list the variables used in a function in a var section before the begin, as shown in the function intersect. The return value of a function can be either assigned to the implicit variable result or returned through the Exit procedure.

Here are a few advantages of FreePascal over C++:

There are a few disadvantages as well:

If you are interested in developing a graphical interface for the program we will develop during this course, you should pick FreePascal without hesitation, as no other language in this document enables the same easiness in building a GUI.

A few references to learn FreePascal and Lazarus:

FreeBasic

Together with Pascal, the BASIC language is one of the oldest presented here, as its first implementation appeared in 1964. The name BASIC stands for «Beginners’ All-purpose Symbolic Instruction Code», which stresses the fact that it is a language conceived for «beginners». In the 1980s, it was a very common way to interact with personal computers; the personal computers built by Commodore (Vic-20, C64, C128, etc.) and the Sinclair ZX Spectrum greeted the user with a prompt where BASIC commands could be issued directly:

In the 1980s and 90s, a widely used BASIC compiler was Microsoft BASIC PDS, together with Microsoft QuickBASIC (the latter was a simplified edition of BASIC PDS). Microsoft introduced a number of enhancements to the original BASIC language, and it included BASIC compilers in its own operating system, MS-DOS (an ancestor of Windows). The most advanced of these bundled BASIC compilers, QBasic9, was a stripped clone of Microsoft’s own QuickBasic, and the fact that it was available on every computer running MS-DOS contributed to the popularity of the language.

Today there are several BASIC compilers; most of them implement a modernized version of Microsoft’s BASIC dialect, but unfortunately there are several slight differences in the syntax and grammar. (This is in contrast with C++ compilers like g++ and clang, which understand pretty much the same language.). The most notable are the following:

If you plan to use BASIC for this course, you are suggested to use FreeBasic, and in any case you must avoid commercial compilers, as the teacher must have a way to recompile your code to test it.

Here is an example showing how to implement a Vec data structure similar to the one implemented in C++; the code assumes you are using FreeBASIC:

type Vec
    as double x, y, z

    ' This converts a Vec into a printable string
    declare operator cast() as string
end type

operator Vec.cast() as string
    return "<" + str(x) + ", " + str(y) + ", " + str(z) + ">"
end operator

' Dot product
operator*(byval v1 as Vec, byval v2 as Vec) as double
    return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z
end operator

' Scalar-vector product
operator*(t as double, byval v as Vec) as Vec
    return type<Vec>(t * v.x, t * v.y, t * v.z)
end operator

As this example shows, the syntax of the BASIC language is very lightweight:

Here is the implementation of the ray and sphere types, as well as the code to check the intersection between a ray and a sphere:

type ray
    as Vec m_o
    as Vec m_d
    as double m_tmin
    as double m_tmax
end type

type sphere
    as Vec m_p
    as double m_r
end type

function intersect(byval s as sphere, byval r as ray) as boolean
    dim op as Vec = s.m_p - r.m_o
    dim dop as double = r.m_d * op
    dim d as double = dop * dop - op * op + s.m_r * s.m_r

    if 0.0 > d then return false

    dim sqrtd as double = sqr(d)
    dim tmin as double = dop - sqrtd
    if (r.m_tmin < tmin) and (tmin < r.m_tmax) then return true

    dim tmax as double = dop + sqrtd
    return (r.m_tmin < tmax) and (tmax < r.m_tmax)
end function

In FreeBASIC, variables are declared using the keyword dim (which originally stood for dimension, because in the old Microsoft BASIC it was used primarily to initialize arrays). Like C++ but unlike Pascal, variables can be defined everywhere in the code.

FreeBASIC has several advantages over C++:

FreeBASIC has a few disadvantages too:

Julia

Julia is a modern language for scientific computing. It is heavily inspired by the Scheme language, a LISP dialect that introduced several interesting programming paradigms like hygienic macros and functional patterns.

Julia’s main purpose is to provide a highly performant language for scientific applications, and as a consequence has stellar support for mathematical operations. Although the language is very easy to learn, it is not straightforward to develop fast code, as the execution model is significantly different from any other language listed in this document.

To see how Julia helps in writing scientific code, here is the implementation of the vector data structure:

using LinearAlgebra
using StaticArrays

const Vec = SVector{3,Float64}

There is no need to define any operator for Vec class, as Julia has native support for vector operations: addition, dot product and scalar-product multiplication are already available.

The following code implements the Ray and Sphere data structures, as well as the intersect method:

struct Ray
    o::Vec
    d::Vec
    tmin::Float64
    tmax::Float64

    # We provide three constructors using overloading
    Ray(o, d, tmin, tmax) = new(o, d, tmin, tmax)
    Ray(o, d, tmin) = new(o, d, tmin, DEFAULT_TMAX)
    Ray(o, d) = new(o, d, DEFAULT_TMIN, DEFAULT_TMAX)
end

struct Sphere
    r::Float64
    p::Vec
end

function intersect(s::Sphere, ray::Ray)
    op = s.p - ray.o
    dop = ray.d • op
    d = dop^2 - op • op + s.r^2

    0.0 > d && return false

    sqrtD = √d

    tmin = dop - sqrtD

    # The «Julia way» to write "if … then return true"
    (ray.tmin < tmin < ray.tmax) && return true

    tmax = dop + sqrtD
    ray.tmin < tmax < ray.tmax
end

Even if you do not understand the mathematics used in the intersect method, the sequence of mathematical operations should be clear. The dot operator is provided by the LinearAlgebra package, which was loaded before defining the Vec class. A few other niceties of the language are the following:

The most important advantages of Julia are the following:

However, Julia has a few disadvantages:

Apart from the official documentation, a great book to learn Julia is Hands-On Design Patterns and Best Practices with Julia, by Tom Kwong. It describes with many practical examples how to properly use Julia, and what are the best strategies to optimize code for maximum performance. Manning is going to publish a new book about Julia, Julia as a second language by Erik Engheim, which is another great introduction to the language; although it is not completed yet, you can already buy a draft copy for a discounted price and receive the full copy once it will be finished. (Disclaimer: I’m the technical reviewer of this book.)

There is also some good material on YouTube: A Gentle Introduction to Julia (syntax) and Getting Started with Julia (for Experienced Programmers) (development environment) are two examples.

In the past, the satisfaction of students who picked Julia was quite high, but some of them lamented that it required substantial effort to «un-learn» how to do some tasks in C++ and to understand the different approach required by Julia.

Kotlin

Kotlin is a relatively new language, as its first version was released in 2011. It is a managed language like C#, but it is based on the Java Virtual Machine; so, many of the things discussed for C# apply to Kotlin as well.

Kotlin is being used more and more since Google added full support for Kotlin10 in the Android operating system (previously only Java was fully supported, with partial support for C, C++, and Go). If you are interested in mobile programming, it might be worth picking this language.

Kotlin was developed by JetBrains, a company that offers several advanced IDEs like CLion (C++ and Rust), PyCharm (Python), IntelliJ IDEA (Java/Kotlin), etc. For this reason, JetBrains provides an IDE with stellar support for Kotlin through IntelliJ IDEA. If you pick Kotlin, you should install IDEA and do not look for anything else.

Here is a possible implementation for the Vec class, which implements a 3D vector:

data class Vec(val x: Double, val y: Double, val z: Double) {
    override fun toString(): String {
        return "<$x, $y, $z>"
    }

    operator fun times(other: Vec): Double {
        return x * other.x + y * other.y + z * other.z
    }

    operator fun times(t: Double): Vec {
        return Vec(t * x, t * y, t * z)
    }
}

Kotlin’s syntax is clearly inspired by C++, but it avoids using ;. However, unlike C++, the variable members are listed before the curly braces: similarly to Pascal but unlike C++, types are specified after parameter names.

Here is the implementation of the Ray and Sphere classes:

import kotlin.math.sqrt

data class Ray(val m_o: Vec,
               val m_d: Vec,
               val m_tmin: Double = 1e-10,
               val m_tmax: Double = 1e+10)

data class Sphere(val m_r: Double, val m_p: Vec) {
    fun intersect(ray: Ray) : Boolean {
        val op = m_p - ray.m_o
        val dop = ray.m_d * op
        val D = dop * dop - op * op + m_r * m_r

        if (0 > D) {
            return false
        }

        val sqrtD = sqrt(D)
        val tmin = dop - sqrtD
        if (ray.m_tmin < tmin && tmin < ray.m_tmax) {
            return true
        }

        val tmax = dop + sqrtD
        return (ray.m_tmin < tmax && tmax < ray.m_tmax)
    }
}

A few advantages of Kotlin are the following:

Disadvantages of Kotlin:

A few good resources to learn Kotlin:

Rust

The last language described in this document is Rust (www.rust-lang.org), which is probably the most complex language you can pick for this course.

The first stable release of the language was published in 2010, although Rust was already being used in several professional projects. The development team was led by Graydon Hoare (Mozilla Foundation).

Rust is a language that looks similar to C++ (curly braces {} everywhere!), yet it introduces a few new fundamental concepts:

The concept of «data ownership» is implemented in C++ as well through the classes std::unique_ptr and std::shared_ptr, but Rust implements it in a pervasive way, as it is deeply buried in the fundamentals of the language, while in C++ it is a matter of two classes in the standard library.

Grasping data ownership, borrowing rules and lifetimes can be daunting; in fact, they are probably among the biggest difficulties in learning the language. There are several good resources online that teach these concepts, but unfortunately you can’t postpone learning them, as all of them are fundamental to write any non-trivial codes. On the other side, the usage of Rust is more and more widespread, and it seems that several people in the field of physics are interested in learning it. Moreover, it is a formidable pedagogical tool to learn how values are represented in memory: learning Rust makes you a better programmer, no matter if you later abandon it and move to other languages!

Because of its difficulty, I strongly suggest that only people that have already written some non-trivial code in Rust pick it for this course. It would be optimal if at least three students pick Rust, so that if one of them gets frustrated and picks some other language, the other two can keep using it.

The code we implemented so far is not a good example of the difficulty of the language, as the Vec3 type is a value type (allocated on the stack and not on the heap). The code that implements the data type Vec3 thus looks deceivingly trivial:

use std::ops;

// The ability to print Vec3 variables is automatically enabled
// by "Debug"
#[derive(Copy, Clone, Debug)]
struct Vec3 {
    x: f64,
    y: f64,
    z: f64,
}

// Dot product
impl ops::Mul<Vec3> for Vec3 {
    type Output = f64;

    fn mul(self, other: Vec3) -> f64 {
        self.x * other.x + self.y * other.y + self.z * other.z
    }
}

// Scalar-vector product
impl ops::Mul<f64> for Vec3 {
    type Output = Vec3;

    fn mul(self: Vec3, other: f64) -> Vec3 {
        Vec3 {
            x: other * self.x,
            y: other * self.y,
            z: other * self.z,
        }
    }
}

Here is the implementation of the Ray and Sphere data types, as well as the ray/sphere intersection code. Again, the complexity of the Rust memory model does not appear here because we are still using value types:


#[derive(Copy, Clone, Debug)]
struct Ray {
    m_o: Vec3,
    m_d: Vec3,
    m_tmin: f64,
    m_tmax: f64,
}

#[derive(Copy, Clone, Debug)]
struct Sphere {
    m_r: f64,
    m_p: Vec3,
}

fn intersect(sphere: Sphere, ray: Ray) -> bool {
    let op = sphere.m_p - ray.m_o;
    let dop = ray.m_d * op;
    let d = dop * dop - op * op + sphere.m_r * sphere.m_r;

    if 0.0 > d {
        return false;
    }

    let sqrtd = d.sqrt();
    let tmin = dop - sqrtd;
    if (ray.m_tmin < tmin) && (tmin < ray.m_tmax) {
        return true;
    }

    let tmax = dop + sqrtd;
    (ray.m_tmin < tmax) && (tmax < ray.m_tmax);
}

The code above does not show the mechanism of borrowing, nor data ownership or lifetime, mainly because of the Copy and Clone specifications, which elegantly avoid the issue in this particular problem. However, this is not going to be true for many other data structures that will be used in the course, so this example is a oversimplification.

Here is a list of advantages for Rust:

A few disadvantages of Rust are the following:

A few good resources to learn Rust:


  1. A good choice is fmt, which was included in the C++20 standard.↩︎

  2. For example, if one changes an header file but fails to list the proper dependencies in the Makefile, some .cpp files depending on that header might fail to be recompiled in a .o file, leading to potential disasters.↩︎

  3. Nowadays, there are plenty of solutions to convert other languages to Javascript or WebAssembly: C++, Rust, Kotlin, etc.↩︎

  4. Yes, you can use ROOT in Nim if you want! You just need to force the compiler to output C++ code instead of C and use the {.importcpp.} pragma to create your own bindings.↩︎

  5. Cairo is a C library to create graphics and save it in PNG, PDF, or SVG files. Nim bindings can be installed through the command nimble install cairo.↩︎

  6. For example, if an else branch of an if statement is executed very often, the CLR can rewrite the if statement to swap the code below if with the code below else, as this might make the code faster.↩︎

  7. It is interesting to note that one of the lead developers of the Turbo Pascal compiler was Anders Hejlsberg, the creator of C#.↩︎

  8. If you are curious, you can test how Mac OS looked like in your browser by going to https://macos9.app/.↩︎

  9. If you are curious, you can run a copy of QBasic within your web browser, thanks to the Internet Archive: archive.org/details/msdos_qbasic_megapack.↩︎

  10. In Italy, one of the most famous apps available on the Google Store is the Immuni app. It has been written in Kotlin, and the source code is available online: github.com/immuni-app/immuni-app-android↩︎

  11. The term null safety refers to the ability of a language or a program to properly handle missing values.↩︎