Skip to content

Commit

Permalink
WIP draft of numbers concept
Browse files Browse the repository at this point in the history
  • Loading branch information
colinleach committed Sep 22, 2024
1 parent a467cbb commit 5a9092c
Show file tree
Hide file tree
Showing 4 changed files with 396 additions and 0 deletions.
7 changes: 7 additions & 0 deletions concepts.wip/numbers/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"authors": [
"colinleach"
],
"contributors": [],
"blurb": "Number-crunching is at the heart of Julia, so the language aims to make this fast, flexible and sophisticated."
}
382 changes: 382 additions & 0 deletions concepts.wip/numbers/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,382 @@
# About

Julia is a general-purpose language, that can be used for most programming tasks.
In practice, however, the main use cases tend to be in engineering and science.
Fast, versatile, sophisticated numerical calculations are central to the design.

## Integers

An [integer][integer] is a "round" number with no decimal point.

In the [Basics][basics] concept, we saw that an integer value can be assigned to a variable without specifying a type.

For readability, underscores can be used as a digit separator.
They are ignored by the compiler.

```julia-repl
julia> x = 3
3
julia> typeof(x)
Int64
julia> large_number = 1_234_567_890
1234567890
```

Internally, the compiler will use whatever signed integer type is most appropriate for your CPU.
On modern PCs this will usually be `Int64`, which is perfectly adequate for most tasks.

[Types][types] will be discussed more fully in a future concept.
For now, the _numeric_ types should hopefully be intuitive enough for this document to make sense.

As we will see in a later concept, Julia natively supports very large, multidimensional arrays.
These can challenge both the amount of available memory, and the bandwidth for data transfers.

To give finer control to the programmer, it is both possible and quite common to specify the desired signed or unsigned integer type.
The many Julia learners who have previously used Python may want to compare this to [NumPy][numpy], rather than base Python.

```julia-repl
julia> y = Int8(42)
42
julia> typeof(y)
Int8
julia> z = UInt32(1024)
0x00000400
```

Note that Julia defaults to displaying unsigned integers in [hexadecimal][hex] format, because the unsigned types are often used for low-level bit operations.

Integers can also be entered as binary, octal or hexadecimal, with prefixes `0b`, `0o` and `0x` respectively.

```julia-repl
julia> a = 0x10
0x10
julia> Int(a)
16
julia> b = 0b1010 # displays as hexadecimal
0x0a
```

## Integer overflows and `BigInt`

Each integer type has a maximum and minimum value that it can store:

```julia-repl
julia> typemax(Int8)
127
julia> typemin(Int8)
-128
```

Going outside this valid range will cause [*integer overflow*][integer-overflow], with results that appear very strange.

```julia-repl
julia> c = Int8(126)
126
julia> c * c
4
```

Probably we can agree that `126 * 126` should _not_ equal 4!

A detailed explanation needs a knowledge of how CPU registers store integers, which is outside the scope of this concept.

The problem can be minimized by using a "wider" type, up to Int128 or UInt128 (the meaning of "wider" is discussed below).
However, all have hard limits before running into overflow problems.

One solution is to use the [`BigInt`][bigint] type, which is limited only by the memory in your computer.
It is Julia's implementation of [arbitrary-precision arithmetic][arbitrary-precision].

```julia-repl
julia> 2 ^ 70
0
julia> big = BigInt(2) ^ 70
1180591620717411303424
julia> UInt128(big)
0x00000000000000400000000000000000
```

Because 2 to the power 70 will not fit into an `Int64`, the first calculation in the above example fails: although note that _there is no error message_.
Programmer beware!

[Casting][casting] 2 to a `BigInt` gives the correct answer, and displaying it in hexadecimal makes clearer that this is a power of 2.

## Floating-point

It will be no surprise that [floating-point][float] numbers optionally have a decimal point, and a fractional part after the point.

```julia-repl
julia> f = 3.45
3.45
julia> typeof(f)
Float64
```

Of course, [scientific notation][scientific-notation] is supported.

```julia-repl
julia> avogadro = 6.02e23
6.02e23
```

As with integers, the default type is fine for most purposes, but other signed types are available.
There are no [_unsigned_ floating-point][unsigned-fp] types.

As a shortcut, `Float32` values can be created by using a `f0` suffix.

