Lab 1

Get the code

Find the Lab 1 code in the class repository.

$ ls
README.md  classes/   labs/      scripts/
$ ls labs/lab1
README    cpp/      julia/    python/   rust/
$ ls labs/lab1/cpp
binomial.cpp  makefile      maxint.cpp

You will want to copy the files to a directory outside the class repository, where you intend to store and edit your own class work. I suggest that you set up a Bitbucket repository of your own named phys540-spring2023-myWebID and mimic the labs/lab#/language/ directory structure. Later you can invite me (with write privileges) to your repository as a means of submitting work for grading.

A typical work session might look something like this.

$ git pull
Already up to date.
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
$ pwd
/Users/$USER/Bitbucket/phys540-spring2023-myWebID
$ cd labs/lab1/cpp
$ $EDITOR hello.cpp
$ $EDITOR binomial.cpp
$ $EDITOR maxint.cpp
$ make
g++ -o hello hello.cpp -O -ansi -pedantic -Wall
g++ -o maxint maxint.cpp -O -ansi -pedantic -Wall
g++ -o binomial binomial.cpp -O -ansi -pedantic -Wall
$ cd ../../..
$ echo "labs/lab1/cpp/hello" >> .gitignore
$ echo "labs/lab1/cpp/binomial" >> .gitignore
$ echo "labs/lab1/cpp/maxint" >> .gitignore
$ $EDITOR README
$ git add .
$ git commit -m "Post my work from Lab 1"
$ git push

Here, the user makes changes to various *.cpp files, compiles them via the make command, instructs git not to put the executables under version control (by adding them as entries to the .gitignore file), commits the files, and pushes all the changes to the remote repository.

The workflow is simplified for Python and Julia. With those languages, you don’t need a makefile, and there are no executables to add to the .gitignore.

Hello World!

Start by creating a file hello.cpp that contains a “Hello World!” program. See the C++ code listing below. Which text editor you use is a matter of taste. The editors vim, nano, and emacs -nw are good options that operate within the terminal window. Although I like vim (and its modern rewrite nvim) for some tasks, most students will likely prefer a modern editor with a windowing interface, such as Atom, Brackets, Sublime Text, TextMate; a full-fledged IDE, such as XCode or Visual Studio Code; or a local or cloud-based notebook in the style of IPhython or Jupyter.

lab1/cpp/hello.cpp
#include <iostream>
using std::cout;
using std::endl;
int main()
{
   char a = 'k';
   cout << "He" << ++a << "lo World!" << endl;
   return 0;
}

Compile this C++ program with g++ (or clang++) and run the resulting executable. You should be able to reproduce the following terminal session:

$ g++ -o hello hello.cpp
$ ls -F
hello*    hello.cpp
$ ./hello
Hello World!

Follow up by building the comparable program in each of Julia, Python, and Rust:

lab1/julia/hello.jl
a = 'k'
a = a+1
println("He",a,"lo World!")
lab1/python/hello.py
a = 'k'
a = chr(ord(a)+1)
print("He"+a+"lo World!")
lab1/rust/hello.rs
fn main() {
    let mut a = 'k';
    a = ((a as u8) + 1) as char;
    println!("He{}lo World!",a);
}

All four programs produce identical output.

$ cd ../julia
$ julia hello.jl
Hello World!
$ cd ../python
$ python3 hello.py
Hello World!
$ cd ../rust
$ rustc hello.rs
$ ./hello
Hello World!

Look carefully at the program listings and take note of how the languages differ in their treatment of the character type and in their approach to formatting strings/streams for display on the screen. C++ and Julia both treat the character as an integer type and quietly cast between ordinals and characters as needed. Python and Rust both require some extra work to effect the conversion. For Rust, in particular, this is a matter of language philosophy. It is a very strongly typed language, and all type conversions must be made explicit.

Each of these languages has its own look and feel, but there are family resemblances. C++ and Rust are traditional C-style “brace” languages, with all code blocks enclosed in matching braces and each command terminated by a semicolon. Layout is free-form, and extra white space is ignored. (More precisely, any number of spaces, tabs, carriage returns in sequence is equivalent to a single space.) The instructions to be executed are enclosed in a function named main.

Julia and Python each have the cleaner, more minimalistic feel of a scripting language. Semicolons are not needed as a separator. These languages assume one command per line. And braces aren’t used to organize the code. Instead, Python groups any commands that sit at a common level of indentation; Julia uses keywordend blocks, in the style of ALGOL and Pascal.

