An example i’m familiar with is with how i handle route actions for Athena. I convert each route’s action method into a proc so that i’m able to store a reference to the method that should be executed for a given route. Then when an actual request comes in, I can use that stored proc to call the correct action.
A proc also has a different scope, so it also allows me to take a Hash(String, String) as input and convert those strings within the scope of the proc so that there isn’t a union of the types from other actions. Then once the input params are converted to their correct types, i can call the proc with those values as arguments.
So for the static binding, that is: resolving code during compilation, they are mostly the same. The interesting part is dynamic binding, that is: resolving code in runtime. There are three main tools here: method overloading, inheritance overriding and function pointers. They serve different use-cases and there are important distinctions between them.
Inheritance overriding serves to choose a code path based on the instance type.
Method overloading makes the choice based on the argument list.
Function pointers let you make the choice yourself.
There are also important distinctions between inheritance and interfaces (classes vs. modules).
There is a monumental amount of information out there regarding this stuff, it may turn out to be fun for you to explore this further.