From 6658975d8b13541233557250b0a10a2ecf223923 Mon Sep 17 00:00:00 2001 From: Niklas Heim Date: Thu, 9 Nov 2023 18:14:25 +0100 Subject: [PATCH] lecture 07: macros --- docs/Manifest.toml | 38 +- docs/Project.toml | 1 + docs/make.jl | 7 + docs/src/lecture_04/hw.md | 2 +- .../src/lecture_07/Ecosystem.jl/Manifest.toml | 229 ++++++ docs/src/lecture_07/Ecosystem.jl/Project.toml | 8 + .../lecture_07/Ecosystem.jl/src/Ecosystem.jl | 49 ++ .../Ecosystem.jl/src/ecosystem_agents.jl | 57 ++ .../Ecosystem.jl/src/ecosystem_macros.jl | 59 ++ docs/src/lecture_07/hw.md | 53 ++ docs/src/lecture_07/lab.md | 564 ++++++++++++++ docs/src/lecture_07/lecture.md | 735 ++++++++++++++++++ docs/src/lecture_07/macros.md | 1 + 13 files changed, 1783 insertions(+), 20 deletions(-) create mode 100644 docs/src/lecture_07/Ecosystem.jl/Manifest.toml create mode 100644 docs/src/lecture_07/Ecosystem.jl/Project.toml create mode 100644 docs/src/lecture_07/Ecosystem.jl/src/Ecosystem.jl create mode 100644 docs/src/lecture_07/Ecosystem.jl/src/ecosystem_agents.jl create mode 100644 docs/src/lecture_07/Ecosystem.jl/src/ecosystem_macros.jl create mode 100644 docs/src/lecture_07/hw.md create mode 100644 docs/src/lecture_07/lab.md create mode 100644 docs/src/lecture_07/lecture.md create mode 100644 docs/src/lecture_07/macros.md diff --git a/docs/Manifest.toml b/docs/Manifest.toml index 9ab977d5..a16dadea 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -1,8 +1,8 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.9.1" +julia_version = "1.9.3" manifest_format = "2.0" -project_hash = "0b5279d06f321f76004797895f270090bff4b56e" +project_hash = "4724766098b8c6099bfd3209771048f441a4b63c" [[deps.ANSIColoredPrinters]] git-tree-sha1 = "574baf8110975760d391c710b6341da1afa48d8c" @@ -16,9 +16,9 @@ version = "0.4.4" [[deps.Adapt]] deps = ["LinearAlgebra", "Requires"] -git-tree-sha1 = "68c4c187a232e7abe00ac29e3b03e09af9d77317" +git-tree-sha1 = "02f731463748db57cc2ebfbd9fbc9ce8280d3433" uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" -version = "3.7.0" +version = "3.7.1" weakdeps = ["StaticArrays"] [deps.Adapt.extensions] @@ -128,13 +128,13 @@ weakdeps = ["Dates", "LinearAlgebra"] [[deps.CompilerSupportLibraries_jll]] deps = ["Artifacts", "Libdl"] uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "1.0.2+0" +version = "1.0.5+0" [[deps.ConcurrentUtilities]] deps = ["Serialization", "Sockets"] -git-tree-sha1 = "5372dbbf8f0bdb8c700db5367132925c0771ef7e" +git-tree-sha1 = "8cfa272e8bdedfa88b6aefbbca7c19f1befac519" uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb" -version = "2.2.1" +version = "2.3.0" [[deps.ConstructionBase]] deps = ["LinearAlgebra"] @@ -468,9 +468,9 @@ uuid = "dd4b983a-f0e5-5f8d-a1b7-129d4a5fb1ac" version = "2.10.1+0" [[deps.LaTeXStrings]] -git-tree-sha1 = "f2355693d6778a178ade15952b7ac47a4ff97996" +git-tree-sha1 = "50901ebc375ed41dbf8058da26f9de442febbbec" uuid = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" -version = "1.3.0" +version = "1.3.1" [[deps.Latexify]] deps = ["Formatting", "InteractiveUtils", "LaTeXStrings", "MacroTools", "Markdown", "OrderedCollections", "Printf", "Requires"] @@ -607,10 +607,10 @@ uuid = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" version = "0.1.2" [[deps.MbedTLS]] -deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "Random", "Sockets"] -git-tree-sha1 = "03a9b9718f5682ecb107ac9f7308991db4ce395b" +deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "NetworkOptions", "Random", "Sockets"] +git-tree-sha1 = "f512dc13e64e96f703fd92ce617755ee6b5adf0f" uuid = "739be429-bea8-5141-9913-cc70e7f3736d" -version = "1.1.7" +version = "1.1.8" [[deps.MbedTLS_jll]] deps = ["Artifacts", "Libdl"] @@ -731,7 +731,7 @@ version = "0.42.2+0" [[deps.Pkg]] deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.9.0" +version = "1.9.2" [[deps.PlotThemes]] deps = ["PlotUtils", "Statistics"] @@ -862,9 +862,9 @@ version = "0.1.0" [[deps.Scratch]] deps = ["Dates"] -git-tree-sha1 = "30449ee12237627992a99d5e30ae63e4d78cd24a" +git-tree-sha1 = "3bac05bc7e74a75fd9cba4295cde4045d9fe2386" uuid = "6c6a2e73-6563-6170-7368-637461726353" -version = "1.2.0" +version = "1.2.1" [[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" @@ -984,9 +984,9 @@ deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [[deps.TranscodingStreams]] -git-tree-sha1 = "49cbf7c74fafaed4c529d47d48c8f7da6a19eb75" +git-tree-sha1 = "1fbeaaca45801b4ba17c251dd8603ef24801dd84" uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" -version = "0.10.1" +version = "0.10.2" weakdeps = ["Random", "Test"] [deps.TranscodingStreams.extensions] @@ -1073,9 +1073,9 @@ version = "1.1.34+0" [[deps.XZ_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "cf2c7de82431ca6f39250d2fc4aacd0daa1675c0" +git-tree-sha1 = "522b8414d40c4cbbab8dee346ac3a09f9768f25d" uuid = "ffd25f8a-64ca-5728-b0f7-c24cf3aae800" -version = "5.4.4+0" +version = "5.4.5+0" [[deps.Xorg_libICE_jll]] deps = ["Libdl", "Pkg"] diff --git a/docs/Project.toml b/docs/Project.toml index fa88d397..facaef29 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -4,6 +4,7 @@ BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8" GraphRecipes = "bd48cda9-67a9-57be-86fa-5b3c104eda73" +MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Scientific_Programming_in_Julia = "ff288558-a322-4c39-84bc-85bf6acf7e03" SoftPosit = "0775deef-a35f-56d7-82da-cfc52f91364d" diff --git a/docs/make.jl b/docs/make.jl index de8ef7f0..1fd8c968 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -76,6 +76,12 @@ lecture_06 = [ "Homework" => "./lecture_06/hw.md" ] +lecture_07 = [ + "Lecture" => "./lecture_07/lecture.md" + "Lab" => "./lecture_07/lab.md" + "Homework" => "./lecture_07/hw.md" +] + makedocs(; modules = [Scientific_Programming_in_Julia], authors = "JuliaTeachingCTU", @@ -98,6 +104,7 @@ makedocs(; "4: Package development, unit tests & CI" => lecture_04, "5: Performance benchmarking" => lecture_05, "6: Lanuage introspection" => lecture_06, + "7: Macros" => lecture_07, ], ) diff --git a/docs/src/lecture_04/hw.md b/docs/src/lecture_04/hw.md index 133f7556..03d132b2 100644 --- a/docs/src/lecture_04/hw.md +++ b/docs/src/lecture_04/hw.md @@ -1,4 +1,4 @@ -# Homework 4 +# [Homework 4](@id hw4) In this homework you will have to write two additional `@testset`s for the Ecosystem. One testset should be contained in a file `test/sheep.jl` and verify diff --git a/docs/src/lecture_07/Ecosystem.jl/Manifest.toml b/docs/src/lecture_07/Ecosystem.jl/Manifest.toml new file mode 100644 index 00000000..87e3383a --- /dev/null +++ b/docs/src/lecture_07/Ecosystem.jl/Manifest.toml @@ -0,0 +1,229 @@ +# This file is machine-generated - editing it directly is not advised + +[[ArgTools]] +uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" + +[[Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" + +[[Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[ChainRulesCore]] +deps = ["Compat", "LinearAlgebra", "SparseArrays"] +git-tree-sha1 = "f885e7e7c124f8c92650d61b9477b9ac2ee607dd" +uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +version = "1.11.1" + +[[ChangesOfVariables]] +deps = ["LinearAlgebra", "Test"] +git-tree-sha1 = "9a1d594397670492219635b35a3d830b04730d62" +uuid = "9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0" +version = "0.1.1" + +[[Compat]] +deps = ["Base64", "Dates", "DelimitedFiles", "Distributed", "InteractiveUtils", "LibGit2", "Libdl", "LinearAlgebra", "Markdown", "Mmap", "Pkg", "Printf", "REPL", "Random", "SHA", "Serialization", "SharedArrays", "Sockets", "SparseArrays", "Statistics", "Test", "UUIDs", "Unicode"] +git-tree-sha1 = "dce3e3fea680869eaa0b774b2e8343e9ff442313" +uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" +version = "3.40.0" + +[[DataAPI]] +git-tree-sha1 = "cc70b17275652eb47bc9e5f81635981f13cea5c8" +uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a" +version = "1.9.0" + +[[DataStructures]] +deps = ["Compat", "InteractiveUtils", "OrderedCollections"] +git-tree-sha1 = "7d9d316f04214f7efdbb6398d545446e246eff02" +uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +version = "0.18.10" + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[DelimitedFiles]] +deps = ["Mmap"] +uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab" + +[[Distributed]] +deps = ["Random", "Serialization", "Sockets"] +uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" + +[[DocStringExtensions]] +deps = ["LibGit2"] +git-tree-sha1 = "b19534d1895d702889b219c382a6e18010797f0b" +uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +version = "0.8.6" + +[[Downloads]] +deps = ["ArgTools", "LibCURL", "NetworkOptions"] +uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" + +[[EcosystemCore]] +deps = ["StatsBase"] +git-tree-sha1 = "234cff8809f0c32fde3d7ed7c20b66af4286db14" +repo-rev = "onlycore" +repo-url = "https://github.com/JuliaTeachingCTU/EcosystemCore.jl.git" +uuid = "3e0d8730-8ea0-4ee2-afe6-c85384c618a2" +version = "0.1.0" + +[[InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[InverseFunctions]] +deps = ["Test"] +git-tree-sha1 = "a7254c0acd8e62f1ac75ad24d5db43f5f19f3c65" +uuid = "3587e190-3f89-42d0-90ee-14403ec27112" +version = "0.1.2" + +[[IrrationalConstants]] +git-tree-sha1 = "7fd44fd4ff43fc60815f8e764c0f352b83c49151" +uuid = "92d709cd-6900-40b7-9082-c6be49f344b6" +version = "0.1.1" + +[[LibCURL]] +deps = ["LibCURL_jll", "MozillaCACerts_jll"] +uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" + +[[LibCURL_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] +uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" + +[[LibGit2]] +deps = ["Base64", "NetworkOptions", "Printf", "SHA"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" + +[[LibSSH2_jll]] +deps = ["Artifacts", "Libdl", "MbedTLS_jll"] +uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" + +[[Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[LinearAlgebra]] +deps = ["Libdl"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + +[[LogExpFunctions]] +deps = ["ChainRulesCore", "ChangesOfVariables", "DocStringExtensions", "InverseFunctions", "IrrationalConstants", "LinearAlgebra"] +git-tree-sha1 = "be9eef9f9d78cecb6f262f3c10da151a6c5ab827" +uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" +version = "0.3.5" + +[[Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[MbedTLS_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" + +[[Missings]] +deps = ["DataAPI"] +git-tree-sha1 = "bf210ce90b6c9eed32d25dbcae1ebc565df2687f" +uuid = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" +version = "1.0.2" + +[[Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[MozillaCACerts_jll]] +uuid = "14a3606d-f60d-562e-9121-12d972cd8159" + +[[NetworkOptions]] +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" + +[[OrderedCollections]] +git-tree-sha1 = "85f8e6578bf1f9ee0d11e7bb1b1456435479d47c" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.4.1" + +[[Pkg]] +deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" + +[[Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[Random]] +deps = ["Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[[Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[SharedArrays]] +deps = ["Distributed", "Mmap", "Random", "Serialization"] +uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383" + +[[Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[SortingAlgorithms]] +deps = ["DataStructures"] +git-tree-sha1 = "b3363d7460f7d098ca0912c69b082f75625d7508" +uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c" +version = "1.0.1" + +[[SparseArrays]] +deps = ["LinearAlgebra", "Random"] +uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + +[[Statistics]] +deps = ["LinearAlgebra", "SparseArrays"] +uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + +[[StatsAPI]] +git-tree-sha1 = "1958272568dc176a1d881acb797beb909c785510" +uuid = "82ae8749-77ed-4fe6-ae5f-f523153014b0" +version = "1.0.0" + +[[StatsBase]] +deps = ["DataAPI", "DataStructures", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"] +git-tree-sha1 = "eb35dcc66558b2dda84079b9a1be17557d32091a" +uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" +version = "0.33.12" + +[[TOML]] +deps = ["Dates"] +uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" + +[[Tar]] +deps = ["ArgTools", "SHA"] +uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" + +[[Test]] +deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[[UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" + +[[Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" + +[[Zlib_jll]] +deps = ["Libdl"] +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" + +[[nghttp2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" + +[[p7zip_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" diff --git a/docs/src/lecture_07/Ecosystem.jl/Project.toml b/docs/src/lecture_07/Ecosystem.jl/Project.toml new file mode 100644 index 00000000..490ae7aa --- /dev/null +++ b/docs/src/lecture_07/Ecosystem.jl/Project.toml @@ -0,0 +1,8 @@ +name = "Ecosystem" +uuid = "e629da61-eb5c-4a46-88f0-e08c691183e3" +authors = ["Jan Francu "] +version = "0.1.0" + +[deps] +EcosystemCore = "3e0d8730-8ea0-4ee2-afe6-c85384c618a2" +StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" diff --git a/docs/src/lecture_07/Ecosystem.jl/src/Ecosystem.jl b/docs/src/lecture_07/Ecosystem.jl/src/Ecosystem.jl new file mode 100644 index 00000000..02d9145e --- /dev/null +++ b/docs/src/lecture_07/Ecosystem.jl/src/Ecosystem.jl @@ -0,0 +1,49 @@ +module Ecosystem + +using StatsBase +using EcosystemCore + +include("./ecosystem_macros.jl") +include("./ecosystem_agents.jl") + +export World +export agent_step!, agent_count, world_step!, simulate!, every_nth + +function simulate!(world::World, iters::Int; cb=()->()) + for i in 1:iters + world_step!(world) + cb() + end +end + +agent_count(p::Plant) = size(p)/EcosystemCore.max_size(p) +agent_count(::Animal) = 1 +agent_count(as::Vector{<:Agent}) = sum(agent_count,as) + +function agent_count(w::World) + function op(d::Dict,a::Agent{S}) where S<:Species + n = nameof(S) + if n in keys(d) + d[n] += agent_count(a) + else + d[n] = agent_count(a) + end + return d + end + foldl(op, w.agents |> values |> collect, init=Dict{Symbol,Real}()) +end + +function every_nth(f::Function, n::Int) + i = 1 + function callback(args...) + # display(i) # comment this out to see out the counter increases + if i == n + f(args...) + i = 1 + else + i += 1 + end + end +end + +end diff --git a/docs/src/lecture_07/Ecosystem.jl/src/ecosystem_agents.jl b/docs/src/lecture_07/Ecosystem.jl/src/ecosystem_agents.jl new file mode 100644 index 00000000..2be3a789 --- /dev/null +++ b/docs/src/lecture_07/Ecosystem.jl/src/ecosystem_agents.jl @@ -0,0 +1,57 @@ +### old definition + +#= +# animals +abstract type Sheep <: AnimalSpecies end +abstract type Wolf <: AnimalSpecies end + +# plants +abstract type Mushroom <: PlantSpecies end +abstract type Grass <: PlantSpecies end + +export Grass, Sheep, Wolf, Mushroom + +Base.show(io::IO, ::Type{Sheep}) = print(io,"🐑") +Base.show(io::IO, ::Type{Wolf}) = print(io,"🐺") + +Base.show(io::IO,::Type{Mushroom}) = print(io,"🍄") +Base.show(io::IO, ::Type{Grass}) = print(io,"🌿") + + +function EcosystemCore.eat!(s::Animal{Sheep}, m::Plant{Mushroom}, w::World) + if size(p)>0 + incr_energy!(s, -size(m)*Δenergy(s)) + m.size = 0 + end +end + +function EcosystemCore.eat!(a::Animal{Wolf}, b::Animal{Sheep}, w::World) + incr_energy!(a, energy(b)*Δenergy(a)) + kill_agent!(b,w) +end + +function EcosystemCore.eat!(a::Animal{Sheep}, b::Plant{Grass}, w::World) + incr_energy!(a, size(b)*Δenergy(a)) + b.size = 0 +end + +EcosystemCore.eats(::Animal{Sheep}, ::Plant{Mushroom}) = true +EcosystemCore.eats(::Animal{Sheep}, ::Plant{Grass}) = true +EcosystemCore.eats(::Animal{Wolf},::Animal{Sheep}) = true + +EcosystemCore.mates(::Animal{S,Female}, ::Animal{S,Male}) where S<:Species = true +EcosystemCore.mates(::Animal{S,Male}, ::Animal{S,Female}) where S<:Species = true +EcosystemCore.mates(a::Agent, b::Agent) = false +=# + +### new definition using macros from `ecosystem_macros.jl` +@plant Grass 🌿 +@plant Broccoli 🥦 +@plant Mushroom 🍄 +@animal Sheep 🐑 +@animal Wolf 🐺 +@animal Rabbit 🐇 + +@eats Rabbit [Grass => 0.5, Broccoli => 1.0] +@eats Sheep [Grass => 0.5, Broccoli => 1.0, Mushroom => -1.0] +@eats Wolf [Sheep => 0.9] \ No newline at end of file diff --git a/docs/src/lecture_07/Ecosystem.jl/src/ecosystem_macros.jl b/docs/src/lecture_07/Ecosystem.jl/src/ecosystem_macros.jl new file mode 100644 index 00000000..63c1cfc4 --- /dev/null +++ b/docs/src/lecture_07/Ecosystem.jl/src/ecosystem_macros.jl @@ -0,0 +1,59 @@ +### species definition +macro species(typ, name, icon) + esc(_species(typ, name, icon)) +end + +function _species(typ, name, icon) + quote + abstract type $name <: $(typ == :Animal ? AnimalSpecies : PlantSpecies) end + Base.show(io::IO, ::Type{$name}) = print(io, $(QuoteNode(icon))) + export $name + end +end + +macro plant(name, icon) + return :(@species Plant $name $icon) +end + +macro animal(name, icon) + return :(@species Animal $name $icon) +end + +### eating behavior +macro eats(species::Symbol, foodlist::Expr) + return esc(_eats(species, foodlist)) +end + + +function _generate_eat(eater::Type{<:AnimalSpecies}, food::Type{<:PlantSpecies}, multiplier) + quote + EcosystemCore.eats(::Animal{$(eater)}, ::Plant{$(food)}) = true + function EcosystemCore.eat!(a::Animal{$(eater)}, p::Plant{$(food)}, w::World) + if size(p)>0 + incr_energy!(a, $(multiplier)*size(p)*Δenergy(a)) + p.size = 0 + end + end + end +end + +function _generate_eat(eater::Type{<:AnimalSpecies}, food::Type{<:AnimalSpecies}, multiplier) + quote + EcosystemCore.eats(::Animal{$(eater)}, ::Animal{$(food)}) = true + function EcosystemCore.eat!(ae::Animal{$(eater)}, af::Animal{$(food)}, w::World) + incr_energy!(ae, $(multiplier)*energy(af)*Δenergy(ae)) + kill_agent!(af, w) + end + end +end + +_parse_eats(ex) = Dict(arg.args[2] => arg.args[3] for arg in ex.args if arg.head == :call && arg.args[1] == :(=>)) + +function _eats(species, foodlist) + cfg = _parse_eats(foodlist) + code = Expr(:block) + for (k,v) in cfg + push!(code.args, _generate_eat(eval(species), eval(k), v)) + end + code +end \ No newline at end of file diff --git a/docs/src/lecture_07/hw.md b/docs/src/lecture_07/hw.md new file mode 100644 index 00000000..04f9b9c6 --- /dev/null +++ b/docs/src/lecture_07/hw.md @@ -0,0 +1,53 @@ +# [Homework 7: Creating world in 3 days/steps](@id hw07) + +## How to submit +Put all the code of inside `hw.jl`. Zip only this file (not its parent folder) and upload it to BRUTE. You should assume that only +```julia +using Ecosystem +``` +will be put before your file is executed, but do not include them in your solution. The version of `Ecosystem` pkg should be the same as in [HW4](@ref hw4). + +```@raw html +
+
Homework (2 points)
+
+``` +Create a macro `@ecosystem` that should be able to define a world given a list of statements `@add # $species ${optional:sex}` +```julia +world = @ecosystem begin + @add 10 Sheep female # adds 10 female sheep + @add 2 Sheep male # adds 2 male sheep + @add 100 Grass # adds 100 pieces of grass + @add 3 Wolf # adds 5 wolf with random sex +end +``` +`@add` should not be treated as a macro, but rather just as a syntax, that can be easily matched. + +As this is not a small task let's break it into 3 steps. (These intemediate steps will also be checked in BRUTE.) +1. Define method `default_config(::Type{T})` for each `T` in `Grass, Wolf,...`, which returns a named tuple of default parameters for that particular agent (you can choose the default values however you like). +2. Define method `_add_agents(max_id, count::Int, species::Type{<:Species})` and `_add_agents(max_id, count::Int, species::Type{<:AnimalSpecies}, sex::Sex)` that return an array of `count` agents of species `species` with `id` going from `max_id+1` to `max_id+count`. Default parameters should be constructed with `default_config`. Make sure you can handle even animals with random sex (`@add 3 Wolf`). +3. Define the underlying function `_ecosystem(ex)`, which parses the block expression and creates a piece of code that constructs the world. + +You can test the macro (more precisely the `_ecosystem` function) with the following expression +```julia +ex = :(begin + @add 10 Sheep female + @add 2 Sheep male + @add 100 Grass + @add 3 Wolf +end) +genex = _ecosystem(ex) +world = eval(genex) +``` + +```@raw html +
+ +``` diff --git a/docs/src/lecture_07/lab.md b/docs/src/lecture_07/lab.md new file mode 100644 index 00000000..52b12081 --- /dev/null +++ b/docs/src/lecture_07/lab.md @@ -0,0 +1,564 @@ +# [Lab 07: Macros](@id macro_lab) +A little reminder from the [lecture](@ref macro_lecture), a macro in its essence is a function, which +1. takes as an input an expression (parsed input) +2. modifies the expressions in arguments +3. inserts the modified expression at the same place as the one that is parsed. + +In this lab we are going to use what we have learned about manipulation of expressions and explore avenues of where macros can be useful +- convenience (`@repeat`, `@show`) +- performance critical code generation (`@poly`) +- alleviate tedious code generation (`@species`, `@eats`) +- just as a syntactic sugar (`@ecosystem`) + +## Show macro +Let's start with dissecting "simple" `@show` macro, which allows us to demonstrate advanced concepts of macros and expression manipulation. +```@repl lab07_show +x = 1 +@show x + 1 +let y = x + 1 # creates a temporary local variable + println("x + 1 = ", y) + y # show macro also returns the result +end + +# assignments should create the variable +@show x = 3 +let y = x = 2 + println("x = 2 = ", y) + y +end +x # should be equal to 2 +``` + +The original Julia's [implementation](https://github.com/JuliaLang/julia/blob/ae8452a9e0b973991c30f27beb2201db1b0ea0d3/base/show.jl#L946-L959) is not dissimilar to the following macro definition: +```@example lab07_show +macro myshow(ex) + quote + println($(QuoteNode(ex)), " = ", repr(begin local value = $(esc(ex)) end)) + value + end +end +``` +Testing it gives us the expected behavior +```@repl lab07_show +@myshow xx = 1 + 1 +xx # should be defined +``` +In this "simple" example, we had to use the following concepts mentioned already in the [lecture](@ref macro_lecture): +- `QuoteNode(ex)` is used to wrap the expression inside another layer of quoting, such that when it is interpolated into `:()` it stays being a piece of code instead of the value it represents - [**TRUE QUOTING**](@ref lec7_quotation) +- `esc(ex)` is used in case that the expression contains an assignment, that has to be evaluated in the top level module `Main` (we are `esc`aping the local context) - [**ESCAPING**](@ref lec7_hygiene) +- `$(QuoteNode(ex))` and `$(esc(ex))` is used to evaluate an expression into another expression. [**INTERPOLATION**](@ref lec7_quotation) +- `local value = ` is used in order to return back the result after evaluation + +Lastly, let's mention that we can use `@macroexpand` to see how the code is manipulated in the `@myshow` macro +```@repl lab07_show +@macroexpand @show x + 1 +``` + +## Repeat macro +In the profiling/performance [labs](@ref perf_lab) we have sometimes needed to run some code multiple times in order to gather some samples and we have tediously written out simple for loops inside functions such as this +```julia +function run_polynomial(n, a, x) + for _ in 1:n + polynomial(a, x) + end +end +``` + +We can remove this boilerplate code by creating a very simple macro that does this for us. +```@raw html +
+
Exercise
+
+``` +Define macro `@repeat` that takes two arguments, first one being the number of times a code is to be run and the other being the actual code. +```julia +julia> @repeat 3 println("Hello!") +Hello! +Hello! +Hello! +``` +Before defining the macro, it is recommended to write the code manipulation functionality into a helper function `_repeat`, which helps in organization and debugging of macros. +```julia +_repeat(3, :(println("Hello!"))) # testing "macro" without defining it +``` + +**HINTS**: +- use `$` interpolation into a for loop expression; for example given `ex = :(1+x)` we can interpolate it into another expression `:($ex + y)` -> `:(1 + x + y)` +- if unsure what gets interpolated use round brackets `:($(ex) + y)` +- macro is a function that *creates* code that does what we want + +**BONUS**: +What happens if we call `@repeat 3 x = 2`? Is `x` defined? + +```@raw html +
+
+Solution:

+``` + +```@repl lab07_repeat +macro repeat(n::Int, ex) + return _repeat(n, ex) +end + +function _repeat(n::Int, ex) + :(for _ in 1:$n + $ex + end) +end + +_repeat(3, :(println("Hello!"))) +@repeat 3 println("Hello!") +``` +Even if we had used escaping the expression `x = 2` won't get evaluated properly due to the induced scope of the for loop. In order to resolve this we would have to specially match that kind of expression and generate a proper syntax withing the for loop `global $ex`. However we may just warn the user in the docstring that the usage is disallowed. + +```@raw html +

+``` +Note that this kind of repeat macro is also defined in the [`Flux.jl`](https://fluxml.ai/) machine learning framework, wherein it's called `@epochs` and is used for creating training [loop](https://fluxml.ai/Flux.jl/stable/training/training/#Datasets). + +## [Polynomial macro](@id lab07_polymacro) +This is probably the last time we are rewriting the `polynomial` function, though not quite in the same way. We have seen in the last [lab](@ref introspection_lab), that some optimizations occur automatically, when the compiler can infer the length of the coefficient array, however with macros we can *generate* optimized code directly (not on the same level - we are essentially preparing already unrolled/inlined code). + +Ideally we would like to write some macro `@poly` that takes a polynomial in a mathematical notation and spits out an anonymous function for its evaluation, where the loop is unrolled. + +*Example usage*: +```julia +p = @poly x 3x^2+2x^1+10x^0 # the first argument being the independent variable to match +p(2) # return the value +``` + +However in order to make this happen, let's first consider much simpler case of creating the same but without the need for parsing the polynomial as a whole and employ the fact that macro can have multiple arguments separated by spaces. + +```julia +p = @poly 3 2 10 +p(2) +``` + +```@raw html +
+
Exercise
+
+``` +Create macro `@poly` that takes multiple arguments and creates an anonymous function that constructs the unrolled code. Instead of directly defining the macro inside the macro body, create helper function `_poly` with the same signature that can be reused outside of it. + +Recall Horner's method polynomial evaluation from previous [labs](@ref horner): +```julia +function polynomial(a, x) + accumulator = a[end] * one(x) + for i in length(a)-1:-1:1 + accumulator = accumulator * x + a[i] + #= accumulator = muladd(x, accumulator, a[i]) =# # equivalent + end + accumulator +end +``` + +**HINTS**: +- you can use `muladd` function as replacement for `ac * x + a[i]` +- think of the `accumulator` variable as the mathematical expression that is incrementally built (try to write out the Horner's method[^1] to see it) +- you can nest expression arbitrarily +- the order of coefficients has different order than in previous labs (going from high powers of `x` last to them being first) +- use `evalpoly` to check the correctness +```julia +using Test +p = @poly 3 2 10 +@test p(2) == evalpoly(2, [10,2,3]) # reversed coefficients +``` + +[^1]: Explanation of the Horner schema can be found on [https://en.wikipedia.org/wiki/Horner%27s\_method](https://en.wikipedia.org/wiki/Horner%27s_method). +```@raw html +
+
+Solution:

+``` + +```@repl lab07_poly +using InteractiveUtils #hide +macro poly(a...) + return _poly(a...) +end + +function _poly(a...) + N = length(a) + ex = :($(a[1])) + for i in 2:N + ex = :(muladd(x, $ex, $(a[i]))) # equivalent of :(x * $ex + $(a[i])) + end + :(x -> $ex) +end + +p = @poly 3 2 10 +p(2) == evalpoly(2, [10,2,3]) +@code_lowered p(2) # can show the generated code +``` + +```@raw html +

+``` + +Moving on to the first/harder case, where we need to parse the mathematical expression. + +```@raw html +
+
Exercise
+
+``` +Create macro `@poly` that takes two arguments first one being the independent variable and second one being the polynomial written in mathematical notation. As in the previous case this macro should define an anonymous function that constructs the unrolled code. +```julia +julia> p = @poly x 3x^2+2x^1+10x^0 # the first argument being the independent variable to match +``` + +**HINTS**: +- though in general we should be prepared for some edge cases, assume that we are really strict with the syntax allowed (e.g. we really require spelling out x^0, even though it is mathematically equivalent to `1`) +- reuse the `_poly` function from the previous exercise +- use the `MacroTools.jl` to match/capture `a_*$v^(n_)`, where `v` is the symbol of independent variable, this is going to be useful in the following steps + 1. get maximal rank of the polynomial + 2. get coefficient for each power + +!!! note "`MacroTools.jl`" + Though not the most intuitive, [`MacroTools.jl`](https://fluxml.ai/MacroTools.jl/stable/) pkg help us with writing custom macros. We will use two utilities + #### `@capture` + This macro is used to match a pattern in a *single* expression and return values of particular spots. For example + ```julia + julia> using MacroTools + julia> @capture(:[1, 2, 3, 4, 5, 6, 7], [1, a_, 3, b__, c_]) + true + + julia> a, b, c + (2,[4,5,6],7) + ``` + #### `postwalk`/`prewalk` + In order to extend `@capture` to more complicated expression trees, we can used either `postwalk` or `prewalk` to walk the AST and match expression along the way. For example + ```julia + julia> using MacroTools: prewalk, postwalk + julia> ex = quote + x = f(y, g(z)) + return h(x) + end + + julia> postwalk(ex) do x + @capture(x, fun_(arg_)) && println("Function: ", fun, " with argument: ", arg) + x + end; + Function: g with argument: z + Function: h with argument: x + ``` + Note that the `x` or the iteration is required, because by default postwalk/prewalk replaces currently read expression with the output of the body of `do` block. + +```@raw html +
+
+Solution:

+``` + +```@example lab07_poly +using MacroTools +using MacroTools: postwalk, prewalk + +macro poly(v::Symbol, p::Expr) + a = Tuple(reverse(_get_coeffs(v, p))) + return _poly(a...) +end + +function _max_rank(v, p) + mr = 0 + postwalk(p) do x + if @capture(x, a_*$v^(n_)) + mr = max(mr, n) + end + x + end + mr +end + +function _get_coeffs(v, p) + N = _max_rank(v, p) + 1 + coefficients = zeros(N) + postwalk(p) do x + if @capture(x, a_*$v^(n_)) + coefficients[n+1] = a + end + x + end + coefficients +end +``` +Let's test it. +```@repl lab07_poly +p = @poly x 3x^2+2x^1+10x^0 +p(2) == evalpoly(2, [10,2,3]) +@code_lowered p(2) # can show the generated code +``` + +```@raw html +

+``` + +## Ecosystem macros +There are at least two ways how we can make our life simpler when using our `Ecosystem` and `EcosystemCore` pkgs. Firstly, recall that in order to test our simulation we always had to write something like this: +```julia +function create_world() + n_grass = 500 + regrowth_time = 17.0 + + n_sheep = 100 + Δenergy_sheep = 5.0 + sheep_reproduce = 0.5 + sheep_foodprob = 0.4 + + n_wolves = 8 + Δenergy_wolf = 17.0 + wolf_reproduce = 0.03 + wolf_foodprob = 0.02 + + gs = [Grass(id, regrowth_time) for id in 1:n_grass]; + ss = [Sheep(id, 2*Δenergy_sheep, Δenergy_sheep, sheep_reproduce, sheep_foodprob) for id in n_grass+1:n_grass+n_sheep]; + ws = [Wolf(id, 2*Δenergy_wolf, Δenergy_wolf, wolf_reproduce, wolf_foodprob) for id in n_grass+n_sheep+1:n_grass+n_sheep+n_wolves]; + World(vcat(gs, ss, ws)) +end +world = create_world(); +``` +which includes the tedious process of defining the agent counts, their parameters and last but not least the unique id manipulation. As part of the [HW](@ref hw07) for this lecture you will be tasked to define a simple DSL, which can be used to define a world in a few lines. + +Secondly, the definition of a new `Animal` or `Plant`, that did not have any special behavior currently requires quite a bit of repetitive code. For example defining a new plant type `Broccoli` goes as follows +```julia +abstract type Broccoli <: PlantSpecies end +Base.show(io::IO,::Type{Broccoli}) = print(io,"🥦") + +EcosystemCore.eats(::Animal{Sheep},::Plant{Broccoli}) = true +``` + +and definition of a new animal like a `Rabbit` looks very similar +```julia +abstract type Rabbit <: AnimalSpecies end +Base.show(io::IO,::Type{Rabbit}) = print(io,"🐇") + +EcosystemCore.eats(::Animal{Rabbit},p::Plant{Grass}) = size(p) > 0 +EcosystemCore.eats(::Animal{Rabbit},p::Plant{Broccoli}) = size(p) > 0 +``` +In order to make this code "clearer" (depends on your preference) we will create two macros, which can be called at one place to construct all the relations. + +### New Animal/Plant definition +Our goal is to be able to define new plants and animal species, while having a clear idea about their relations. For this we have proposed the following macros/syntax: +```julia +@species Plant Broccoli 🥦 +@species Animal Rabbit 🐇 +@eats Rabbit [Grass => 0.5, Broccoli => 1.0, Mushroom => -1.0] +``` +Unfortunately the current version of `Ecosystem` and `EcosystemCore`, already contains some definitions of species such as `Sheep`, `Wolf` and `Mushroom`, which may collide with definitions during prototyping, therefore we have created a modified version of those pkgs, which will be provided in the lab. + +!!! note "Testing relations" + We can test the current definition with the following code that constructs "eating matrix" + ```julia + using Ecosystem + using Ecosystem.EcosystemCore + + function eating_matrix() + _init(ps::Type{<:PlantSpecies}) = ps(1, 10.0) + _init(as::Type{<:AnimalSpecies}) = as(1, 10.0, 1.0, 0.8, 0.7) + function _check(s1, s2) + try + if s1 !== s2 + EcosystemCore.eats(_init(s1), _init(s2)) ? "✅" : "❌" + else + return "❌" + end + catch e + if e isa MethodError + return "❔" + else + throw(e) + end + end + end + + animal_species = subtypes(AnimalSpecies) + plant_species = subtypes(PlantSpecies) + species = vcat(animal_species, plant_species) + em = [_check(s, ss) for (s,ss) in Iterators.product(animal_species, species)] + string.(hcat(["🌍", animal_species...], vcat(permutedims(species), em))) + end + eating_matrix() + 🌍 🐑 🐺 🌿 🍄 + 🐑 ❌ ❌ ✅ ✅ + 🐺 ✅ ❌ ❌ ❌ + ``` + +```@raw html +
+
Exercise
+
+``` +Based on the following example syntax, +```julia +@species Plant Broccoli 🥦 +@species Animal Rabbit 🐇 +``` +write macro `@species` inside `Ecosystem` pkg, which defines the abstract type, its show function and exports the type. For example `@species Plant Broccoli 🥦` should generate code: +```julia +abstract type Broccoli <: PlantSpecies end +Base.show(io::IO,::Type{Broccoli}) = print(io,"🥦") +export Broccoli +``` +Define first helper function `_species` to inspect the macro's output. This is indispensable, as we are defining new types/constants and thus we may otherwise encounter errors during repeated evaluation (though only if the type signature changed). +```julia +_species(:Plant, :Broccoli, :🥦) +_species(:Animal, :Rabbit, :🐇) +``` + +**HINTS**: +- use `QuoteNode` in the show function just like in the `@myshow` example +- escaping `esc` is needed for the returned in order to evaluate in the top most module (`Ecosystem`/`Main`) +- ideally these changes should be made inside the modified `Ecosystem` pkg provided in the lab (though not everything can be refreshed with `Revise`) - there is a file `ecosystem_macros.jl` just for this purpose +- multiple function definitions can be included into a `quote end` block +- interpolation works with any expression, e.g. `$(typ == :Animal ? AnimalSpecies : PlantSpecies)` + +**BONUS**: +Based on `@species` define also macros `@animal` and `@plant` with two arguments instead of three, where the species type is implicitly carried in the macro's name. + +```@raw html +
+
+Solution:

+``` +Macro `@species` +```julia +macro species(typ, name, icon) + esc(_species(typ, name, icon)) +end + +function _species(typ, name, icon) + quote + abstract type $name <: $(typ == :Animal ? AnimalSpecies : PlantSpecies) end + Base.show(io::IO, ::Type{$name}) = print(io, $(QuoteNode(icon))) + export $name + end +end + +_species(:Plant, :Broccoli, :🥦) +_species(:Animal, :Rabbit, :🐇) +``` + +And the bonus macros `@plant` and `@animal` +```julia +macro plant(name, icon) + return :(@species Plant $name $icon) +end + +macro animal(name, icon) + return :(@species Animal $name $icon) +end +``` + +```@raw html +

+``` + +The next exercise applies macros to the agents eating behavior. +```@raw html +
+
Exercise
+
+``` +Define macro `@eats` inside `Ecosystem` pkg that assigns particular species their eating habits via `eat!` and `eats` functions. The macro should process the following example syntax +```julia +@eats Rabbit [Grass => 0.5, Broccoli => 1.0], +``` +where `Grass => 0.5` defines the behavior of the `eat!` function. The coefficient is used here as a multiplier for the energy balance, in other words the `Rabbit` should get only `0.5` of energy for a piece of `Grass`. + +**HINTS**: +- ideally these changes should be made inside the modified `Ecosystem` pkg provided in the lab (though not everything can be refreshed with `Revise`) - there is a file `ecosystem_macros.jl` just for this purpose +- escaping `esc` is needed for the returned in order to evaluate in the top most module (`Ecosystem`/`Main`) +- you can create an empty `quote end` block with `code = Expr(:block)` and push new expressions into its `args` incrementally +- use dispatch to create specific code for the different combinations of agents eating other agents (there may be catch in that we have to first `eval` the symbols before calling in order to know if they are animals or plants) + +!!! note "Reminder of `EcosystemCore` `eat!` and `eats` functionality" + In order to define that an `Wolf` eats `Sheep`, we have to define two methods + ``` + EcosystemCore.eats(::Animal{Wolf}, ::Animal{Sheep}) = true + + function EcosystemCore.eat!(ae::Animal{Wolf}, af::Animal{Sheep}, w::World) + incr_energy!(ae, $(multiplier)*energy(af)*Δenergy(ae)) + kill_agent!(af, w) + end + ``` + In order to define that an `Sheep` eats `Grass`, we have to define two methods + ``` + EcosystemCore.eats(::Animal{Sheep}, p::Plant{Grass}) = size(p)>0 + + function EcosystemCore.eat!(a::Animal{Sheep}, p::Plant{Grass}, w::World) + incr_energy!(a, $(multiplier)*size(p)*Δenergy(a)) + p.size = 0 + end + ``` + +**BONUS**: +You can try running the simulation with the newly added agents. + +```@raw html +
+
+Solution:

+``` + +```julia +macro eats(species::Symbol, foodlist::Expr) + return esc(_eats(species, foodlist)) +end + + +function _generate_eat(eater::Type{<:AnimalSpecies}, food::Type{<:PlantSpecies}, multiplier) + quote + EcosystemCore.eats(::Animal{$(eater)}, p::Plant{$(food)}) = size(p)>0 + function EcosystemCore.eat!(a::Animal{$(eater)}, p::Plant{$(food)}, w::World) + incr_energy!(a, $(multiplier)*size(p)*Δenergy(a)) + p.size = 0 + end + end +end + +function _generate_eat(eater::Type{<:AnimalSpecies}, food::Type{<:AnimalSpecies}, multiplier) + quote + EcosystemCore.eats(::Animal{$(eater)}, ::Animal{$(food)}) = true + function EcosystemCore.eat!(ae::Animal{$(eater)}, af::Animal{$(food)}, w::World) + incr_energy!(ae, $(multiplier)*energy(af)*Δenergy(ae)) + kill_agent!(af, w) + end + end +end + +_parse_eats(ex) = Dict(arg.args[2] => arg.args[3] for arg in ex.args if arg.head == :call && arg.args[1] == :(=>)) + +function _eats(species, foodlist) + cfg = _parse_eats(foodlist) + code = Expr(:block) + for (k,v) in cfg + push!(code.args, _generate_eat(eval(species), eval(k), v)) + end + code +end + +species = :Rabbit +foodlist = :([Grass => 0.5, Broccoli => 1.0]) +_eats(species, foodlist) +``` + +```@raw html +

+``` + +--- +## Resources +- macros in Julia [documentation](https://docs.julialang.org/en/v1/manual/metaprogramming/#man-macros) + +### `Type{T}` type selectors +We have used `::Type{T}` signature[^2] at few places in the `Ecosystem` family of packages (and it will be helpful in the HW as well), such as in the `show` methods +```julia +Base.show(io::IO,::Type{World}) = print(io,"🌍") +``` +This particular example defines a method where the second argument is the `World` type itself and not an instance of a `World` type. As a result we are able to dispatch on specific types as values. + +Furthermore we can use subtyping operator to match all types in a hierarchy, e.g. `::Type{<:AnimalSpecies}` matches all animal species + +[^2]: [https://docs.julialang.org/en/v1/manual/types/#man-typet-type](https://docs.julialang.org/en/v1/manual/types/#man-typet-type) diff --git a/docs/src/lecture_07/lecture.md b/docs/src/lecture_07/lecture.md new file mode 100644 index 00000000..c365fed3 --- /dev/null +++ b/docs/src/lecture_07/lecture.md @@ -0,0 +1,735 @@ +# [Macros](@id macro_lecture) +What is macro? +In its essence, macro is a function, which +1. takes as an input an expression (parsed input) +2. modify the expressions in argument +3. insert the modified expression at the same place as the one that is parsed. + +Macros are necessary because they execute after the code is parsed (2nd step in conversion of source code to binary as described in last lect, after `Meta.parse`) therefore, macros allow the programmer to generate and include fragments of customized code before the full program is compiled run. **Since they are executed during parsing, they do not have access to the values of their arguments, but only to their syntax**. + +To illustrate the difference, consider the following example: + +A very convenient and highly recommended ways to write macros is to write functions modifying the `Expr`ession and then call that function in the macro. Let's demonstrate on an example, where every occurrence of `sin` is replaced by `cos`. +We defined the function recursively traversing the AST and performing the substitution +```julia +replace_sin(x::Symbol) = x == :sin ? :cos : x +replace_sin(e::Expr) = Expr(e.head, map(replace_sin, e.args)...) +replace_sin(u) = u +``` +and then we define the macro +```julia +macro replace_sin(ex) + replace_sin(esc(ex)) +end + +@replace_sin(cosp1(x) = 1 + sin(x)) +cosp1(1) == 1 + cos(1) +``` +notice the following +- the definition of the macro is similar to the definition of the function with the exception that instead of the keyword `function` we use keyword `macro` +- when calling the macro, we signal to the compiler our intention by prepending the name of the macro with `@`. +- the macro receives the expression(s) as the argument instead of the evaluated argument and also returns an expression that is placed on the position where the macro has been called +- when you are using macro, you should be as a user aware that the code you are entering can be arbitrarily modified and you can receive something completely different. This meanst that `@` should also serve as a warning that you are leaving Julia's syntax. In practice, it make sense to make things akin to how they are done in Julia or to write Domain Specific Language with syntax familiar in that domain. +Inspecting the lowered code +```julia +Meta.@lower @replace_sin( 1 + sin(x)) +``` +We obeserve that there is no trace of macro in lowered code (compare to `Meta.@lower 1 + cos(x)`, which demonstrates that the macro has been expanded after the code has been parsed but before it has been lowered. In this sense macros are indispensible, as you cannot replace them simply by the combination of `Meta.parse` end `eval`. You might object that in the above example it is possible, which is true, but only because the effect of the macro is in the global scope. +```julia +ex = Meta.parse("cosp1(x) = 1 + sin(x)") +ex = replace_sin(ex) +eval(ex) +``` +The following example cannot be achieved by the same trick, as the output of the macro modifies just the body of the function +```julia +function cosp2(x) + @replace_sin 2 + sin(x) +end +cosp2(1) ≈ (2 + cos(1)) +``` +This is not possible +```julia +function parse_eval_cosp2(x) + ex = Meta.parse("2 + sin(x)") + ex = replace_sin(ex) + eval(ex) +end +``` +as can be seen from +```julia +julia> @code_lowered cosp2(1) +CodeInfo( +1 ─ %1 = Main.cos(x) +│ %2 = 2 + %1 +└── return %2 +) + +julia> @code_lowered parse_eval_cosp2(1) +CodeInfo( +1 ─ %1 = Base.getproperty(Main.Meta, :parse) +│ ex = (%1)("2 + sin(x)") +│ ex = Main.replace_sin(ex) +│ %4 = Main.eval(ex) +└── return %4 +) +``` + +!!! info + ### Scope of eval + `eval` function is always evaluated in the global scope of the `Module` in which the macro is called (note that there is that by default you operate in the `Main` module). Moreover, `eval` takes effect **after** the function has been has been executed. This can be demonstrated as + ```julia + add1(x) = x + 1 + function redefine_add(x) + eval(:(add1(x) = x - 1)) + add1(x) + end + julia> redefine_add(1) + 2 + + julia> redefine_add(1) + 0 + + ``` + +Macros are quite tricky to debug. Macro `@macroexpand` allows to observe the expansion of macros. Observe the effect as +```julia +@macroexpand @replace_sin(cosp1(x) = 1 + sin(x)) +``` + +## What goes under the hood of macro expansion? +Let's consider that the compiler is compiling +```julia +function cosp2(x) + @replace_sin 2 + sin(x) +end +``` + +First, Julia parses the code into the AST as +```julia +ex = Meta.parse(""" + function cosp2(x) + @replace_sin 2 + sin(x) +end +""") |> Base.remove_linenums! +dump(ex) +``` +We observe that there is a macrocall in the AST, which means that Julia will expand the macro and put it in place +```julia +ex.args[2].args[1].head # the location of the macrocall +ex.args[2].args[1].args[1] # which macro to call +ex.args[2].args[1].args[2] # line number +ex.args[2].args[1].args[3] # on which expression +``` +We can manullay run `replace_sin` and insert it back on the relevant sub-part of the sub-tree +```julia +ex.args[2].args[1] = replace_sin(ex.args[2].args[1].args[3]) +ex |> dump +``` +now, `ex` contains the expanded macro and we can see that it correctly defines the function +```julia +eval(ex) +``` +## Calling macros +Macros can be called without parentheses +```julia +macro showarg(ex) + println("single argument version") + @show ex + ex +end +@showarg(1 + 1) +@showarg 1 + 1 +``` +Macros use the very same multiple dispatch as functions, which allows to specialize macro calls +```julia +macro showarg(x1, x2::Symbol) + println("two argument version, second is Symbol") + @show x1 + @show x2 + x1 +end +macro showarg(x1, x2::Expr) + println("two argument version, second is Expr") + @show x1 + @show x2 + x1 +end +@showarg(1 + 1, x) +@showarg(1 + 1, 1 + 3) +@showarg 1 + 1, 1 + 3 +@showarg 1 + 1 1 + 3 +``` +(the `@showarg(1 + 1, :x)` raises an error, since `:(:x)` is of Type `QuoteNode`). + + +Observe that macro dispatch is based on the types of AST that are handed to the macro, not the types that the AST evaluates to at runtime. + +## [Notes on quotation](@id lec7_quotation) +In the previous lecture we have seen that we can *quote a block of code*, which tells the compiler to treat the input as a data and parse it. We have talked about three ways of quoting code. +1. `:(quoted code)` +2. `Meta.parse(input_string)` +3. `quote ... end` +The truth is that Julia does not do full quotation, but a *quasiquotation* as it allows you to **interpolate** expressions inside the quoted code using `$` symbol similar to the string. This is handy, as sometimes, when we want to insert into the quoted code an result of some computation / preprocessing. +Observe the following difference in returned code +```julia +a = 5 +:(x = a) +:(x = $(a)) +let y = :x + :(1 + y), :(1 + $y) +end +``` +In contrast to the behavior of `:()` (or `quote ... end`, true quotation would not perform interpolation where unary `$` occurs. Instead, we would capture the syntax that describes interpolation and produce something like the following: +```julia +( + :(1 + x), # Quasiquotation + Expr(:call, :+, 1, Expr(:$, :x)), # True quotation +) +``` + +```jula +for (v, f) in [(:sin, :foo_sin)] + quote + $(f)(x) = $(v)(x) + end |> dump +end +``` + +When we need true quoting, i.e. we need something to stay quoted, we can use `QuoteNode` as +```julia +macro true_quote(e) + QuoteNode(e) +end +let y = :x + ( + @true_quote(1 + $y), + :(1 + $y), + ) +end +``` +At first glance, `QuoteNode` wrapper seems to be useless. But `QuoteNode` has clear value when it's used inside a macro to indicate that something should stay quoted even after the macro finishes its work. Also notice that the expression received by macro are quoted, not quasiquoted, since in the latter case `$y` would be replaced. We can demonstate it using the `@showarg` macro introduced earlier, as +```julia +@showarg(1 + $x) +``` +The error is raised after the macro was evaluated and the output has been inserted to parsed AST. + +!!! info + Some macros like `@eval` (recall last example) + ```julia + for f in [:setindex!, :getindex, :size, :length] + @eval $(f)(A::MyMatrix, args...) = $(f)(A.x, args...) + end + ``` + or `@benchmark` support interpolation of values. This interpolation needs to be handled by the logic of the macro and is not automatically handled by Julia language. + +Macros do not know about runtime values, they only know about syntax trees. When a macro receives an expression with a $x in it, it can't interpolate the value of x into the syntax tree because it reads the syntax tree before `x` ever has a value! + +Instead, when a macro is given an expression with $ in it, it assumes you're going to give your own meaning to $x. In the case of BenchmarkTools.jl they return code that has to wait until runtime to receive the value of x and then splice that value into an expression which is evaluated and benchmarked. Nowhere in the actual body of the macro do they have access to the value of x though. + + +!!! info + ### Why `$` for interpolation? + The `$` string for interpolation was used as it identifies the interpolation inside the string and inside the command. For example + ```julia + a = 5 + s = "a = $(a)" + typoef(s) + println(s) + filename = "/tmp/test_of_interpolation" + run(`touch $(filename)`) + ``` + +## [Macro hygiene](@id lec7_hygiene) +Macro hygiene is a term coined in 1986. The problem it addresses is following: if you're automatically generating code, it's possible that you will introduce variable names in your generated code that will clash with existing variable names in the scope in which a macro is called. These clashes might cause your generated code to read from or write to variables that you should not be interacting with. A macro is hygienic when it does not interact with existing variables, which means that when macro is evaluated, it should not have any effect on the surrounding code. + +By default, all macros in Julia are hygienic which means that variables introduced in the macro have automatically generated names, where Julia ensures they will not collide with user's variable. These variables are created by `gensym` function / macro. + +!!! info + ### gensym + + `gensym([tag])` Generates a symbol which will not conflict with other variable names. + ```julia + julia> gensym("hello") + Symbol("##hello#257") + ``` + +Let's demonstrate it on our own version of an macro `@elapsed` which will return the time that was needed to evaluate the block of code. +```julia +macro tooclean_elapsed(ex) + quote + tstart = time() + $(ex) + time() - tstart + end +end + +fib(n) = n <= 1 ? n : fib(n-1) + fib(n - 2) +let + tstart = "should not change the value and type" + t = @tooclean_elapsed r = fib(10) + println("the evaluation of fib took ", t, "s and result is ", r) + @show tstart +end +``` +We see that variable `r` has not been assigned during the evaluation of macro. We have also used `let` block in orders not to define any variables in the global scope. +Why is that? +Let's observe how the macro was expanded +```julia +julia> Base.remove_linenums!(@macroexpand @tooclean_elapsed r = fib(10)) +quote + var"#12#tstart" = Main.time() + var"#13#r" = Main.fib(10) + Main.time() - var"#12#tstart" +end +``` +We see that `tstart` in the macro definition was replaced by `var"#12#tstart"`, which is a name generated by Julia's gensym to prevent conflict. The same happens to `r`, which was replaced by `var"#13#r"`. This names are the result of Julia's hygiene-enforcing pass, which is intended to prevent us from overwriting existing variables during macro expansion. This pass usually makes our macros safer, but it is also a source of confusion because it introduces a gap between the expressions we generate and the expressions that end up in the resulting source code. Notice that in the case of `tstart`, we actually wanted to replace `tstart` with a unique name, such that if we by a bad luck define `tstart` in our code, it would not be affected, as we can see in this example. +```julia +let + tstart = "should not change the value and type " + t = @tooclean_elapsed r = fib(10) + println(tstart, " ", typeof(tstart)) +end +``` +But in the second case, we would actually very much like the variable `r` to retain its name, such that we can accesss the results (and also, `ex` can access and change other local variables). Julia offer a way to `escape` from the hygienic mode, which means that the variables will be used and passed as-is. Notice the effect if we escape just the expression `ex` +```julia +macro justright_elapsed(ex) + quote + tstart = time() + $(esc(ex)) + time() - tstart + end +end + +let + tstart = "should not change the value and type " + t = @justright_elapsed r = fib(10) + println("the evaluation of fib took ", t, "s and result is ", r) + println(tstart, " ", typeof(tstart)) +end +``` +which now works as intended. We can inspect the output again using `@macroexpand` +```julia +julia> Base.remove_linenums!(@macroexpand @justright_elapsed r = fib(10)) +quote + var"#19#tstart" = Main.time() + r = fib(10) + Main.time() - var"#19#tstart" +end +``` +and compare it to `Base.remove_linenums!(@macroexpand @justright_elapsed r = fib(10))`. We see that the expression `ex` has its symbols intact. To use the escaping / hygience correctly, you need to have a good understanding how the macro evaluation works and what is needed. Let's now try the third version of the macro, where we escape everything as +```julia +macro toodirty_elapsed(ex) + ex = quote + tstart = time() + $(ex) + time() - tstart + end + esc(ex) +end + +let + tstart = "should not change the value and type " + t = @toodirty_elapsed r = fib(10) + println("the evaluation of fib took ", t, "s and result is ", r) + println(tstart, " ", typeof(tstart)) +end +``` +Using `@macroexpand` we observe that `@toodirty_elapsed` does not have any trace of hygiene. +```julia +julia> Base.remove_linenums!(@macroexpand @toodirty_elapsed r = fib(10)) +quote + tstart = time() + r = fib(10) + time() - tstart +end +``` +From the above we can also see that hygiene-pass occurs after the macro has been applied but before the code is lowered. `esc` is inserted to AST as a special node `Expr(:escape,...),` which can be seen from the follows. +```julia +julia> esc(:x) +:($(Expr(:escape, :x))) +``` +The definition in `essentials.jl:480` is pretty simple as `esc(@nospecialize(e)) = Expr(:escape, e)`, but it does not tell anything about the actual implementation, which is hidden probably in the macro-expanding logic. + +With that in mind, we can now understand our original example with `@replace_sin`. Recall that we have defined it as +```julia +macro replace_sin(ex) + replace_sin(esc(ex)) +end +``` +where the escaping `replace_sin(esc(ex))` in communicates to compiler that `ex` should be used as without hygienating the `ex`. Indeed, if we lower it +```julia +function cosp2(x) + @replace_sin 2 + sin(x) +end + +julia> @code_lowered(cosp2(1.0)) +CodeInfo( +1 ─ %1 = Main.cos(x) +│ %2 = 2 + %1 +└── return %2 +) +``` +we see it works as intended. Whereas if we use hygienic version +```julia +macro hygienic_replace_sin(ex) + replace_sin(ex) +end + +function hcosp2(x) + @hygienic_replace_sin 2 + sin(x) +end + +julia> @code_lowered(hcosp2(1.0)) +CodeInfo( +1 ─ %1 = Main.cos(Main.x) +│ %2 = 2 + %1 +└── return %2 +) +``` + + +### Why hygienating the function calls? + +```julia +function foo(x) + cos(x) = exp(x) + @replace_sin 1 + sin(x) +end + +foo(1.0) ≈ 1 + exp(1.0) + +function foo2(x) + cos(x) = exp(x) + @hygienic_replace_sin 1 + sin(x) +end + +x = 1.0 +foo2(1.0) ≈ 1 + cos(1.0) +``` + +### Can I do the hygiene by myself? +Yes, it is by some considered to be much simpler (and safer) then to understand, how macro hygiene works. +```julia +macro manual_elapsed(ex) + x = gensym() + esc(quote + $(x) = time() + $(ex) + time() - $(x) + end + ) +end + +let + t = @manual_elapsed r = fib(10) + println("the evaluation of fib took ", t, "s and result is ", r) +end + +``` + +## How macros compose? +```julia +macro m1(ex) + println("m1: ") + dump(ex) + ex +end + +macro m2(ex) + println("m2: ") + dump(ex) + esc(ex) +end + +@m1 @m2 1 + sin(1) +``` +which means that macros are expanded in the order from the outer most to inner most, which is exactly the other way around than functions. +``` +@macroexpand @m1 @m2 1 + sin(1) +``` +also notice that the escaping is only partial (running `@macroexpand @m2 @m1 1 + sin(1)` would not change the results). + +## Write @exfiltrate macro +Since Julia's debugger is a complicated story, people have been looking for tools, which would simplify the debugging. One of them is a macro `@exfiltrate`, which copies all variables in a given scope to a afe place, from where they can be collected later on. This helps you in evaluating the function. F + +Whyle a full implementation is provided in package [`Infiltrator.jl`](https://github.com/JuliaDebug/Infiltrator.jl), we can implement such functionality by outselves. +- We collect names and values of variables in a given scope using the macro `Base.@locals` +- We store variables in some global variable in some module, such that we have one place from which we can retrieve them and we are certain that this storage would not interact with any existing code. +- If the `@exfiltrate` should be easy, ideally called without parameters, it has to be implemented as a macro to supply the relevant variables to be stored. + +```julia +module Exfiltrator + +const environment = Dict{Symbol, Any}() + +function copy_variables!(d::Dict) + foreach(k -> delete!(environment, k), keys(environment)) + for (k, v) in d + environment[k] = v + end +end + +macro exfiltrate() + v = gensym(:vars) + quote + $(v) = $(esc((Expr(:locals)))) + copy_variables!($(v)) + end +end + +end +``` + +Test it to +```julia +using Main.Exfiltrator: @exfiltrate +let + x,y,z = 1,"hello", (a = "1", b = "b") + @exfiltrate +end + +Exfiltrator.environment + +function inside_function() + a,b,c = 1,2,3 + @exfiltrate +end + +inside_function() + +Exfiltrator.environment + +function a() + a = 1 + @exfiltrate +end + +function b() + b = 1 + a() +end +function c() + c = 1 + b() +end + +c() +Exfiltrator.environment +``` + +## Domain Specific Languages (DSL) +Macros are convenient for writing domain specific languages, which are languages designed for specific domain. This allows them to simplify notation and / or make the notation familiar for people working in the field. For example in `Turing.jl`, the model +of coinflips can be specified as +``` +@model function coinflip(y) + + # Our prior belief about the probability of heads in a coin. + p ~ Beta(1, 1) + + # The number of observations. + N = length(y) + for n in 1:N + # Heads or tails of a coin are drawn from a Bernoulli distribution. + y[n] ~ Bernoulli(p) + end +end; +``` +which resembles, but not copy Julia's syntax due to the use of `~`. A similar DSLs can be seen in `ModelingToolkit.jl` for differential equations, in `Soss.jl` again for expressing probability problems, in `Metatheory.jl` / `SymbolicUtils.jl` for defining rules on elements of algebras, or `JuMP.jl` for specific mathematical programs. + +One of the reasons for popularity of DSLs is that macro system is very helpful in their implementation, but it also contraints the DSL, as it has to be parseable by Julia's parser. This is a tremendous helps, because one does not have to care about how to parse numbers, strings, parenthesess, functions, etc. (recall the last lecture about replacing occurences of `i` variable). + +Let's jump into the first example adapted from [John Myles White's howto](https://github.com/johnmyleswhite/julia_tutorials/blob/master/From%20Macros%20to%20DSLs%20in%20Julia%20-%20Part%202%20-%20DSLs.ipynb). +We would like to write a macro, which allows us to define graph in `Graphs.jl` just by defining edges. +```julia +@graph begin + 1 -> 2 + 2 -> 3 + 3 -> 1 +end +``` +The above should expand to +```julia +using Graphs +g = DiGraph(3) +add_edge!(g, 1,2) +add_edge!(g, 2,3) +add_edge!(g, 3,1) +g +``` +Let's start with easy and observe, how +```julia +ex = Meta.parse(""" +begin + 1 -> 2 + 2 -> 3 + 3 -> 1 +end +""") +ex = Base.remove_linenums!(ex) +``` +is parsed to +```julia +quote + 1->begin + 2 + end + 2->begin + 3 + end + 3->begin + 1 + end +end +``` +We see that +- the sequence of statements is parsed to `block` (we know that from last lecture). +- `->` is parsed to `->`, i.e. `ex.args[1].head == :->` with parameters being the first vertex `ex.args[1].args[1] == 1` and the second vertex is quoted to `ex.args[1].args[2].head == :block`. + +The main job will be done in the function `parse_edge`, which will parse one edge. It will check that the node defines edge (otherwise, it will return nothing, which will be filtered out) +```julia +function parse_edge(ex) + #checking the syntax + !hasproperty(ex, :head) && return(nothing) + !hasproperty(ex, :args) && return(nothing) + ex.head != :-> && return(nothing) + length(ex.args) != 2 && return(nothing) + !hasproperty(ex.args[2], :head) && return(nothing) + ex.args[2].head != :block && length(ex.args[2].args) == 1 && return(nothing) + + #ready to go + src = ex.args[1] + @assert src isa Integer + dst = ex.args[2].args[1] + @assert dst isa Integer + :(add_edge!(g, $(src), $(dst))) +end + +function parse_graph(ex) + @assert ex.head == :block + ex = Base.remove_linenums!(ex) + edges = filter(!isnothing, parse_edge.(ex.args)) + n = maximum(e -> maximum(e.args[3:4]), edges) + quote + g = Graphs.DiGraph($(n)) + $(edges...) + g + end +end +``` +Once we have the first version, let's make everything hygienic + +```julia +function parse_edge(g, ex::Expr) + #checking the syntax + ex.head != :-> && return(nothing) + length(ex.args) != 2 && return(nothing) + !hasproperty(ex.args[2], :head) && return(nothing) + ex.args[2].head != :block && length(ex.args[2].args) == 1 && return(nothing) + + #ready to go + src = ex.args[1] + @assert src isa Integer + dst = ex.args[2].args[1] + @assert dst isa Integer + :(add_edge!($(g), $(src), $(dst))) +end +parse_edge(g, ex) = nothing + +function parse_graph(ex) + @assert ex.head == :block + g = gensym(:graph) + ex = Base.remove_linenums!(ex) + edges = filter(!isnothing, parse_edge.(g, ex.args)) + n = maximum(e -> maximum(e.args[3:4]), edges) + quote + $(g) = Graphs.DiGraph($(n)) + $(edges...) + $(g) + end +end +``` +and we are ready to go +```julia +macro graph(ex) + parse_graph(ex) +end + +@graph begin + 1 -> 2 + 2 -> 3 + 3 -> 1 +end +``` +and we can check the output with `@macroexpand`. +```julia +julia> @macroexpand @graph begin + 1 -> 2 + 2 -> 3 + 3 -> 1 + end +quote + #= REPL[173]:8 =# + var"#27###graph#273" = (Main.Graphs).DiGraph(3) + #= REPL[173]:9 =# + Main.add_edge!(var"#27###graph#273", 1, 2) + Main.add_edge!(var"#27###graph#273", 2, 3) + Main.add_edge!(var"#27###graph#273", 3, 1) + #= REPL[173]:10 =# + var"#27###graph#273" +end +``` + +## non-standard string literals +Julia allows to customize parsing of strings. For example we can define regexp matcher as +`r"^\s*(?:#|$)"`, i.e. using the usual string notation prepended by the string `r`. + +You can define these "parsers" by yourself using the macro definition with suffix `_str` +```julia +macro debug_str(p) + @show p + p +end +``` +by invoking it +```julia +debug"hello" +``` +we see that the string macro receives string as an argument. + +Why are they useful? Sometimes, we want to use syntax which is not compatible with Julia's parser. For example `IntervalArithmetics.jl` allows to define an interval open only from one side, for example `[a, b)`, which is something that Julia's parser would not like much. String macro solves this problem by letting you to write the parser by your own. + +```julia +struct Interval{T} + left::T + right::T + left_open::Bool + right_open::Bool +end + +function Interval(s::String) + s[1] == '(' || s[1] == '[' || error("left nterval can be only [,(") + s[end] == ')' || s[end] == ']' || error("left nterval can be only ],)") + left_open = s[1] == '(' ? true : false + right_open = s[end] == ')' ? true : false + ss = parse.(Float64, split(s[2:end-1],",")) + length(ss) != 2 && error("interval should have two numbers separated by ','") + Interval(ss..., left_open, right_open) +end + +function Base.show(io::IO, r::Interval) + lb = r.left_open ? "(" : "[" + rb = r.right_open ? ")" : "]" + print(io, lb,r.left,",",r.right,rb) +end +``` +We can check it does the job by trying `Interval("[1,2)")`. +Finally, we define a string macro as +```julia +macro int_str(s) + Interval(s) +end +``` +which allows us to define interval as `int"[1,2)"`. + +## Sources +- Great discussion on [evaluation of macros](https://discourse.julialang.org/t/interpolation-in-macro-calls/25530). diff --git a/docs/src/lecture_07/macros.md b/docs/src/lecture_07/macros.md new file mode 100644 index 00000000..19921943 --- /dev/null +++ b/docs/src/lecture_07/macros.md @@ -0,0 +1 @@ +# Macros