diff --git a/.github/workflows/update-episodes.yml b/.github/workflows/update-episodes.yml index 5fdac058..f5ae567e 100644 --- a/.github/workflows/update-episodes.yml +++ b/.github/workflows/update-episodes.yml @@ -46,6 +46,7 @@ jobs: run: | ls -RF mv output/carpentries/*.md episodes + mv output/code/* code - name: Create Pull Request uses: peter-evans/create-pull-request@v5 with: diff --git a/05_Write_functions.jl b/05_Write_functions.jl index 70a106c0..4b8507ac 100644 --- a/05_Write_functions.jl +++ b/05_Write_functions.jl @@ -141,8 +141,7 @@ Trebuchets.shoot(5, 0.25pi, 500)[2] function shoot_distance(windspeed, angle, weight) Trebuchets.shoot(windspeed, angle, weight)[2] -#md end ; -#nb end +end # !!! note "Implicit return" # Note that Melissa didn't have to use the `return` keyword, since in Julia the @@ -161,8 +160,7 @@ shoot_distance(5, 0.25pi, 500) function shoot_distance(trebuchet::Trebuchet, env::Environment) shoot_distance(env.wind, trebuchet.release_angle, trebuchet.counterweight) -#md end ; -#nb end +end # This method will call the former method and pass the correct fields from the # `Trebuchet` and `Environment` structures. @@ -176,8 +174,7 @@ function shoot_distance(trebuchet::Trebuchet, env::Environment) function shoot_distance(args...) # slurping Trebuchets.shoot(args...)[2] # splatting -#md end ; -#nb end +end # ### Anonymous functions @@ -186,40 +183,40 @@ function shoot_distance(args...) # slurping # These are _anonymous functions_. # They can be defined with either the so-called stabby lambda notation, -(windspeed, angle, weight) -> Trebuchets.shoot(windspeed, angle, weight)[2] ; +(windspeed, angle, weight) -> Trebuchets.shoot(windspeed, angle, weight)[2] # or in long form, by omitting the name: function (windspeed, angle, weight) Trebuchets.shoot(windspeed, angle, weight)[2] -#md end ; -#nb end +end -# ### Errors and macros +# ### Calling methods +# +# Now, that she defined all these methods she tests calling a few -# Melissa would like to set the fields of a `Trebuchet` using an index. -# She writes +shoot_distance(5, 0.25pi, 500) -#md # ```julia -#md # Trebuchets[1] = 2 -#md # ``` +# -#nb Trebuchets[1] = 2 +shoot_distance([5, 0.25pi, 500]) -#md # ```error -#md # ERROR: MethodError: no method matching setindex!(::Trebuchet, ::Int64, ::Int64) -#md # Stacktrace: -#md # [1] top-level scope -#md # @ REPL[4]:1 -#md # ``` +# For the other method she needs to construct `Trebuchet` and `Environment` objects first +env = Environment(5, 100) + +# + +trebuchet = Trebuchet(500, 0.25pi) + +# ### Errors and macros -# The error tells her two things: +# This error tells her two things: -# 1. a function named `setindex!` was called +# 1. a function named `size` was called # 2. it didn't have a method for `Trebuchet` -# Melissa wants to add the missing method to `setindex!` but she doesn't know +# Melissa wants to add the missing method to `size` but she doesn't know # where it is defined. # There is a handy _macro_ named `@which` that obtains the module where the # function is defined. @@ -230,23 +227,51 @@ function (windspeed, angle, weight) # Macros can transform any valid Julia expression and are quite powerful. # They can be expanded by prepending `@macroexpand` to the macro call of # interest. +using InteractiveUtils #hide -#md # ```julia -#md # @which setindex! -#md # ``` +@which size -#nb @which setindex! +# Now Melissa knows she needs to add a method to `Base.size` with the +# signature `(::Trebuchet)`. +# She can also lookup the docstring using the `@doc` macro -#md # ```output -#md # Base -#md # ``` +@doc size + +# With that information she can now implement this method: + +Base.size(::Trebuchet) = tuple(2) +# Now she can try again + +trebuchet = Trebuchet(500, 0.25pi) + +# Again, there is an error but this time the error message is different: +# It's no longer a method for `size` that is missing but for `getindex`. +# She looks up the documentation for that function + +@doc getindex + +# Note that the documentation for all methods gets shown and Melissa needs to look for the relevant method first. +# In this case its the paragraph starting with +# ```` +# getindex(A, inds...) +# ```` +# After a bit of pondering the figures it should be enough to add a method for `getindex` with a single number. +# ```` +# getindex(trebuchet::Trebuchet, i::Int) +# ```` +# +# !!! note "Syntactic sugar" +# In Julia `a[1]` is equivalent to `getindex(a, 1)` +# and `a[2] = 3` to `setindex!(a, 3, 2)` +# Likewise `a.b` is equivalent to `getproperty(a, :b)` +# and `a.b = 4` to `setproperty!(a, :b, 4)`. -# Now Melissa knows she needs to add a method to `Base.setindex!` with the -# signature `(::Trebuchet, ::Int64, ::Int64)`. # !!! keypoints # - "You can think of functions being a collection of methods" +# - "Methods are defined by their signature" +# - "The signature is defined by the number of arguments, their order and their type" # - "Keep the number of positional arguments low" # - "Macros transform Julia expressions" diff --git a/06_Interfacing_conditions.jl b/06_Interfacing_conditions.jl new file mode 100644 index 00000000..6956f289 --- /dev/null +++ b/06_Interfacing_conditions.jl @@ -0,0 +1,124 @@ +# !!! yaml +# --- +# title: "Interfaces & conditionals" +# teaching: 30 +# exercises: 30 +# --- +# +# !!! questions +# - "How to use conditionals?" +# - "What is an interface?" +# +# !!! objectives +# + +# ## Conditionals + +include("definition.jl") +Base.size(::Trebuchet) = tuple(2) + +# Now that Melissa knows that she has to add a method for +# ```` +# getindex(trebuchet::Trebuchet, i::Int) +# ```` +# she thinks about the implementation. + +# If the index is `1` she wants to get the `counterweight` field and if the index is `2` +# she wants to get `release_angle` and since these are the only two fields she +# wants to return an error if anything else comes in. +# In Julia the keywords to specify conditions are `if`, `elseif` and `else`, +# closed with an `end`. +# Thus she writes + +function Base.getindex(trebuchet::Trebuchet, i::Int) + if i === 1 + return trebuchet.counterweight + elseif i === 2 + return trebuchet.release_angle + else + error("Trebuchet only accepts indices 1 and 2, yours is $i") + end +end + +# And tries again: + +trebuchet = Trebuchet(500, 0.25pi) + +# Notice, that the printing is different from our `trebuchet` in [the former episode](03_Julia_type_system.ipynb). + +# ### Interfaces + +# Why is that? +# By subtyping `Trebuchet` as `AbstractVector` we implicitly opted into +# a widespread _interface_ in the Julia +# language: `AbstractArray`s. +# An interface is a collection of methods that should be implemented by all subtypes of the interface type in order for generic code to work. +# For example, the [Julia manual](https://docs.julialang.org/en/v1/manual/interfaces/#man-interface-array) lists all methods that a subtype of +# `AbstractArray` need to implement to adhere to the `AbstractArray` interface: + +# - `size(A)` returns a tuple containing the dimensions of `A` +# - `getindex(A, i::Int)` returns the value associated with index `i` +# - `setindex!(A, v, i::Int)` writes a new value `v` at the index `i` (optional) + +# Now, that Melissa implemented the mandatory methods for this interface for the `Trebuchet` type, it will work +# with every function in `Base` that accepts an `AbstractArray`. +# She tries a few things that now work without her writing explicit code for it: + +trebuchet + trebuchet + +# + +using LinearAlgebra +dot(trebuchet, trebuchet) + +# + +trebuchet * transpose(trebuchet) + +# That is, it now behaves like you would expect from an ordinary matrix. + +# Now she goes about implementing the missing optional method for `setindex!` of the `AbstractArray` interface. + +# !!! freecode "Implement `setindex!`" +# +# Write the missing method for `setindex(trebuchet::Trebuchet, v, i::Int)` similar to Melissas `getindex` function. +# +# !!! solution +# +# ```julia +# function Base.setindex!(trebuchet::Trebuchet, v, i::Int) +# if i === 1 +# trebuchet.counterweight = v +# elseif i === 2 +# trebuchet.release_angle = v +# else +# error("Trebuchet only accepts indices 1 and 2, yours is $i") +# end +# end +# ``` + +#md function Base.setindex!(trebuchet::Trebuchet, v, i::Int) #hide +#md if i === 1 #hide +#md trebuchet.counterweight = v #hide +#md elseif i === 2 #hide +#md trebuchet.release_angle = v #hide +#md else #hide +#md error("Trebuchet only accepts indices 1 and 2, yours is $i") #hide +#md end #hide +#md end #hide + +# With the new `Trebuchet` defined with a complete `AbstractArray` interface, +# Melissa tries her new method to modify a counterweight by index: + +trebuchet[1] = 2 +#- + +trebuchet + + + + +# !!! keypoints +# - "Interfaces are informal" +# - "Interfaces facilitate code reuse" +# - "Conditions use `if`, `elseif`, `else` and `end`" diff --git a/06_Control_flow.jl b/07_Loops.jl similarity index 64% rename from 06_Control_flow.jl rename to 07_Loops.jl index d3e47856..bc2e4c13 100644 --- a/06_Control_flow.jl +++ b/07_Loops.jl @@ -1,137 +1,43 @@ # !!! yaml # --- -# title: "Control flow" -# teaching: 60 -# exercises: 60 +# title: "Loops" +# teaching: 30 +# exercises: 30 # --- # # !!! questions # - "What are for and while loops?" -# - "How to use conditionals?" -# - "What is an interface?" +# - "What is a comprehension?" # # !!! objectives # -# ## Conditionals - include("definition.jl") - -# Now that Melissa knows which method to add she thinks about the implementation. - -# If the index is `1` she wants to set `counterweight` while if the index is `2` -# she wants to set `release_angle` and since these are the only two fields she -# wants to return an error if anything else comes in. -# In Julia the keywords to specify conditions are `if`, `elseif` and `else`, -# closed with an `end`. -# Thus she writes - -#- - -function Base.setindex!(trebuchet::Trebuchet, v, i::Int) +Base.size(::Trebuchet) = tuple(2) +function Base.getindex(trebuchet::Trebuchet, i::Int) if i === 1 - trebuchet.counterweight = v + return trebuchet.counterweight elseif i === 2 - trebuchet.release_angle = v + return trebuchet.release_angle else error("Trebuchet only accepts indices 1 and 2, yours is $i") end end - -# ### Interfaces - -# `setindex!` is actually one function of a widespread _interface_ in the Julia -# language: `AbstractArray`s. -# An interface is a collection of methods that are all implemented by a certain -# type. -# For example, the [Julia manual](https://docs.julialang.org/en/v1/manual/interfaces/#man-interface-array) lists all methods that a subtype of -# `AbstractArray` need to implement to adhere to the `AbstractArray` interface: - -# - `size(A)` returns a tuple containing the dimensions of `A` -# - `getindex(A, i::Int)` returns the value associated with index `i` -# - `setindex!(A, v, i::Int)` writes a new value `v` at the index `i` - -# If Melissa implements this interface for the `Trebuchet` type, it will work -# with every function in `Base` that accepts an `AbstractArray`. - -# She also needs to make `Trebuchet` a proper subtype of `AbstractArray` as she -# tried in [the types episode](03_Julia_type_system.ipynb). -# Therefore she _restarts her REPL_ and redefines `Trebuchet` and `Environment`, -# as well as the slurp-and-splat `shoot_distance` function: - -using Pkg -Pkg.activate("projects/trebuchet") - -import Trebuchet as Trebuchets - -mutable struct Trebuchet <: AbstractVector{Float64} - counterweight::Float64 - release_angle::Float64 -end - -struct Environment - wind::Float64 - target_distance::Float64 -end - -function shoot_distance(args...) - Trebuchets.shoot(args...)[2] +function Base.setindex!(trebuchet::Trebuchet, v, i::Int) + if i === 1 + trebuchet.counterweight = v + elseif i === 2 + trebuchet.release_angle = v + else + error("Trebuchet only accepts indices 1 and 2, yours is $i") + end end - function shoot_distance(trebuchet::Trebuchet, env::Environment) shoot_distance(env.wind, trebuchet.release_angle, trebuchet.counterweight) end - -# Then she goes about implementing the `AbstractArray` interface. - -# !!! freecode "Implement the `AbstractArray` interface for `Trebuchet`" -# -# Now we know enough to actually implement the `AbstractArray` interface. -# You don't need to implement the optional methods. -# -# Hint: Take a look at the docstrings of `getfield` and `tuple`. -# -# !!! solution -# -# ```julia -# Base.size(trebuchet::Trebuchet) = tuple(2) -# Base.getindex(trebuchet::Trebuchet, i::Int) = getfield(trebuchet, i) -# function Base.setindex!(trebuchet::Trebuchet, v, i::Int) -# if i === 1 -# trebuchet.counterweight = v -# elseif i === 2 -# trebuchet.release_angle = v -# else -# error("Trebuchet only accepts indices 1 and 2, yours is $i") -# end -# end -# ``` - -#md Base.size(trebuchet::Trebuchet) = tuple(2) #hide -#md Base.getindex(trebuchet::Trebuchet, i::Int) = getfield(trebuchet, i) #hide -#md function Base.setindex!(trebuchet::Trebuchet, v, i::Int) #hide -#md if i === 1 #hide -#md trebuchet.counterweight = v #hide -#md elseif i === 2 #hide -#md trebuchet.release_angle = v #hide -#md else #hide -#md error("Trebuchet only accepts indices 1 and 2, yours is $i") #hide -#md end #hide -#md end #hide - -# With the new `Trebuchet` defined with a complete `AbstractArray` interface, -# Melissa tries again to modify a counterweight by index: - -trebuchet = Trebuchet(500, 0.25pi) -#- - -trebuchet[1] = 2 -#- - -trebuchet - - -# ## Loops +function shoot_distance(args...) # slurping + Trebuchets.shoot(args...)[2] # splatting +end # Now Melissa knows how to shoot the virtual trebuchet and get the distance of # the projectile, but in order to aim she needs to take a lot of trial shots in a @@ -142,7 +48,30 @@ trebuchet # parameters, but that gets tiresome quickly. # A better way to do this is to use loops. -# But first Melissa needs a way to improve her parameters. +# ### Random search +# +# The first thing that comes to her mind is to randomly sample points of the parameter space of the trebuchet. +# The function `rand()` will give her a random number between 0 and 1 that is uniformly distributed. +# So + +Trebuchet( rand() * 500, rand() * pi/2 ) + +# will give her a Trebuchet with a weight between 0 and 500 and a release angle between 0 and pi/2 radians at random. + +# Now she can store the results of 3 random trebuchets in an array like this +env = Environment(5, 100) +distances = [shoot_distance(Trebuchet(rand() * 500, rand() * pi / 2), env) for _ in 1:3] + +# This is called an _array comprehension_. +# To get the information of the parameters and the results in one place she writes that again a bit differently +N = 10 +weights = [rand() * 500 for _ in 1:N] +angles = [rand() * pi/2 for _ in 1:N] +distances = [(w,a) => shoot_distance(Trebuchet(w, a), env) for (w, a) in zip(weights, angles)] + +# ### Gradient descent +# +# That is working out so far, but Melissa wonders if she can improve her parameters more systematically. # !!! note "Digression: Gradients" # The `shoot_distance` function takes three input parameters and returns one @@ -298,8 +227,5 @@ shoot_distance(better_trebuchet, environment) # That is more what she had in mind. Your trebuchet may be tuned differently, # but it should hit just as close as hers. - # !!! keypoints -# - "Interfaces are informal" # - "Use for loops for a known number of iterations and while loops for an unknown number of iterations." -# - "Julia packages compose nicely." diff --git a/07_Using_Modules.jl b/08_Using_Modules.jl similarity index 90% rename from 07_Using_Modules.jl rename to 08_Using_Modules.jl index ce8570b3..56d646cc 100644 --- a/07_Using_Modules.jl +++ b/08_Using_Modules.jl @@ -25,7 +25,8 @@ # the current working directory and pastes the code she got so far in there. # This is what it looks like: -#md open("aim_trebuchet.jl","w") do io #hide +#md path = mkpath(joinpath(@__DIR__, "code")) #hide +#md open(joinpath(path, "aim_trebuchet.jl"),"w") do io #hide #md print(io, raw""" #hide using Pkg Pkg.activate("projects/trebuchet") @@ -37,8 +38,6 @@ mutable struct Trebuchet <: AbstractVector{Float64} release_angle::Float64 end -Base.copy(trebuchet::Trebuchet) = Trebuchet(trebuchet.counterweight, trebuchet.release_angle) - Base.size(trebuchet::Trebuchet) = tuple(2) Base.getindex(trebuchet::Trebuchet, i::Int) = getfield(trebuchet, i) @@ -58,10 +57,6 @@ struct Environment target_distance::Float64 end -function shoot_distance(windspeed, angle, weight) - Trebuchets.shoot(windspeed, angle, weight)[2] -end - function shoot_distance(args...) Trebuchets.shoot(args...)[2] end @@ -118,7 +113,7 @@ shoot_distance(precise_trebuchet, environment) # `Environment`. # This way she can leave her other code unchanged. -#md open("MelissasModule.jl","w") do io #hide +#md open(joinpath(path, "MelissasModule.jl"),"w") do io #hide #md print(io, raw""" #hide module MelissasModule @@ -134,8 +129,6 @@ mutable struct Trebuchet <: AbstractVector{Float64} release_angle::Float64 end -Base.copy(trebuchet::Trebuchet) = Trebuchet(trebuchet.counterweight, trebuchet.release_angle) - Base.size(trebuchet::Trebuchet) = tuple(2) Base.getindex(trebuchet::Trebuchet, i::Int) = getfield(trebuchet, i) @@ -155,10 +148,6 @@ struct Environment target_distance::Float64 end -function shoot_distance(windspeed, angle, weight) - Trebuchets.shoot(windspeed, angle, weight)[2] -end - function shoot_distance(args...) Trebuchets.shoot(args...)[2] end @@ -187,7 +176,7 @@ end # MelissasModule # The rest of the code goes to a file she calls `MelissasCode.jl`. -#md open("MelissasCode.jl","w") do io #hide +#md open(joinpath(path, "MelissasCode.jl"),"w") do io #hide #md print(io, raw""" #hide using .MelissasModule @@ -228,9 +217,10 @@ using Revise #nb includet("MelissasModule.jl") #nb include("MelissasCode.jl") -#md include(joinpath(@__DIR__,"MelissasModule.jl")) #hide -#md includet(joinpath(@__DIR__,"MelissasModule.jl")) -#md include(joinpath(@__DIR__,"MelissasCode.jl")) +#md include(joinpath(path,"MelissasModule.jl")) #hide +#md includet(joinpath(path,"MelissasModule.jl")) +#md include(joinpath(path,"MelissasCode.jl")) +#md # where `path` is the path to her files. # and any change she makes in `MelissasModule.jl` will be visible in the next run diff --git a/08_Creating_Packages.jl b/09_Creating_Packages.jl similarity index 84% rename from 08_Creating_Packages.jl rename to 09_Creating_Packages.jl index 16510330..a000779a 100644 --- a/08_Creating_Packages.jl +++ b/09_Creating_Packages.jl @@ -51,6 +51,20 @@ # Open your `Project.toml` and add `name = `, `uuid = ` # and optionally an `authors` field, each on a separate line. +#md path = mkpath(joinpath(@__DIR__, "code")) #hide +#md rm(joinpath(path, "MelissasPackage"), recursive = true, force = true) #hide +#md old = pwd() #hide +#md cd(path) #hide +#md using Pkg #hide +#md Pkg.generate("MelissasPackage") #hide +#md open(joinpath(path, "MelissasPackage", "src", "MelissasPackage.jl"), "w") do io #hide +#md for line in eachline(joinpath(path, "MelissasModule.jl")) #hide +#md write(io, replace(line, "MelissasModule" => "MelissasPackage")) #hide +#md end #hide +#md end #hide +#md cd(old) #hide + + # Now Melissa can use #md # ```julia diff --git a/09_Adding_tests.jl b/10_Adding_tests.jl similarity index 100% rename from 09_Adding_tests.jl rename to 10_Adding_tests.jl diff --git a/10_Wrapping_Up.jl b/11_Wrapping_Up.jl similarity index 100% rename from 10_Wrapping_Up.jl rename to 11_Wrapping_Up.jl diff --git a/generate.jl b/generate.jl index c08d4b28..19c2daab 100644 --- a/generate.jl +++ b/generate.jl @@ -1,4 +1,3 @@ -cd(@__DIR__) using Distributed while nprocs() < min(4, length(Sys.cpu_info())) addprocs(1) @@ -85,8 +84,8 @@ end pmap(files) do file # Literate.notebook(file, notebook_out; execute=false, preprocess = replace_includes) # Literate.markdown(file, markdown_out; credit = false, execute=true, preprocess = replace_includes, postprocess = md_print∘setup_link_replace) - if contains(file, "Overview") return end + if contains(file, "Overview") || contains(file, "Creating_Packages") return end Literate.markdown(file, lesson_out; credit = false, execute=true, flavor=Literate.CarpentriesFlavor(), preprocess = replace_includes∘carpentries_div_names, postprocess = setup_link_replace∘remove_sandbox_output∘handle_repl∘fix_activation_output) end - +Literate.markdown(only(filter(contains("Creating_Packages"), files)), lesson_out; credit = false, execute=true, flavor=Literate.CarpentriesFlavor(), preprocess = replace_includes∘carpentries_div_names, postprocess = setup_link_replace∘remove_sandbox_output∘handle_repl∘fix_activation_output) @everywhere Pkg.activate(@__DIR__)