raytracer
Post conditions in Elixir
What do you think about pre and post condition checks in Elixir?
I’ve just started learning Elixir and can see how guards are similar to pre-condition checks. Is there anything that could be used for post-condition checks?
Last night I realised the pipe operator could be used (abused?) to help implement post-condition checks and wrote this marcro:
defmacro ensure input, pattern1, pattern2 do
quote do
case unquote(input) do
unquote(pattern1) -> unquote(input)
unquote(pattern2) -> unquote(input)
_ -> raise PostConditionError
end
end
end
It would be used like:
def is_food(input) do
case input do
"Cheese" -> true
"Beer" -> {:error, "Item is not food"}
"Phone" -> false
end
|> ensure(true, {:error, _reason})
end
A is_food("Phone") call would result in a PostConditionError being raised.
Thoughts?
Most Liked
LostKobrakai
You might be able to create a macro def/3, which allows for a syntax like
def is_foot(input) when is_binary(food) do
case input do
"Cheese" -> true
"Beer" -> {:error, "Item is not food"}
"Phone" -> false
end
after
result == true or match?({:error, _reason}, result)
end
christhekeele
Worth noting this is already valid syntax with very different implications: it creates an implicit try block around the function and ensures your code runs after it whether or not it raised; and does not allow accessing the result of the function (since there may be none since it could have raised).
Playing around with a couple of observations:
-
What you propose is still actually
def/2: the macro receives a call and a keyword list of blocks, so in our example the macro would receive:defmacro def(head, [do: body, after: postcheck]) -
Semantically what we want is actually closer to the
elseblock in atrythat matches on the result iff no error was raised, allowing for post-checks and result transformation:def is_foot(input) when is_binary(food) do case input do "Cheese" -> true "Beer" -> {:error, "Item is not food"} "Phone" -> false end else true -> true {:error, reason} -> {:error, {:not_foot, reason}} _ -> raise PostConditionError # possibly implicit, though traditionally not end -
You can’t make up your own block keywords, the parser only allows usage of the existing secondary-block terms after a
do:catch,rescue,afterandelse -
The existing implementation of
defwith an implicittryonly usescatch,rescue, andafter
Conclusion: We could make a custom def/2 macro that employs an else block and transforms the provided implementation into something traditionally allowed in Kernel.def/2, and else is already the only syntactically valid unused block keyword in Kernel.def/2.
Cons: def(head, do: body, else: postcheck) does not indicate intent nearly as well as def(head, do: body, after: postcheck). But such a macro would definitely be possible!
Alternatively:
-
Since block arguments must come last, the only form of
def/3the parser would recognize would be:defmacro def(head, postcheck, [do: body]) -
This isn’t that great though:
def is_foot(input) when is_binary(food), true or {:error, _reason} do case input do "Cheese" -> true "Beer" -> {:error, "Item is not food"} "Phone" -> false end end -
Interestingly we could employ a fictional
ensure/1function that the parser doesn’t choke on, to make the intent more legible:def is_foot(input) when is_binary(food), ensure true or {:error, _reason} do case input do "Cheese" -> true "Beer" -> {:error, "Item is not food"} "Phone" -> false end end
Cons: certain complicated expressions could make the parser decide it no longer likes this construct, it’s arguably even hackier though it reads well..








