“It is dangerous to be right in matters on which the established authorities are wrong.”
- Voltaire, The Age of Louis XIV
Forbidden library that contains macros for conditional compilation of Elixir code.
Currently, this package is only available on GitHub.
While a library with such a heretical name as schism?
Suppose, fellow citizen, that your pious code contains a deeply nested stack of functions (as it should, for a function should do one thing and one thing only).
Your code might well end up like this:
defmodule MyLib.ModuleA do
def f(x) do
# ...
end
def g(x) do
# ...
y = f(x)
# ...
end
def h(x) do
# ...
y = g(x)
# ...
end
end
defmodule MyLib.ModuleB do
def w(x) do
# ...
y = ModuleA.f(x)
# ...
end
end
Now suppose further that your code is slow. Such code will be wasteful and consume valuable CPU cycles. Such waste is abhorrent, and naturally heretical in nature.
This is an unacceptable state of affairs! You must find the source of slowness, root it out with extreme prejudice and document it publicly so that such wasteful lines of code shall never know the light of day again.
You must profile your code, detect the problem and benchmark your functions to guarantee you've improved performance.
So far, the best options is to allow the Benchee to possess your code and document the performance improvements.
However, let's say you care about the performance of the function MyLib.ModuleB.w/1
defined above, and that you manage to fix a performance problem in the function MyLib.ModuleA.f/1
. You want to benchmark the performance of w/1
under the new conditions and compare it to the performance under the old conditions.
This would usually require you to compile the code, benchmark it and save the Benchee charts for future comparison. Then, you'd have to apply your changes, recompile, run the benchmark and "manually" compare the Benchee data.
This is far from ideal.
You'd like to be able to compare the before/after performance directly in the same chart.
One solution is to define an extra module such as MyLib.ModuleA__Temp
with the changes you want and compare the performance to the old module.
This usually require a lot of copy and paste, which is quite error prone
(for the nature of Man is to be imperfect, and imperfect we are), and if the function you want to test is in a different module you have to copy that module too.
Things need not to be so complex.
You don't need to copy and paste, and you don't need extra modules.
This might be a sign that your dogmatic code has gone stale under the yoke of the Official Truth.
You need to distance yourself from the dogmatic tyrants of the past.
You need... a Schism!
A schism will allow you to have conditional compilation on your modules, and will allow you to choose between the (so called) truthful dogma and several heresies, to pick the one with the most favorable performance characteristics.
“The world is kept alive only by heretics: the heretic Christ, the heretic Copernicus, the heretic Tolstoy. Our symbol of faith is heresy. (Tomorrow)”
- Yevgeny Zamyatin
Let us then abandon the obsolete orthodoxy and embrace the heretical ideas of change:
defmodule MyLib.ModuleA do
# Taint your code with the seeds of doubt and heresy
import Schism
schism "structs vs records" do
# boldly reafirm the dogma of the elixir:
dogma "structs are superior"
def f(x) do
# implementation of `f/1` that uses structs
end
end
# Spread the hateful screed that the old rusty records from Erlang
# might still have a place in code written today, despite their
# archaic and primitive nature
heresy "records are superior"
def f(x) do
# implementation of `f/1` that uses records
end
end
end
# The rest of the module remains the same at visual inspection,
# although it is now tainted by heresy...
# Through conditional compilation, the meaning of all this
# functions may be changed as they are corrupted
# by the heretical ideas that defy the dogma
def g(x) do
# ...
y = f(x)
# ...
end
def h(x) do
# ...
y = g(x)
# ...
end
end
defmodule MyLib.ModuleB do
# Now, when you call functions from MyLib.ModuleA,
# you'll be calling functions already corrupted by heresy.
def w(x) do
# ...
y = MyLib.ModuleA.f(x)
# ...
end
end
By default, the Schism.schism/2
macro will compile the dogma
branch
and discard the heresies
You can now write the following benchmark:
# benchmarks/structs_vs_records.exs
Benchee.run(%{
"structs are superior" => {
fn _input -> MyLib.ModuleB.w(666) end,
# Before running the benchmark, recompile the code according to the dogma
before_scenario: fn _input ->
Schism.convert(%{"structs vs records" => "structs are superior"})
end
},
"records are superior" => {
# The code is the same as above, but it's being run under different conditions...
fn _input -> MyLib.ModuleB.w(666) end,
# Before running the benchmark, recompile the code according to the heresy
# The same code will now have better or worse performance.
before_scenario: fn _input ->
Schism.convert(%{"structs vs records" => "records are superior"})
end
}
})
As usual, you can run the benchmark using:
mix run benchmarks/structs_vs_records.exs
And the mix task will print something like the following:
Operating System: Windows"
CPU Information: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
Number of Available Cores: 8
Available memory: 7.87 GB
Elixir 1.6.2
Erlang 20.0
Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 μs
parallel: 1
inputs: none specified
Estimated total run time: 14 s
Benchmarking records are superior...
Compiling 3 files (.ex)
Benchmarking structs are superior...
Compiling 3 files (.ex)
Name ips average deviation median 99th %
records are superior 4 250 ms ±0.00% 250 ms 250 ms
structs are superior 2.13 468.73 ms ±0.10% 469 ms 469 ms
Comparison:
records are superior 4
structs are superior 2.13 - 1.87x slower
What does this code do?
This code defines a before_scenario
hook for Benchee.
This is a function that should run before the benchmark.
In this case, the Schism.convert/1
function converts the code into the
"correct" set of beliefs for each schism.
This entails recompiling the code and picking the correct dogma or heresy
everywhere the schism
macro is used.
If no belief is specified for a given schism, the dogma
will be picked instead.
You can confirm from the logs above that the code has been compiled twice, as we would expect.
The Schism.convert/1
function depends on Mix, so it can only be used in development,
and not in production where Mix won't be available.
This doubles as a safety measure, as you most definitely don't want to conditionally
recompile your code at runtime in production.
Such action is heretical, and will be met with extreme disapproval from your peers!
For added safety in some very unlikely edge cases, the Schism.force_convert/1
function
may be used instead of Schism.convert/1
“I myself have read the writings and teachings of the heretics, polluting my soul for a while with their abominable notions, though deriving this benefit: I was able to refute them for myself and loathe them even more.”
- Eusebius, The Church History
The :schism
library can be used to make your code faster.
But you must ensure that the heresies you want to test are compatible with the dogma.
One way of doing this is by running the same test suites for the heresy and for the dogma.
The Schism.Testing
module provides the Schism.Testing.defsnipped
macro that reduces
the amount of boilerplate you need.
The use of this macro is best explained by example.
First, you define a snippet (don't forget to require
the Schism.Testing
module!)
require Schism.Testing
Schism.Testing.defsnippet StructsVsRecordsTestSnippet do
use ExUnit.Case, async: false
test "..." do
# ...
end
# ...
end
Then, you can use
the snippet inside your real testing modules:
defmodule StructsVsRecords.StructsAreSuperior do
use StructsVsRecordsTestSnippet,
conversions: %{"structs vs records" => "structs are superior"}
end
defmodule StructsVsRecords.RecordsAreSuperior do
use StructsVsRecordsTestSnippet,
conversions: %{"structs vs records" => "records are superior"}
end
The code above injects the code of the snippet inside your modules and
makes sure your project is converted to the right beliefs before the tests
in the module are run.
For this to work, the snippet injects a setup_all
macro that handles
the conversions when the tests start and reverts to the dogma when the tests stop.
If you need more control, simply omit the :conversions
option and invoke the
setup_all
macro yourself:
defmodule StructsVsRecords.RecordsAreSuperior do
use StructsVsRecordsTestSnippet
setup_all do
Schism.convert(%{"structs vs records" => "records are superior"}
# ... custom setup code ...
# After all tests are done, convert to the dogma
on_exit fn ->
# ... custom teardown code ...
Schism.convert_to_dogma()
end
end
end
Tests that use schism
can't be run with async: true
, because that will
break all of the guarantees schism
needs to work properly.
“History warns us ... that it is the customary fate of new truths to begin as heresies and to end as superstitions.”
- Thomas Henry Huxley, *Collected Essays of Thomas Henry Huxley *
The above looks more magical than it really is...
The defsnippet
macro is just a wrapper around defmodule
that defines
a __using__/2
macro so that the module can be used.
The __using__/2
macro is defined such that it splices the AST of the snippet
into the module it's invoked on (it also adds the setup_all
macro, as defined above).
If confused, just check the implementation, which is very simple.
Using schism
is safe in production, because the dogma
will be chosen every time
and the heresies will simply be discarded.
The conversion functions Schism.convert/2
, Schism.force_convert/2
,
Schism.convert_to_dogma/1
and Schism.force_convert_to_dogma/1
depend on Mix
and will fail if invoked in production.
The only drawback is that you're adding yet another dependency to your project.
Although schism
doesn't do much at runtime, you really require the schism
macro
for this to work in production.