How to speed up your Elixir compile times (part 2) — test your understanding!
This post gives a few examples to help you get your head around how the Elixir compiler deals with dependencies. Try your best to work out the answers without reading ahead — it’ll do wonders for your understanding! You can find the examples on Github. I also recommend reading part 1 of the series which explains some key details of how Elixir compilation works. Part 3 is now available which covers strategies for improving your compile times.
Challenges (solutions below)
1. Starting simple
If we edit a.ex
below which files will be recompiled?
# a.ex
defmodule A do
def a, do: "a"
end# b.ex
defmodule B do
def b, do: A.a()
end
2. Little harder…
Part a) If we edit a.ex
below which files will be recompiled?
Part b) If we edit b.ex
below which files will be recompiled?
# a.ex
defmodule A do
def a, do: "a"
end# b.ex
defmodule B do
@a A.a()
def b, do: @a
end
3. Keep that brain working…
If file a.ex
is changed which files get recompiled?
# a.ex
defmodule A do
def a, do: "a"
end# b.ex
defmodule B do
def b, do: A.a()
end# c.ex
defmodule C do
@b B.b()
def c, do: @b
end
4. Still with me?
If file b.ex
is changed which files will be recompiled?
# a.ex
defmodule A do
def a, do: "a"
end# b.ex
defmodule B do
def b, do: A.a()
end# c.ex
defmodule C do
def c, do: B.b()
end# d.ex
defmodule D do
@a A.a()
def d, do: @a
end
5. Let’s go round again…
Part a)
If file b.ex
is changed which files will be recompiled?
# a.ex
defmodule A do
def a, do: C.c()
end# b.ex
defmodule B do
def b, do: A.a()
end# c.ex
defmodule C do
def c, do: B.b()
end# d.ex
defmodule D do
@a A.a()
def d, do: @a
end
Part b)
Same question but with the following tweak to the files:
# a.ex
defmodule A do
def a, do: C5.c()
def a2, do: "a"
end# b.ex
defmodule B do
def b, do: A.a()
end# c.ex
defmodule C do
def c, do: B.b()
end# d.ex
defmodule D do
@a A.a2()
def d, do: @a
end
6. Let’s think about structs
# a.ex
defmodule A do
defstruct [:name, :age] def a, do: "a"
end#b.ex
defmodule B6 do
def b(%A{name: name}), do: name
end
Part a) If the field height
were added to the struct defined in a.ex
which file(s) would be recompiled?
Part b) If A.a()
were updated to return "b"
then which file(s) would be recompiled?
7. More structs…
If the field height
were added to the struct defined in a.ex
which file(s) would be recompiled?
# a.ex
defmodule A do
defstruct [:name, :age] def a, do: "a"
end#b.ex
defmodule B6 do
def b(%{name: name}), do: name
end
8. What about using import?
# a.ex
defmodule A do
def a, do: "a"
end#b.ex
defmodule B6 do
import A
def b, do: a()
end
Part a) Which files would be recompiled if change A.a()
so it takes 1 argument?
Part b) Which files would be recompiled if we add a new function foo
to the A
module?
Part c) Which files would be recompiled if we make a
return "a2"
instead of just returning "a"
?
9. And Macros?
# a.ex
defmodule A9 do
defmacro a(_clause), do: "X" def woo, do: "woo"
end# b.ex
defmodule B9 do
alias A9
require A9
def b, do: A9.a("some clause")
end
Part a) If we change the implementation of the a
macro which files will be recompiled?
Part b) If we change the implementation of the woo
function which files will be recompiled?
Solutions
Have you answered all of the above challenges? Check below to see how you got on!
- Only
a.ex
will be recompiled.b.ex
only has a runtime dependency ona.ex
, not a compile-time dependency - a)
a.ex
andb.ex
will both be recompiled becauseb.ex
has a compile-time dependency ona.ex
. Remember that any code outside of a method will be called at compile-time.
b) Onlyb.ex
will be recompiled — the dependency is only from b to a, not the other way around a.ex
will be recompiled because it was modified.c.ex
will be recompiled because it has a transitive compile-time dependency on C. Remember anything with a compile-time dependency on another file, will also have a transitive compile-time dependency on all the other files runtime dependencies- Only
b.ex
will be recompiled, no other files have a compile-time or transitive compile-time dependency on it! - Part a) This is a bit of a trick question. What will happen is the compiler will hang indefinitely in an infinite loop. At compile-time module
D
needs to callA.a()
, which callsC.c()
, which callsB.b()
which callsA.a()
, and so on in a never-ending loop.
Part b) Fixes this issue, so in that caseb.ex
andd.ex
will both be recompiled.d.ex
will be recompiled because it has a compile-time dependency ona.ex
. Becausea.ex
has a runtime dependency onc.ex
, andc.ex
depends onb.ex
,d.ex
therefore has transitive compile-time dependencies ona.ex
,b.ex
, andc.ex
. You may have noticed a, b, and c form a dependency cycle, which is something to avoid if you can! - Part a) Both
a.ex
andb.ex
would be recompiled —a.ex
because it has been modified.b.ex
because it depends on the struct froma.ex
and that struct has been modified.
Part b) Onlya.ex
would be recompiled. Becauseb.ex
has an export dependency (because it depends on the struct from A), it will need to be recompiled only if the struct itself changes. Other details ofA
can change and not impactB
at all - Only
a.ex
would be recompiled — asB
no longer references the struct fromA
it doesn’t have any dependencies onA
. - Part a)
a.ex
andb.ex
would both be recompiled. When you useimport
then if any of the function signatures (i.e. names of functions or the number of arguments) change then
Part b)a.ex
andb.ex
would both be recompiled. Again because we usedimport
any change in the function signatures (including adding a new function)
Part c) Onlya.ex
would be recompiled because only the implementation changed — the function signatures inA
didn’t change (all function names and numbers of arguments remained the same) - Part a)
b.ex
has a compile time dependency ona.ex
because it calls the macro froma.ex
, which is executed at compile-time
Part b) As with part a, both files will be recompiled. Remember compile-time dependencies are just known about at the file level, so even though we haven’t changed the macro itself,b.ex
still needs to be recompiled. Of course the compiled version ofb.ex
will be identical, but the compiler doesn’t know that
What next?
In the next final article in this series I’ll share strategies to help improve compile times. Also Multiverse is hiring so if you love working on interesting technical challenges and would like to help us build an outstanding alternative to university we’d love to hear from you!