fireproofsocks
Why are behaviour methods referred to as "callbacks"?
Coming from an OO background, my first reaction is to think of behaviours as interfaces. So it makes sense that a module must define one or more functions outlined in a behaviour if it wishes to implement that behaviour.
So why in Elixir are the behaviour’s functions referred to as “callbacks”? In other languages, a “callback” has quite different connotations which have little to do with inheritance.
In fact, the page at https://elixir-lang.org/getting-started/typespecs-and-behaviours.html has left me scratching my head, especially the section on “Dynamic dispatch”. Since “callbacks” are apparently not at all what you might think, it makes me really wonder what exactly is meant by “dynamic dispatch”. There are no clarification or examples in that section (!!!), only a reassurance that “you don’t need to define a behaviour in order to dynamically dispatch on a module, but those features often go hand in hand.”
Can someone help shed light on this? Thank you!
Most Liked
peerreynders
In simple terms the “behaviour” already exists as the behaviour module capturing the generic parts. You are implementing the callback module which contains the specific parts. To execute those specific parts the behaviour module calls the “callback” functions on the callback module.
The behaviour module is passed a callback module much in the same way a function is passed a callback function - the difference is that the behaviour module expects certain functions to be implemented by the callback module rather than there just being that one function.
defmodule Parser do
@callback parse(String.t) :: {:ok, term} | {:error, String.t}
@callback extensions() :: [String.t]
def parse!(callback_module, contents) do
case callback_module.parse(contents) do
{:ok, data} -> data
{:error, error} -> raise ArgumentError, "parsing error: #{error}"
end
end
end
I swapped implementation with callback_module - so it makes sense that callback_module.parse/1 is a callback function.
Another example from the Access behaviour
def fetch(%module{} = container, key) do
module.fetch(container, key)
rescue
exception in UndefinedFunctionError ->
raise_undefined_behaviour(exception, module, {^module, :fetch, [^container, ^key], _})
end
The fetch/2 function implemented by Access grabs the callback module from the struct so it can turn around and call the module.fetch/2 callback function.
With behaviours you are composing the callback module with the behaviour module - there is no inheritance.
peerreynders
Thought experiment - referring to the Frequency callback module from my last post
def start,
do: Server.start(__MODULE__, [])
That sample code (server.exs) can be easily rewritten to work in the following fashion:
def start,
do: Server.start(
&__MODULE__.init/1,
&__MODULE__.handle/2,
&__MODULE__.terminate/1,
[]
)
Are init/1, handle/2, terminate/1 callbacks now?
Wouldn’t developers say: Why can’t I just pass the module that implements those functions?
peerreynders
You have not yet used GenServers? They do it very similar to this…
Just to underline this point: example
The Demo module is the callback module. None of the usual behaviour ceremony is actually necessary - other than implementing the mandatory callback functions.
{:ok, pid} = GenServer.start_link(Demo,[])
The Demo callback module is composed with the GenServer behaviour module leading to a GenServer based process with the specialized Demo functionality.
In OO:
parse!/1would be a method on the abstract class. It may have some generic logic but doesn’t do the actual work. It would call a hook methoddo_parse/1that is left for the subclass to implement the content specific parsing.- The subclass overides
do_parse/1implementing the content specific implementation. You use the subclass to parse the content it is specialized for.
So:
Parser.parse!/2is the generic function in the behaviour module that will delegate the details of parsing the specific content toMyParser.MyParseris a collection of hook function implementations needed to parse the specific content (organized as a callback module).
fireproofsocks
Thank you for so many educational responses! So the name “callbacks” DOES make sense, but ONLY for the parse! function where we are passing in a module and then calling a method on it. I still don’t see any callbacks happening for our regular behaviour implementations. How about the following?:
Parser.parse(AnotherParser, contents)
That would make sense: at least that looks like it is performing a dispatching/callback option, but of course that does not work because function Parser.parse/2 is undefined or private.
So when we actually define a @callback and an @impl of it, then as far as I can see, we aren’t actually DOING a callback. We are just enforcing that implementations follow our behaviour blueprint in the same way that OO languages implement an interface and must implement the required methods.
When we define the parse!/2 function and provide a module and actually DO module dispatch via a callback, then we aren’t actually directly using anything to do with the @callback or @impl functions. In principle, we could do that entire module-dispatch and callback stuff entirely without any of the @behaviour, @callback, or @impl stuff.
Forgive me for being dense, but the naming convention here still does not make sense to me. I would not call those @callbacks, I would call them “signatures”, “members”, “actions”, or almost anything other than a loaded term like “callbacks” because nothing about them requires a callback: you can write them without delving into behaviours at all! “Callbacks” really seems like the wrong word for what is going on here and it has sent my brain on a wild goose chase. It seems that they were named after HOW they get typically used and NOT after what they actually ARE. For me at least, this has caused a lot of mental friction.
Summary
The best I can sum up for myself after the patient guidance of the contributors here is this:
- The name
@callbackscan be a confusing misnomer! A@callbacksimply defines a function signature that every implementation of the behaviour must define. This is conceptually the same as an Object Oriented class implementing all of the functions defined by an interface. - The practice of using “dynamic dispatch” works well with behaviours: your “dispatching” function can accept a module as input, then call a method on that module. Behaviours offer a way to guarantee that the module provided as input has implemented the function that you wish to call on it. E.g. in the following example, you had better be 100% sure that your provided target module has implemented the
do_something/1function:
def dispatch_to_module(target_module, args) do
target_module.do_something(args)
end
asummers
Not to detract from your point, but the name callback in the Erlang case is 30+ years old at this point. Some of the original authors that pop around here every now and then (rvirding from above, e.g.) may be able to comment on the etymology of the name but due to the telephony nature of Erlang, I think it may actually be referencing a literal (phone) call back. GenServer has a concept called call so I would imagine it stemmed from the generic behavior around GenServers. That’s pure speculation, however.







