Lab 1
=====
Get the code
------------
Find the Lab 1 code in the class repository.
.. code-block:: console
$ 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.
.. code-block:: console
$ 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 `_.
.. code-block:: c++
:caption: lab1/cpp/hello.cpp
#include
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:
.. code-block:: console
$ 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:
.. code-block:: julia
:caption: lab1/julia/hello.jl
a = 'k'
a = a+1
println("He",a,"lo World!")
.. code-block:: python
:caption: lab1/python/hello.py
a = 'k'
a = chr(ord(a)+1)
print("He"+a+"lo World!")
.. code-block:: rust
:caption: 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.
.. code-block:: console
$ 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.
.. code-block:: console
$ 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. :math:`\mathsf{2^{50}}` is representable, so this code snippet produces no overflow:
.. code-block:: julia
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.
.. code-block:: julia
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.
.. code-block:: julia
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
(":math:`\mathsf{m}` choose :math:`\mathsf{k}`") binomial coefficient
.. math::
\mathsf{{m \choose k} = \frac{n!}{k!(n-k)!}}.
The naive implementation
.. code-block:: c++
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
.. code-block:: console
$ 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
.. code-block:: console
$ ./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
.. math::
\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 :math:`\mathsf{p} = \mathsf{\textsf{max}(k,n-k)}` and :math:`\mathsf{q} = \mathsf{\textsf{min}(k,n-k)}`. You should be
able to reproduce the following terminal session.
.. code-block:: console
$ ./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.