Appendix B — Code to generate diagrams
The diagrams in this book are all generated in Julia. The code is given below:
B.1 Different numeric types
B.1.1 Figure 3.1
# Useful for convenient broadcasting in the arrange function
import Base.+
+(x::Any,::Nothing) = x
# Arranges the subtype tree of T with positions of each of the subtypes
# Format of output is a vector of tuples of the form:
# (type, number of nodes below, depth, position from top)
function arrange(T::Type)
# Deals with small cases, where a type is not defined in Base we ignore it
isdefined(Base, Symbol(T)) || return Tuple{Type, Int64, Int64, Rational{Int64}}[]
isabstracttype(T) || return [(T, 1, 1, 1//1)]
subT = subtypes(T)
typelist = Tuple{Type, Int64, Int64, Rational{Int64}}[]
offset = 0
for S ∈ subT
# Prevents an infinite loop
S == Any && continue
# Recursively gets the arrangement of each subtype
Stypelist = arrange(S)
# Alters the arrangement to fit with the new graph
append!(typelist,
[st .+ (nothing, nothing, 1, offset) for st ∈ Stypelist]
)
# Recalculates the offset to accommodate for the new points added
isempty(Stypelist) || (offset += (Stypelist[end])[2] + 1)
end
# Adds the tuple for T itself to the end of the vector, and returns
push!(typelist, (T, offset, 1, offset//2))
end
using Plots
# Plots the type tree for the subtypes of T
function typetree(T::Type)
typelist = arrange(T)
types::Vector{Type} = [st[1] for st ∈ typelist]
spans::Vector{Int64} = [st[2] for st ∈ typelist]
depths::Vector{Int64} = [st[3] for st ∈ typelist]
vpositions::Vector{Rational{Int64}} = [st[4] for st ∈ typelist]
n = length(typelist)
xgap = 750 ÷ max(depths...)
ygap = 750 ÷ spans[end]
plot(
size = (750, 750),
legend = false,
ticks = false,
framestyle = :none,
xlims = (xgap - 50, 750 + 50),
ylims = (ygap - 20, 750 + 20)
)
# Abstract types in grey, concrete types in black
plot!(
annotations = [
(depths[i] * xgap, vpositions[i] * ygap,
(types[i], 10, isabstracttype(types[i]) ? :grey : :black), "sans-serif")
for i ∈ 1:n
]
)
lines = [
([(depths[i] + 1//2) * xgap, (depths[i] + 1//2) * xgap],
[(vpositions[i] - spans[i]//2) * ygap, (vpositions[i] + spans[i]//2) * ygap])
for i ∈ 1:n if isabstracttype(types[i])
]
# Lines delineate the set of subtypes for each abstract type
plot!(
[Shape(line...) for line ∈ lines],
fillcolor = :white,
linecolor = :lightgrey,
linewidth = 4
)
end
typetree(Number)B.1.2 The function sevensegment
The custom function sevensegment creates the calculator-like displays used when explaining UInt and Int types.
# Defines which segments light up for each number
# nothing represents an empty space, -1 represents a minus sign
digitionary = Dict(
nothing => ntuple(_ -> false, 7),
-1 => (false, false, false, true , false, false, false),
0 => (true , true , true , false, true , true , true ),
1 => (false, false, true , false, false, true , false),
2 => (true , false, true , true , true , false, true ),
3 => (true , false, true , true , false, true , true ),
4 => (false, true , true , true , false, true , false),
5 => (true , true , false, true , false, true , true ),
6 => (true , true , false, true , true , true , true ),
7 => (true , false, true , false, false, true , false),
8 => (true , true , true , true , true , true , true ),
9 => (true , true , true , true , false, true , true ),
)
# Defines the corners of the seven segments
shapedex = Dict(
1 => ([-25, -20, 20, 25, 20, -20], [60, 65, 65, 60, 55, 55]),
2 => ([-30, -35, -35, -30, -25, -25], [5, 10, 50, 55, 50, 10]),
3 => ([30, 35, 35, 30, 25, 25], [5, 10, 50, 55, 50, 10]),
4 => ([-25, -20, 20, 25, 20, -20], [0, 5, 5, 0, -5, -5]),
5 => ([-30, -35, -35, -30, -25, -25], [-5, -10, -50, -55, -50, -10]),
6 => ([30, 35, 35, 30, 25, 25], [-5, -10, -50, -55, -50, -10]),
7 => ([-25, -20, 20, 25, 20, -20], [-60, -55, -55, -60, -65, -65])
)
using Plots
# N is the number to be displayed
# n is the number of digits to display it in
# If negative numbers are allowed, the first digit will be taken up by a minus sign
function sevensegment(N, n, allownegative = false)
# The number that will actually be displayed
# Without negatives allowed, functions like UInt
# With negatives allowed, not exactly like Int, as can't display -10^(n-1)
N = allownegative ?
mod(N, -(10^(n-1)-1):(10^(n-1)-1)) :
mod(N, 10^n)
# Logic to work out the sequence of digits to be displayed
digitsN = digits(abs(N))
truncateddigitsN = reverse(digitsN[1:min(length(digitsN), n - allownegative)])
displaydigits = [
allownegative ? (N < 0 ? -1 : nothing) : Nothing[];
fill(nothing, n - allownegative - length(truncateddigitsN));
truncateddigitsN
]
# Creates canvas to work on
p = plot(
size = (75n + 20, 150),
legend = false,
ticks = false,
framestyle = :none,
xlims = (25, 75n + 35),
ylims = (-75, 75),
bg = :black
)
# Plots segments with colours according to digits
for i ∈ 1:n
for j ∈ 1:7
plot!(
p,
Shape(broadcast(.+, shapedex[j], (75i, 0))...),
fillcolor = (digitionary[displaydigits[i]][j] ? :red : :grey15),
linewidth = 0
)
end
end
return p
end;B.1.3 Figure 3.2
# Generate a vector of all possible Float16s from their bitstrings
float16s = [reinterpret(Float16, n) for n ∈ 0x0000:0xffff]
# Parameters for the diagram
totalwidth = 20
intervalwidth = 0.1
# List of endpoints of intervals
intervals = -totalwidth/2:intervalwidth:totalwidth/2
nbins = length(intervals) - 1
# Counts the number of Float16s found in each interval
frequencies = [count(x -> intervals[i] ≤ x < intervals[i+1], float16s) for i ∈ 1:nbins]
# Plot the diagram
plot(
intervals[1:nbins] .+ intervalwidth/2,
frequencies ./ intervalwidth,
legend = false,
showaxis = :x,
yticks = false
)B.1.4 Figure 3.3
f(x) = x^2
δfδx(x, δx) = (f(x + δx) - f(x))/δx # The approximate derivative δf/δx at x
plot(
Float16(0.0001):Float16(0.0001):Float16(0.1),
δx -> δfδx(1, δx), # Don't need to specify 1 as Float16 because of multiple dispatch
legend = false,
xlabel = "δx",
ylabel = "δf/δx at x = 1"
)B.2 Representing text
B.2.1 The function stringshow
The custom function stringshow creates the visualisations of Strings seen in Chapter 4.
using Plots
# Shows an String graphically
function stringshow(str::String)
n = ncodeunits(str)
sc = max(min(1, (15//2)//n), 15//32)
h = (n+15) ÷ 16
p = plot(
legend = false,
ticks = false,
framestyle = :none,
size = (min(750, 10+100n), (12+120h)*sc),
xlims = (4//5, min(n, 16) + 1),
ylims = (-h-13//12, -11//12)
)
# Each byte is depicted as a box with the corresponding number inside
for (i, c) ∈ enumerate(codeunits(str))
iᵥ, iₕ = divrem(i-1, 16) .+ (1, 1)
plot!(p,
Shape([0, 0, 4//5, 4//5] .+ iₕ, [0, -11//12, -11//12, 0] .- iᵥ),
fillcolor = RGB(0.9),
linecolor = RGB(0.6),
linewidth = 3,
annotations = (iₕ + 2//5, -iᵥ - 11/24, c)
)
end
p
end;B.3 Creating custom structures
B.3.1 Preliminary functions
The scope figures all use the following functions:
"""
WARNING
This code is deliberately scrappy and unoptimised.
Its only purpose is to generate the scope diagrams.
Known issues
- Code must be correct Julia code (in particular, no missing or unmatched `end`s)
- Only blocks `begin`, `if`, `elseif`, `else`, `while`, `for`, `function`, `struct` can be used
- Functions can't be defined short-form (e.g. `f(x) = x + 2`), they must use `function` to be recognised
- Variables given as inputs to functions are only detected if they are the first input
- Doesn't deal with branched code (e.g. `if`-statements) properly
- Ignores `local` and `global`
"""
using Plots
# Given a list of lines of code, finds what the indentation level of each should be,
# as well as a list of blocks in which said line is nested
function findstructure(code::Vector{String})
n = length(code)
indents = zeros(Int64, n)
blocks = fill(String[], n+1)
# The keywords that mark an indentation. Not comprehensive, e.g. do and let are not included
INDENTORS = ["begin", "if ", "while ", "for ", "function ", "struct "]
# Keeps track of the current indent level
rollingindent = 0
# Goes line by line, increasing/decreasing indent and updating blocks as it goes
for i ∈ 1:n
if code[i] == "end"
rollingindent -= 1
indents[i] = rollingindent
pop!(blocks[i])
elseif code[i] == "else"
indents[i] = rollingindent - 1
else
indents[i] = rollingindent
for indentor ∈ INDENTORS
if startswith(code[i], indentor)
rollingindent += 1
append!(blocks[i], [indentor])
end
end
end
blocks[i+1] = copy(blocks[i])
end
return indents, blocks[1:n]
end
# Returns a list of `Symbol`s detailing the scope of a given variable `var` line by line in the code `code`
# `var` is assumed to be defined on line `linedefined`
# `indents` and `blocks` are as from `findstructure`
#
# Returned `Symbol`s are:
# - :out means out of scope
# - :in means in scope
# - :def means that the variable is defined (or redefined) on this line
# - :paused means that the variable is currently out of scope due to local redefinition
function findscope(
var::Symbol,
linedefined::Int64,
code::Vector{String},
indents::Vector{Int64},
blocks::Vector{Vector{String}}
)
n = length(code)
scope = Vector{Symbol}(undef, n)
@assert 1 ≤ linedefined ≤ n
# The keywords that begin a new scope
BEGIN_SCOPERS = ["while ", "for ", "function ", "struct "]
# The keywords that begin a new scope in which the scope of `var` may be paused
PAUSE_SCOPERS = ["for " * string(var), "function ", "struct "]
# Set the scope before and at initial definition
scope[1:(linedefined-1)] .= :out
scope[linedefined] = :def
# Finds the indent of the scope in which the variable lies
scopeindent = findlast([b ∈ BEGIN_SCOPERS for b ∈ blocks[linedefined]])
isnothing(scopeindent) && (scopeindent = 0)
# Lists the indentation levels where new scopes have begun
pausableindents = Int64[]
pauseindent = 0
# Goes line by line, finding in what state of scope the variable lies at the end of each line
for i ∈ (linedefined+1):n
# If scope is currently paused, check if it has ended yet
if scope[i-1] == :paused
if indents[i] == pauseindent
scope[i] = :in
else
scope[i] = :paused
end
# If scope is not paused
else
# If we exit the block in which the variable is defined, it is out of scope forever
if indents[i] < scopeindent
scope[i:end] .= :out
break
# If we start a new scope, we need to track it by adding its indentation level to `pausableindents`
elseif any(startswith.(code[i], PAUSE_SCOPERS))
append!(pausableindents, indents[i])
# If our variable isn't defined in a function's arguments, it remains in scope
if startswith(code[i], "function ") &&
!startswith(code[i], Regex("function .+\\(" * string(var) * "[,)]"))
scope[i] = :in
# Otherwise, the scope is paused
else
scope[i] = :paused
pauseindent = pop!(pausableindents)
end
# If our variable is redefined
elseif startswith(code[i], string(var) * " =")
# If this happens in a new scope, then it is locally redefined
if !isempty(pausableindents)
scope[i] = :paused
pauseindent = pop!(pausableindents)
# Otherwise, the value is overwritten
else
scope[i] = :def
end
# Otherwise, we remain in scope
else
# If an `end` keyword ends a new scope, we need to delete it from `pausableindents`
if code[i] == "end" &&
!isempty(pausableindents) &&
indents[i] == pausableindents[end]
pop!(pausableindents)
end
scope[i] = :in
end
end
end
return scope
end
# Creates a diagram detailing the scope of variables in `vars` defined on lines `linesdefined` of code `code`
function scopediagram(
vars::Vector{Symbol},
linesdefined::Vector{Int64},
code::Vector{String}
)
n = length(code)
nvars = length(vars)
nvars == length(linesdefined) || error("mismatched number of variables and lines where they are defined")
nvars > 4 && error("can draw scope of at most 4 variables at a time")
p = plot(
size = (400, 18n),
legend = false,
ticks = false,
framestyle = :none,
xlims = (0, 40),
ylims = (-n, 0)
)
indents, blocks = findstructure(code)
# Writes the code with correct indentations
plot!(
annotations = [
(1 + nvars + 2indents[i], 0.5-i, (code[i], 8, :black, :left), "courier")
for i ∈ 1:n]
)
cs = [:lime, :skyblue, :orange, :pink]
# Draws the scope of each of the variables
for j ∈ 1:nvars
scope = findscope(vars[j], linesdefined[j], code, indents, blocks)
# Dots for (re)definitions
scatter!(
[(j-0.5, 0.5-i)
for i ∈ 1:n if scope[i] == :def],
markercolor = cs[j],
markerstrokecolor = cs[j]
)
# Filled line for in scope
plot!(
[Shape([j-0.6, j-0.4, j-0.4, j-0.6], [1-i, 1-i, -i, -i])
for i ∈ 1:n if scope[i] == :in],
fillcolor = cs[j],
linecolor = cs[j]
)
# Dotted line for paused scope
plot!(
[Shape([j-0.6, j-0.4, j-0.4, j-0.6], [1-i, 1-i, -i, -i])
for i ∈ 1:n if scope[i] == :paused],
fillcolor = cs[j],
linecolor = :white,
fillstyle = :x
)
# Key
scatter!(
(35, 0.5-j),
markercolor = cs[j],
markerstrokecolor = cs[j],
annotations = (36, 0.5-j, (string(vars[j]), 8, :black, :left), "courier")
)
end
return p
end;B.3.2 Figure 6.1
scopediagram(
[:x, :y], [1, 3],
[
"x = 1",
"begin",
"y = 2",
"x = y",
"end",
"",
"x",
"y"
]
)B.3.3 Figure 6.2
scopediagram(
[:x,:y], [1,6],
[
"x = 0",
"if x > 3",
"y = 1",
"x = 2",
"else",
"y = 2",
"x = 1",
"end",
"",
"x",
"y"
]
)B.3.4 Figure 6.3
scopediagram(
[:x], [1],
[
"x = 0",
"if x > 3",
"y = 1",
"x = 2",
"end",
"",
"x",
"y"
]
)B.3.5 Figure 6.4
scopediagram(
[:x, :y], [1, 2],
[
"x = 2",
"for y ∈ 1:3",
"y = x^2",
"x = y",
"end",
"",
"x",
"y"
]
)B.3.6 Figure 6.5
scopediagram(
[:x, :x, :y], [1, 2, 3],
[
"x = 2",
"for x ∈ 1:3",
"y = x^2",
"x = y",
"end",
"",
"x",
"y"
]
)B.3.7 Figure 6.6
scopediagram(
[:x, :x, :y, :z], [1, 4, 2, 3],
[
"x = 4",
"function f(y)",
"z = x",
"x = y - z",
"y = x^2",
"return y + z",
"end",
"",
"x",
"y",
"z"
]
)B.3.8 Figure 6.7
scopediagram(
[:x, :y], [1, 4],
[
"x = 0",
"struct A",
"y::Int64",
"A(y) = new(x + y)",
"end",
"",
"x",
"y"
]
)B.3.9 Figure 6.8
scopediagram(
[:x, :y], [1, 3],
[
"for x ∈ 1:2",
"begin",
"y = 1",
"end",
"y = x",
"x = 1",
"end",
"",
"x",
"y"
]
)B.4 Multiple dispatch
B.4.1 Figure 7.1
using Plots
# Creates a `Vector` of the types in question with coordinates
types = Vector{Tuple{Int64, Rational{Int64}, DataType}}(undef, 28)
unsignedtypes = [eval(Meta.parse("UInt$(2^n)")) for n ∈ 3:7]
primitiveintegers = [Bool; (collect ∘ Iterators.flatten ∘ zip)(signed.(unsignedtypes), unsignedtypes)]
for (i,T) ∈ enumerate(primitiveintegers)
types[2i-1:2i] = [(1, i//1, T); (2, i//1 + 1//2, Rational{T})]
end
types[23:28] = [
(2, 25//2, Float16);
(2, 27//2, Float32);
(2, 29//2, Float64);
(1, 77//6, BigInt);
(1, 85//6, Rational{BigInt});
(2, 31//2, BigFloat)
]
# Creates a `Vector` of lines that will be drawn on the diagram
lines = Tuple{Vector{Int64}, Vector{Rational{Int64}}}[]
for i ∈ 1:10
append!(lines, [ ([1,1],[i,i+1]), ([1,2],[i,i+1//2]), ([2,2],[i+1//2,i+3//2]) ])
end
append!(lines, [
([1,2],[11,23//2]),
([2,2],[23//2,25//2]),
([2,2],[25//2,27//2]),
([2,2],[27//2,29//2]),
([2,2],[29//2,31//2]),
([2,1],[23//2,77//6]),
([1,1],[77//6,85//6]),
([1,2],[85//6,31//2])
])
p = plot(
xlims = (0.5, 2.5),
ylims = (0, 16),
legend = false,
ticks = false,
framestyle = :none,
size = (500, 750)
)
# Draws the lines
plot!(p, [Shape(line...) for line ∈ lines], linecolor = :lightgrey)
# Draws white rectangles for the labels to sit on
plot!(p,
[Shape(
t[1] .+ length(string(t[3])) .* [-0.026,-0.026,0.03,0.03],
t[2] .+ [-0.3,0.3,0.3,-0.3]) for t ∈ types],
fillcolor = :white,
linewidth = 0
)
# Labels the points
plot!(p, annotations = types)B.5 Using packages
B.5.1 Figure 8.1
using Plots
# Black background with orbit
illustration = plot(
(θ -> (cos(θ) - 0.2, sin(θ))).(0:0.1:2π),
linestyle = :dash,
linecolor = :white,
linewidth = 1,
bg = :black,
legend = false,
ticks = false,
framestyle = :none
)
# Arrowhead for orbit
plot!(
illustration,
Shape([(0.8, -0.07),(0.82,-0.1),(0.78,-0.1)]),
c = :white,
markerstrokewidth = 0
)
# Lagrange points with labels
scatter!(
illustration,
[(0.438,0),(1.271,0),(-1.083,0),(0.3,0.866),(0.3,-0.866)],
ann = [
(0.538 , 0.1 , "L₁"),
(1.171 , 0.1 , "L₂"),
(-0.983, 0.1 , "L₃"),
(0.2 , 0.766 , "L₄"),
(0.2 , -0.766, "L₅")
],
markercolor = :white,
markersize = 3
)
# Sun and Earth
scatter!(
illustration,
[(-0.2,0), (0.8, 0)],
markercolor = [:yellow, :aqua],
markersize = [10, 4],
markerstrokewidth = 0
)B.5.2 Figure 8.2
plot(
[plot(
0.001:0.001:0.999,
[lagrangepoints[i, :, 1];
.- lagrangepoints[(1, 3, 2, 5, 4)[i], end-1:-1:1, 1]]
)
for i ∈ 1:5]...,
layout = (5, 1),
size = (500, 1000),
legend = false,
xlabel = "α",
ylabel = "x",
title = ["L₁" "L₂" "L₃" "L₄" "L₅"]
)B.6 Collections of values
B.6.1 Figure 9.1
This uses the same functions as Figure 3.1 in Chapter 3, but with a different function call at the end:
typetree(AbstractArray)B.7 Data from and to files
B.7.1 The function iobuffershow
The custom function iobuffershow creates the visualisations of IOBuffers seen in Chapter 10.
using Plots
# Shows an IOBuffer graphically
function iobuffershow(io::IOBuffer)
n = io.size
sc = max(min(1, (15//2)//n), 15//32)
h = (n+15) ÷ 16
p = plot(
legend = false,
ticks = false,
framestyle = :none,
size = (min(750, 10+100n), (12+120h)*sc),
xlims = (4//5, min(n, 16) + 1),
ylims = (-h-13//12, -11//12)
)
pos = position(io)
seekstart(io)
# Each byte is depicted as a box with the corresponding number inside
for (i, x) ∈ enumerate(readeach(io, UInt8))
iᵥ, iₕ = divrem(i-1, 16) .+ (1, 1)
plot!(p,
Shape([0, 0, 4//5, 4//5] .+ iₕ, [0, -11//12, -11//12, 0] .- iᵥ),
fillcolor = RGB(0.9),
linecolor = RGB(0.6),
linewidth = 3,
annotations = (iₕ + 2//5, -iᵥ - 11/24, x)
)
end
seek(io, pos)
posq, posr = pos == 0 ? (0, 0) : (divrem(pos-1, 16) .+ (0, 1))
# The pointer is drawn as a cursor
plot!(p,
Shape([posr + 9//10, posr + 9//10], [-posq - 11//12, -posq - 2]),
linewidth = 4
)
end;