diff --git a/content/post/guards-guards.md b/content/post/guards-guards.md new file mode 100644 index 0000000..efa507a --- /dev/null +++ b/content/post/guards-guards.md @@ -0,0 +1,92 @@ ++++ +date = 2026-06-27 +title = "Guards! Guards!" +description = """ +Small gotcha about boolean operators in Elixir. +""" + +[taxonomies] +tags = [ + "beam" +] ++++ + +Let's start with simple quiz. + +--- + +Given module defined as: + +```elixir +defmodule Foo do + def a(x) when is_integer(x) or is_map_key(x, :foo), do: true + def a(x), do: false + + def b(x) when is_map_key(x, :foo) or is_integer(x), do: true + def b(x), do: false +end +``` + +Try to answer these questions. + +{% question( + name="a-map", + desc="What will be result of `Foo.a(%{foo: 21})`?", + options = ["`true`", "`false`"], + correct = 0 +) %} +This one is straightforward. + +We check guard, it has one condition `is_integer(x) or is_map_key(x, :foo)`. +First one returns `false`, second returns `true`, Boolean's alternative results +in `true` and first case is matched. +{% end %} + +{% question( + name="a-int", + desc="What will be result of `Foo.a(37)`?", + options = ["`true`", "`false`"], + correct = 0 +) %} +This one is straightforward as well. + +We check guard, it has one condition `is_integer(x) or is_map_key(x, :foo)`. +First one returns `true`, second one isn't fired at all, because `or` operator +is short circuiting. +{% end %} + +{% question( + name="b-map", + desc="What will be result of `Foo.b(%{foo: 21})`?", + options = ["`true`", "`false`"], + correct = 0 +) %} +Again, similar to the previous questions. + +We check guard, it has one condition `is_map_key(x, :foo) or is_integer(x)`. +First one returns `true` and the rest is short circuited. +{% end %} + +{% question( + name = "b-int", + desc = "What will be result of `Foo.b(37)`?", + options = ["`true`", "`false`"], + correct = 1 +) %} +Ouch, something changed… + +Again, we check guard, one condition `is_map_key(x, :foo) or is_integer(x)`. We +hit first clause `is_map_key(x, :foo)` and this **doesn't** return `false`, +instead it fail. Failure in one of guard functions isn't converted to `false` +but instead makes whole guard expression fail. This mean that `is_integer(x)` +part will **never** be called. +{% end %} + +--- + +This behaviour is often surprising for a lot of Elixir developers, as it +seemingly breaks commutative property of boolean operators. However, to be +honest, these never were commutative because of short circuiting. + +It seems that Elixir at the time of writing (Elixir 1.20.1, OTP 29) do not warn +about this issue. diff --git a/sass/_quiz.scss b/sass/_quiz.scss new file mode 100644 index 0000000..7e7ab50 --- /dev/null +++ b/sass/_quiz.scss @@ -0,0 +1,22 @@ +.question { + .answer { + display: none; + border: 1px solid pink; + padding: 1em; + margin: 2em 0; + + .right { display: none; color: lightgreen; font-weight: 500; font-size: larger; } + .wrong { display: block; color: pink; font-weight: 500; font-size: larger; } + } + + &:has(.option > input[type=radio]:checked) .answer { + display: block; + } + + &:has(.option.correct > input[type=radio]:checked) .answer { + border-color: lightgreen; + + .right { display: block; } + .wrong { display: none; } + } +} diff --git a/sass/style.scss b/sass/style.scss index b2e0093..4fa10a6 100644 --- a/sass/style.scss +++ b/sass/style.scss @@ -7,6 +7,7 @@ @import 'pagination'; @import 'footer'; @import 'rings'; +@import 'quiz'; @media print { @import 'print'; diff --git a/templates/shortcodes/question.html b/templates/shortcodes/question.html new file mode 100644 index 0000000..c8c52f9 --- /dev/null +++ b/templates/shortcodes/question.html @@ -0,0 +1,21 @@ +
Q: {{ desc | markdown(inline=true) | safe }}
+ + {% for option in options %} + {% set id = name ~ "-a-" ~ loop.index %} +