Overflow

Compile and run the program maxint. It generates the first six powers of two, starting from the zeroth.

$ make maxint
g++ -o maxint maxint.cpp -O -ansi -pedantic -Wall
$ ./maxint
2^0 = 1
2^1 = 2
2^2 = 4
2^3 = 8
2^4 = 16
2^5 = 32

Extend the program to determine the largest int that can be represented on your machine. Are there differences in behavior between the C++, Julia, Python, and Rust implementations? Does the computation fail silently or not? Note your observations (and any other details you want to communicate to me) in a plain-text README or markdown README.md file.

The key determinant of the integer overflow behaviour is whether integer arithmetic is carried out in software (with some arbitrary-precision integer scheme) or in machine hardware. If it’s the latter, then we need to be aware of the width of the underlying integer type used in the computation.

For example, without an explicit type specificication, integer variables in Julia default to the Int64 type. \(\mathsf{2^{50}}\) is representable, so this code snippet produces no overflow:

let a = 1
   println("2^0 = ", a);
   for n in 1:50
      a = a*2
      println("2^$n = $a")
   end
end

The BigInt type cannot overflow, since it’s capable of handling integers of arbitrarily large size.

let a::BigInt = 1
   println("2^0 = ", a);
      for n in 1:50
      a = a*2
      println("2^$n = $a")
   end
end

Here, however, the unsigned 32-bit integer type isn’t wide enough to accommodate all the doubling of a in the loop.

let a::UInt32 = 1
   println("2^0 = ", a);
   for n in 1:50
      a = a*2
      println("2^$n = $a")
   end
end

For the remaining questions in Lab 1, you do not need to code in each of C++, Julia, Python, and Rust. Provide solutions in one language of your choice.

Be choosy

Included in the lab1 directory is a program that computes the (”\(\mathsf{m}\) choose \(\mathsf{k}\)”) binomial coefficient

\[\mathsf{{m \choose k} = \frac{n!}{k!(n-k)!}}.\]

The naive implementation

unsigned long int binomial(unsigned long int n, unsigned long int k)
{
   return factorial(n)/factorial(k)/factorial(n-k);
}

seems to work fine for the combinatorics of a small number of items

$ make binomial
$ g++ -o binomial binomial.cpp -O -ansi -pedantic -Wall
$ ./binomial 8
(8 choose 0) = 1
(8 choose 1) = 8
(8 choose 2) = 28
(8 choose 3) = 56
(8 choose 4) = 70
(8 choose 5) = 56
(8 choose 6) = 28
(8 choose 7) = 8
(8 choose 8) = 1

but it fails for larger numbers

$ ./binomial 24 | head -n 10
(24 choose 0) = 1
(24 choose 1) = 1
(24 choose 2) = 0
(24 choose 3) = 0
(24 choose 4) = 0
(24 choose 5) = 0
(24 choose 6) = 2
(24 choose 7) = 5
(24 choose 8) = 12
(24 choose 9) = 22

Convince yourself that this calculational error is caused by integer overflow. Note that Python circumvents this by handling arbitrarily large numbers in software (rather than using fixed-bit-width integers in hardware, trading speed for flexibility), so the naive implementation will work.

Write a smart implementation of the binomial coefficient in any or all of C++, Julia, Python, and Rust based on the identity

\[\mathsf{{m \choose k}} = \mathsf{\frac{1\cdot 2\cdots n} {(1\cdot 2 \cdots p)(1\cdot 2 \cdots q)}} = \mathsf{\frac{(p+1)(p+2)\cdots n}{1\cdot 2\cdots q}},\]

where \(\mathsf{p} = \mathsf{\textsf{max}(k,n-k)}\) and \(\mathsf{q} = \mathsf{\textsf{min}(k,n-k)}\). You should be able to reproduce the following terminal session.

$ ./binomial 24 | head -n 10
(24 choose 0) = 1
(24 choose 1) = 24
(24 choose 2) = 276
(24 choose 3) = 2024
(24 choose 4) = 10626
(24 choose 5) = 42504
(24 choose 6) = 134596
(24 choose 7) = 346104
(24 choose 8) = 735471
(24 choose 9) = 1307504

Think about why this works to avoid overflow. Be sure that you can offer a careful and convincing explanation.