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.
#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:
a = 'k'
a = a+1
println("He",a,"lo World!")
a = 'k'
a = chr(ord(a)+1)
print("He"+a+"lo World!")
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 keyword … end 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
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
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.