I've started a loose port of
Ruby Koans to better understand the nuances of Elixir and share what I learn. While writing tests for try/catch/after I realized I couldn't update variables defined outside the try, catch, or after clauses.
test :affect_state_from_try_catch_after do
defrecord StateTracker, from_try: :default, from_catch: :default, from_after: :default
outside_state = StateTracker.new
try do
assert_equal :default, outside_state.from_try # passes
outside_state = outside_state.from_try(:new_value)
assert_equal :new_value, outside_state.from_try # passes
throw :something
catch: caught_value
assert_equal :default, outside_state.from_catch # passes
outside_state = outside_state.from_catch(:new_value)
assert_equal :new_value, outside_state.from_catch # passes
after:
assert_equal :default, outside_state.from_after # passes
outside_state = outside_state.from_after(:new_value)
assert_equal :new_value, outside_state.from_after # passes
end
assert_equal :new_value, outside_state.from_try # fails: nil
assert_equal :new_value, outside_state.from_catch # fails: nil
assert_equal :new_value, outside_state.from_after # fails: nil
end
Documentation for the try defmacro says variables created inside the try clause can't be accessed externally. What it doesn't address is reading or updating preexisting variables. Based on the test above, reading from a preexisting variable is no problem, but updates don't take hold outside of the try/catch/after.
We can still affect the state of the outer scope using the result of the try statement, but it requires writing try statements like what's shown below.
def some_function()
try_result = try do
do_something()
catch: caught_value
log_catch(caught_value)
{:thrown, caught_value}
after:
IO.puts "finished try/catch"
end
process_result(try_result)
end
I bring it up because many other languages permit changing the state surrounding the try statement from within all parts of the try statement. For example, here's a Ruby method that's similar to the Elixir test, except it behaves much differently:
def affect_state_from_begin_rescue_ensure_test
outside_state = {
:from_begin => nil,
:from_rescue => nil,
:from_ensure => nil
}
begin
outside_state[:from_begin] = :new_value
raise
rescue
outside_state[:from_rescue] = :new_value
ensure
outside_state[:from_ensure] = :new_value
end
puts "From begin: #{outside_state[:from_begin]}" # :new_value
puts "From rescue: #{outside_state[:from_rescue]}" # :new_value
puts "From ensure: #{outside_state[:from_ensure]}" # :new_value
end
Not being able to affect surrounding state from the after clause is particularly strange to me. Unlike the try and catch clauses, the after clause has no impact on the result of the overall try statement. Additionally, we can't write to variables in the surrounding scope that could impact the return value of the enclosing function. From what I've seen in examples and the Elixir codebase, the after clause has only been used to make system calls, such as interacting with the file system or writing to standard output Many other languages let the "finally" clause affect outer state and thereby affect the enclosing function's return value. Do we want that possibility in Elixir too?
It's not my intention to cause commotion over this. Don't hesitate to bring factors I haven't addressed or share your own take on the matter. I could easily be missing something, like relevant details about Erlang--definitely a newb here :)
Thanks!
-Jake