How to conditionally define a local variable

Is there a way to conditionally define a local variable? I’m trying to generate some code where I want to define a local variable but only if the corresponding key to a hash exists. I tried the obvious way of doing it but apparently it does not work:


_locals = {
  organization: 'org',
  account: 'account'
}
# (account, organization, include_orgs) = _locals.values_at(:account, :organization, :include_orgs) #declares all local variables regardless of key value
if _locals.key? :account
  account = _locals[:account]
end
puts defined? include_orgs # returns nil

if _locals.key? :organization
  organization = _locals[:organization]
end
puts defined? include_orgs # returns nil

if _locals.key? :include_orgs
  include_orgs = _locals[:include_orgs]
end
puts defined? include_orgs # does NOT return nil

if defined? include_orgs
  puts include_orgs.inspect
end

# looks like local variables are created INSIDE the if block even if it does not go into the if

I’m working on something that rewrites jbuilders to (plain) ruby, and there’s a lot of existing code that uses defined? in their behavior. I’d like to know if there’s a clean way to conditionally define local variables so I don’t have to rewrite all calls to defined? in the code.

Sure, there is a way to conditionally define a local variable. However, within each ‘if’ statement, the local variable is only defined if the condition is met, i.e., the specified key exists in the hash. If the key does not exist, the local variable is not defined (as expected).

So, your issue with ‘include_orgs’ returning nil in the case it does not exist in the hash is correct. Here’s how you can do it:

_locals = {
  organization: 'org',
  account: 'account'
}

account = _locals[:account] if _locals.key?(:account)
organization = _locals[:organization] if _locals.key?(:organization)
include_orgs = _locals[:include_orgs] if _locals.key?(:include_orgs)

Then, when you want to use the variable, you can use ‘defined?’ to check the variable existence.

if defined?(include_orgs)
  puts include_orgs.inspect
end

This will only print the value of include_orgs if it was defined.

I tried that but it didn’t work either. Looks like the variable is still defined, it just has a nil value. If you try

_locals = {
  organization: 'org',
  account: 'account'
}

account = _locals[:account] if _locals.key?(:account)
organization = _locals[:organization] if _locals.key?(:organization)
include_orgs = _locals[:include_orgs] if _locals.key?(:include_orgs)

puts defined? include_orgs # prints 'local-variable'
if defined?(include_orgs) # is true
  puts include_orgs.inspect
end

it still considers include_orgs defined even if the key is not in the hash.

I’m using Ruby 3.0.1 btw.

The short answer is: NO, that is not possible.

The long answer is that I initially thought that surely it can be done, but then it turns out it can’t.

Ruby being so incredibly dynamic, my first thought was that I could just define it behind an if, just as you have tried. However, as you discovered, that doesn’t work, it’s defined and has a nil value, although the code defining it clearly didn’t run.

And actually, the official documentation has the explanation (From the official Ruby documentation, Assignment Syntax section):

The local variable is created when the parser encounters the assignment, not when the assignment occurs

Ruby is dynamic, but all that dynamism starts only after the parser is done! And since the parser is defining variables, nothing you do at runtime can prevent it being defined.

If you try turning to meta-programming with eval or Binding#local_variable_set you’ll discover that it also doesn’t work because they define variables in a child scope, not in the scope where they’re being executed.

And that’s really the crux of the issue: local variables’ scope is lexical, meaning, it’s defined by the actual layout of the code as it is written. That’s why it’s impossible to be dynamic about it.

If it was instead instance variables, then it would become very possible, since their scope is not lexical.

So, I’m afraid, unless someone can come up with some other trick I missed, there’s no way to avoid refactoring the calls to defined? in the code.

In your code, you’re checking if _locals.key? :account and then assigning a value to the account variable within the if block. This indeed defines the account variable only if the :account key exists in the _locals hash, but its scope is limited to that if block. This behavior is why defined? include_orgs returns nil outside of the if block.

If you want to make the account variable accessible outside of the if block, you should declare it before the if block. Here’s an example:
_locals = {
organization: ‘org’,
account: ‘account’
}

account = nil # Declare the variable with an initial value

if _locals.key? :account
account = _locals[:account]
end

puts defined? account # Will return ‘local-variable’ if :account key exists, otherwise it will return nil

This way, account is defined in the outer scope, and you can check its existence using defined? later in your code without any issues.

For your use case where you want to conditionally define local variables based on hash keys, you will need to declare them in the outer scope before the if blocks, as shown in the example above, to ensure they are accessible throughout the code.