I've started a loose port of Ruby Koans<https://github.com/edgecase/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