```julia-repl
julia> f32 = 4.56f0
4.56f0
julia> typeof(f32)
Float32
```

Other sizes of float need an explicit [cast][casting], as with integers.

The maximum and minimum values may come as a surprise:

```julia-repl
julia> typemax(Float64)
Inf
julia> typemin(Float64)
-Inf
```

[Infinity][infinity] is a valid value!

However, the useful range of floating-point numbers is limited, with anything very large just assigned an `Inf` value, and anything very small rounded to `0.0`.

We can use a different pair of functions to see these limits: roughly `± 10 ^ 308`.

```julia-repl
julia> floatmax(Float64)
1.7976931348623157e308
julia> floatmin(Float64)
2.2250738585072014e-308
```

Also, the precision is limited, with a large but finite number of [significant digits][sig-fig] represented (approximately 15 for `Float64`).

## Arithmetic operators

As discussed in the Basics concept, arithmetic operators mostly work the same as standard arithmetic, as taught to children.
Note that [exponentiation][exponentiation] uses `^`, _not_ `**` (both are common in other languages).

```julia
2 + 3 # 5 (addition)
2 - 3 # -1 (subtraction)
2 * 3 # 6 (mutlplication)
8 / 2 # 4.0 (division)
8 % 3 # 2 (remainder)
2 ^ 3 # 8 (exponentiation)
```

However, a few Julia-specific details are worth discussing.

### Multiplication

```julia-repl
julia> x = 4.2
4.2
julia> 2 * x
8.4
julia> 2x
8.4
julia> 2.4x
10.08
```

That may be surprising.

It is always possible to use `*` as an [infix][infix] operator, as in most other computer languages.

However, Julia is designed by people who believe that code should look as much as possible like mathematical equations.

Because variable names must start with a letter, prefacing the name with a number (integer or floating-point) is treated as implicit multiplication.

For example, if we want the surface area of a sphere, instead of `4 * pi * r * r` we could do this :

```julia-repl
julia> surface(r) = 4π * r^2
surface (generic function with 1 method)
julia> surface(3)
113.09733552923255
```

Although π is a built-in constant, it is also a (Greek) letter.
The parser therefore still needs one explicit `*` to separate `π` from `r`.

### Division

Using `/` as the [infix][infix] operator will always give a floating-point result, even for integer inputs.

For integer division, there are more options:

```julia-repl
julia> 10 / 3
3.3333333333333335
julia> div(10, 3)
3
julia> 10 ÷ 3
3
julia> 10 // 3
10//3
```

The `div()` function is for integer division, with the result truncated towards zero: downwards for positive numbers, upwards for negative numbers.

As a synonym, we can use the infix operator `÷`, again aiming to make it look more mathematical.
If you are using a Julia-aware editor, enter this as `\div` then hit the `<Tab>` key.

The `//` operator will need a concept of its own, later in the syllabus.

For now, we can just say that the result of `//` is a ["rational" number][rational], the formal name for what most people call a [fraction][fraction].

Common factors will be removed from the [numerator][fraction] and [denominator][fraction], to give a ratio of two integers in what is called the ["lowest terms"][lowest-terms].

```julia-repl
julia> rationalnum = 22 // 6
11//3
julia> typeof(rationalnum)
Rational{Int64}
```

We have rational numbers.
What about "irrational" numbers?

```julia-repl
julia> π
π = 3.1415926535897...
julia> typeof(π)
Irrational{:π}
```

