Method definition precedence

When I have same name for a closure, macro & a function then I see closure takes precedence over macro over function. I do not get any compilation errors. Wondering if this is expected behavior with crystal 1.0.0 & LLVM 10.0.0 and if yes then any documentation would be helpful to understand. I assumed my code should have thrown some compilation errors.

  1 func = ->{ puts "Closure func" }
  2 
  3 macro func
  4   ->{ puts "Macro func" }
  5 end
  6 
  7 def func
  8  ->{ puts "Function func" }
  9 end
 10 
 11 func.call

Above code outputs,
Closure func

If I comment out line #1, then it outputs,
Macro func

If I comment out lines #1 to #5, then it outputs,
Function func

Few more examples,
When a function and variable has the same name then variable takes precedence.

  1 func : Int32 = 100
  2 puts func
  3 
  4 def func
  5   "function func"
  6 end
  7 
  8 puts func

Above code outputs

100
100

If I comment out line #1, then it outputs

function func
function func

Last example, when same function is defined multiple times then the last definition is being considered while I would have expected a compilation error. For comparison, if a same variable name is re-declared (of same or different type) then I do get variable already declared error which is what I would have expected for function name as well.

  1 def func
  2   "function func"
  3 end
  4 
  5 def func
  6   "function redefinition"
  7 end
  8 
  9 puts func

outputs function redefinition

I’m pretty sure all of these are working as expected. Something that may help the first and second example is that () can be supplied to force the compiler to invoke the macro/method, otherwise it’ll assume the value of the variable as you’ve seen. However I’m not sure this is explicitly noted anywhere.

E.g.

func : Int32 = 100

def func
  "function func"
end

puts func   # => 100
puts func() # => function func

The last example kinda relates to Methods and instance variables - Crystal. I remember seeing an issue about raising an error when more than 1 function with the same signature is defined. However I can’t seem to find it :/.

EDIT: I found it: [RFC] Compiler: error on method redefinition in same scope by asterite · Pull Request #10071 · crystal-lang/crystal · GitHub.

Thanks @Blacksmoke16 , yes the ticket you referred would fix the function redefinition I have posted in my last example.

While your comment on using ‘()’ for function works but still it is ambiguous IMHO. How would the user know if there was no closure or macro or variable by that name defined before? Else, users should be forced (via a compilation error) to always call a function with ‘()’, but I think this would not be liked by all.

Having said that, how about the following?

  1 func = ->{ puts "Closure func" }
  2 
  3 macro func
  4   ->{ puts "Macro func" }
  5 end
  6 
  7 def func
  8   ->{ puts "Function func" }
  9 end
 10 
 11 func.call   # => Closure func
 12 func().call # => Macro func

Here function func() is never called. IMHO, there should be compilation error for all these cases of same name being used by macro, function, closure or variable, else developers are going to find hard time troubleshooting if they have a very large code base. The current working behavior also implies before declaring a variable, function, closure or macro, the developer has to do a full check if anything by that name already exists, as this would not be caught by the compiler.

Yea this will probably never happen. If anything it would be better suited to a linter like Ameba if that would even be possible to lint.

If I had to guess I’d say that because macros are expanded at compile time, what actually is getting executed is more like:

func = ->{ puts "Closure func" }

def func
  ->{ puts "Function func" }
end

func.call                    # => Closure func
->{ puts "Macro func" }.call # => Macro func

I.e. the method func never gets a chance to be invoked anyway.

Only within the scope where the var/macro/method is defined. I.e. it’s unlikely a dev in a large project would have a bunch of methods/macros/vars with the same name all in the same scope, e.g. the top level namespace.

I really like that one issue I linked earlier, but really the solution here is to just not have macros and methods with the same name in the same scope. I’m not so sure I agree with making that not possible because it shouldn’t be prevented to have the macro generate code that uses the method version of it.

EDIT: On second thought maybe it wouldn’t be that bad because it seems you cant even do that due to an infinite recursion error…

PS: Can you please not include the line numbers in your code blocks. It makes it harder to just copy/paste and run them.

@Blacksmoke16 Sure, I thought line numbers was easy to refer to code, but got it, will not paste my code with line numbers going forward.
By the way, I am new to crystal world. So, is this (macro, function, closure & var names should not conflict with each other) something I should file as a feature request in github ?

I’d maybe wait until some other people see/respond to this thread. I think it’s unclear on if there’s anything actually actionable here, or if there is, what could actually be done.

I can’t really say i’ve experienced this problem (or heard anyone who did), so I’m under the impression that yes this may be a “problem”, but not as much so in the real world.

How would you not know about the local variables in the current scope? :confused: I’m pretty sure this is the behaviour that almost anyone would expect. It’s the same in Ruby, btw.

I second to that. I’m not aware of anyone actually working on large code bases complaining about anything that’s described here as a theoretical “problem”.

I think #10071 would be great to help avoid some mistakes, though. It should probably treat macros and methods alike.

But the differentiation between local variable and call is rarely an issue. Variables are usually scoped to a small section of code and you should be aware of them. If a single scope grows too big to be grasped, it’s a strong sign that it should be refactored into smaller pieces.

As someone never used Ruby, I think I should keep in mind to check Ruby rules first if I have any doubts. Thanks for pointing that out and the answers.

You’re welcome. And please continue pointing out things you don’t understand (or only understood after looking at Ruby docs)! We should have all that in our documentation.

1 Like

Yes, this is really helpful! :pray:

Many of these things we (the current Crystal users) might take for granted because it works more or less the same as Ruby, so we probably forgot to document them.

3 Likes