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;