An [irrational number][irrational] is one that cannot be reduced to a ratio of integers.
Common examples include `π`, `e` ([Euler's number][e]), and many [roots][root] such as `√2` (the square root of 2).

Julia tries to do mathematics _properly_.

## Conversion of numeric types

This can often happen automatically:

```julia-repl
julia> x = 2 + 3.5
5.5
julia> typeof(x)
Float64
```

We added an `Int64` to a `Float64`, and got a `Float64` result.

In fact, the integer was silently converted to a `Float64` before doing the addition.

Julia has a concept of the ["width"][width] of numeric types.

- Within integers, and within floats, this is just the number of bits needed for storage.
Thus, `Int64` is wider than `Int16`.
- Floats are considered wider than integers, because floats can store any fractional part.

If a mixture of types is used within an expression, each is "promoted" as necessary to the widest type used.

To force the conversion, we can cast an integer to a specific type, as in `Float64(5)`.

Alternatively, just use `float(5)` and let the compiler choose an appropriate type.

**Float-to-integer** conversions are inevitably more complicated.
What do you want to do with anything after the decimal point?

- The `round()` function converts to the nearest integer, with ties such as 4.5 rounding to the nearest _even_ integer.
- `floor()` rounds down, `ceil()` rounds up, `trunc()` rounds towards zero.
- Attempting to cast directly, for example with `Int32()`, will fail with an [`InexactError`][inexact].

See the [manual][round] for more details.

## Divide-by-zero

Surely this just throws an error?
In fact, the situation is not that simple.

Integer division with `÷` or `//` will result in an error, as you might expect.

Floating-point division with `/` takes what might be considered an engineering approach, rather than a standard computer science approach:

```julia-repl
julia> 2 / 0
Inf
julia> 0 / 0
NaN
```

As discussed in a previous section, [infinity][infinity] is a valid floating-point number in Julia, represented by `Inf`.

When the numerator is also zero, the result is mathematically undefined.
Julia then treats it as ["not a number"][NaN], represented by `NaN`.

If this seems strange, think of it in the context of working on large arrays of real-world (and thus often quite messy) data.
To make progress, it is best just to flag problematic values and move on.

Endless manual checking of values would be tedious to program, and would certainly hurt runtime performance.

Stopping with an error message at every slight glitch would make your program _very_ unpopular with users!

## Related future concepts

As well as rational numbers, later parts of the syllabus will discuss:

- [Complex numbers][complex], such as `2.3 + 4.5im`.
- [Bitwise][bitwise] operations, for low-level programming.


[basics]: https://exercism.org/tracks/julia/concepts/basics
[integer]: https://en.wikipedia.org/wiki/Integer
[float]: https://en.wikipedia.org/wiki/Floating-point_arithmetic
[types]: https://docs.julialang.org/en/v1/manual/types/
[rational]: https://en.wikipedia.org/wiki/Rational_number
[irrational]: https://en.wikipedia.org/wiki/Irrational_number
[integer-overflow]: https://en.wikipedia.org/wiki/Integer_overflow
[infinity]: https://en.wikipedia.org/wiki/Infinity
[NaN]: https://en.wikipedia.org/wiki/NaN
[numpy]: https://numpy.org/
[hex]: https://en.wikipedia.org/wiki/Hexadecimal
[arbitrary-precision]: https://en.wikipedia.org/wiki/Arbitrary-precision_arithmetic
[casting]: https://en.wikipedia.org/wiki/Type_conversion
[scientific-notation]: https://en.wikipedia.org/wiki/Scientific_notation
[unsigned-fp]: https://discourse.julialang.org/t/why-is-there-no-concept-of-unsigned-floats/14058
[sig-fig]: https://en.wikipedia.org/wiki/Significant_figures
[exponentiation]: https://en.wikipedia.org/wiki/Exponentiation
[infix]: https://en.wikipedia.org/wiki/Infix_notation
[fraction]: https://en.wikipedia.org/wiki/Fraction
[lowest-terms]: https://en.wikipedia.org/wiki/Fraction#Simplifying_(reducing)_fractions
[e]: https://en.wikipedia.org/wiki/E_(mathematical_constant)
[root]: https://en.wikipedia.org/wiki/Nth_root
[width]: https://en.wikipedia.org/wiki/Integer_(computer_science)#Value_and_representation
[round]: https://docs.julialang.org/en/v1/manual/mathematical-operations/#Rounding-functions
[inexact]: https://docs.julialang.org/en/v1/base/base/#Core.InexactError
[complex]: https://en.wikipedia.org/wiki/Complex_number
[bitwise]: https://en.wikipedia.org/wiki/Bitwise_operation
[bigint]: https://docs.julialang.org/en/v1/base/numbers/#BigFloats-and-BigInts
1 change: 1 addition & 0 deletions concepts.wip/numbers/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Introduction
6 changes: 6 additions & 0 deletions concepts.wip/numbers/links.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"url": "",
"description": ""
}
]

0 comments on commit 5a9092c

Please sign in to comment.