I am close to using a behaviour as a pure interface and I feel dirty...

82 views
Skip to first unread message

Brett Hemes

unread,
Aug 23, 2021, 6:00:54 PM8/23/21
to erlang-q...@erlang.org

... should I?

 

I have some servers that carry around references to other (let’s call them child) servers.  Each of the referenced child servers are doing their own thing and are implemented as different modules but also share some common functions (albeit with different implementations).  At some point, I end up fetching a reference to a child server and want to make a call that is common to all of the child servers.  As of right now I do not know what specific type (module) the child server is, only that they ALL offer some subset of functionality.

 

Assuming I am not approaching this entirely wrong (which could be the case) I have identified two leading options for my case:

 

1) use a behaviour (perhaps improperly) as a pure interface (i.e., with only callbacks and no "general" functionality).  For a child implemented in a `my_child_mod` module that implements behaviour `my_iface`, this could result in calls like `my_iface:a_common_fun(ChildServerRef)` after fetching the child server reference where I use the behaviour's module to access the child's functionality... OR

 

2) store the particular module of each child with the reference and use the stored module name at time of the call (and know/assume that all children have some shared functionality).  So using some tuple or similar `{my_child_mod, ChildServerRef}` (versus just the reference) could result in a call like `my_child_mod:a_common_fun(ChildServerRef)` after fetching the reference info (as a tuple).

 

I, perhaps naively, gravitate towards #1 above for various reasons (compile-time checks, only having to carry around refs, my C++ background, etc.), but the internet (and ferd in particular here (https://stackoverflow.com/questions/4119477/implementing-interfaces-in-erlang/4119615#4119615)) seems to imply that this is not the Erlang way (despite finding OTP cases like this: https://github.com/erlang/otp/blob/master/lib/ssh/src/ssh_sftpd_file_api.erl).   ... shame on me?

 

 

I am trying very hard to leave my C++ past behind and do things in the Erlang way, but need a little help in this particular case...

 

Thanks,

Brett

Stanislav Ledenev

unread,
Aug 24, 2021, 12:29:01 AM8/24/21
to Brett Hemes, erlang-q...@erlang.org
Erlang's behaviour is NOT an interface!
If you are looking for some analogy from other languages the closest one is abstract classes in C#.



вт, 24 авг. 2021 г. в 01:00, Brett Hemes <brh...@mmm.com>:

Brett Hemes

unread,
Aug 24, 2021, 5:46:57 PM8/24/21
to erlang-q...@erlang.org

> Erlang's behaviour is NOT an interface!

> If you are looking for some analogy from other languages the closest one is abstract classes in C#.

 

This isn’t very helpful...  it is the exact same response I find in the forums with no reasoning behind it.  I don’t need analogies either; I understand what behaviours are “supposed” to be from the documentation and comments (perhaps this wasn’t made clear by my post).  Where I fall short is “why” are behaviours limited to such and why aren’t more people asking the same questions I am stuck on (regarding polymorphism)?  My logic was: yes, this has been asked and discussed some in the past with no real resolution that I could find... therefore, users must be content/accepting of the tools provided.  I am not so naive to think I am the first to need/want such, so there must be a disconnect.

 

I posted my example to motivate my questioning hoping for some insight and/or comfort.  As of now, I have proceeded with storing “meta refs” to my child servers that are module/reference tuples (along with some dangerous and future-maintenance-issue-causing assumptions regarding their “interface”)... and it’s works... it just smells, and I am always eager to learn and find the right/better/best way.

 

Aside: a colleague came across this repo (https://github.com/eldarko/epolymorph) while digging and the readme seems to capture my use case almost exactly...

 

Brett

Fernando Benavides

unread,
Aug 24, 2021, 5:54:16 PM8/24/21
to Brett Hemes, erlang-q...@erlang.org
I'm not entirely sure if this will help but a while back I wrote two articles and gave a talk about this subject. But I might have faced it from a different angle.

These are the links:
--
Sent from Gmail Mobile by Brujo Benavides

Brett Hemes

unread,
Aug 24, 2021, 6:13:23 PM8/24/21
to Fernando Benavides, erlang-q...@erlang.org

Brujo!  Yes I read both of these (and your other stuff too) some time back and they have been (and still are) very helpful on my journey.  I just watched the presentation earlier today coincidentally in my search and it confirmed most of what I have learned but still doesn’t tackle my deeper questions like (“am I doing something stupid to even arrive at this line of questioning?” and “why shouldn’t I have a delegate behaviour and/or one of just callbacks?”, etc.).

 

Thanks!

Brett

Stanislav Ledenev

unread,
Aug 25, 2021, 5:04:06 AM8/25/21
to Brett Hemes, erlang-q...@erlang.org
OK, I'll try to give you my opinion on this topic. If I understand your question correctly. 
It may look rude but please believe me it is not my intention to be rude.

Your examples for me are the evidence of the "classic OOP" poisoning.
Remark:
I call it "classic" based on how the vast majority of the developers perceive the idea of objects and
interaction between them. 
Though I am on the side of Alan Kay's definition of the OOP - objects communicating with messages.

Anyway, how you "slice" the world and how you make your abstractions - it is influenced by the "classic OOP".
Erlang is the functional language (more or less) so abstractions are made the other way:
1. Basic units are Module and Function;
2. Dynamic function calls - Module:Function(Args). So you can dynamically (in runtime) change the module;
3. Behaviour is just a hint for a compiler to check a Module for a presence of the required functions.

Based on these 3 points - when you write -behaviour(....) in a module you are just saying that
this module must have a bunch of functions implemented to be successfully compiled. It is not for
supporting your own abstractions. It is an instrument to remind you to implement functions.

What you call "general" functionality in Erlang is simply a library implemented as module(s).

Of course it is very tempting to call behaviour an interface, because it looks like an interface.
But it is not because the interface in Erlang are the functions, not a separate entity.

Example of using behaviour from my own experience.
As in almost any application I must have something where I store the data.
And requirements are very vague. So I don't know if it would be Mnesia, PostgreSQL, file, space station or a toaster.
So I've made the behaviour which defines functions for my business logic associated with storage.
To make it simple it is like the CRUD operations on my entities. 
This behaviour is implemented as an independent rebar3 library, so other parties (sometimes remote) can include it as a dependency.
But their implementations are included in the final application and what implementation will be used
is defined in the configuration file. 
(Pseudo) code is like this:

callbacks_db.erl:
    -module(callbacks_db).
    -callback create(Entity::my_entity_t()) -> ok | {error, Result :: term()}.

postgresql.erl:
    -module(postgresql).
    -behaviour(callbacks_db).
    create(Entity) -> ....

toaster_able_to_storage_data.erl:
    -module(toaster_able_to_storage_data).
    -behaviour(callbacks_db).
    create(Entity) -> ....

main_app.config:
     {main_app, [
         {db, #{mod => postgresql}} % If you are using a toaster use "toaster_able_to_storage_data"
     ]}

main_app.erl:
    some_function(DBMod) ->
        Entity = create_entity(...),
        DBmod:create(Entity).

That is all about behaviour.

ср, 25 авг. 2021 г. в 00:46, Brett Hemes <brh...@mmm.com>:

Tristan Sloughter

unread,
Aug 25, 2021, 7:42:41 AM8/25/21
to Erlang Questions
I'll drop in this effort that was made years ago: https://github.com/erlware/erlware_commons/blob/master/doc/signatures.md

But I'm not saying I recommend it, I don't know that it is used much and I'd like to deprecate erlware_commons in the not too distant future (it is a dependency used by rebar3/relx and we want to remove as many of those as possible).

I'll also point out my use of behaviours to provide APIs for functionality (an Application Programming INTERFACE -- so "interface" isn't always an OOP construct). Hah, just realized I even called it an "interface" in my most recent PR https://github.com/open-telemetry/opentelemetry-erlang/pull/269

Brett Hemes

unread,
Aug 25, 2021, 11:31:20 AM8/25/21
to Tristan Sloughter, Erlang Questions

Historically, have there ever been discussion of including stuff like this in the language itself?

 

Brett

Brett Hemes

unread,
Aug 25, 2021, 11:33:39 AM8/25/21
to Stanislav Ledenev, erlang-q...@erlang.org

Thank you Stanislav for taking the time to iterate.  This is nice.

 

> Though I am on the side of Alan Kay's definition of the OOP - objects communicating with messages.

 

> Anyway, how you "slice" the world and how you make your abstractions - it is influenced by the "classic OOP".

 

> Of course it is very tempting to call behaviour an interface, because it looks like an interface.

> But it is not because the interface in Erlang are the functions, not a separate entity.

 

Indeed, but all of the teaching texts are very quick to advocate wrapping your implementations with API calls that delegate to the underlying implementation.  This in my mind adds a layer of abstraction on top of pure message passing... abstractions that play to the weaknesses of "poisoned" practitioners ;)

 

That said, your answer does help; thank you again very much!

 

I am still not all the way there yet, but at a much better place than a few days ago.  My current impression at this point is that the topic is nuanced and perhaps I should be thinking more along the lines of "quit THINKING of behaviours in terms of interfaces" or more generally, "quit THINIKING in terms of interfaces at all"?

 

I found it interesting in your example you call `create/1` in main_app at runtime using `DBmod` where my misguided thinking would have tried to use `callbacks_db` instead (interface thinking).  ...this might be my disconnect.

 

> It is an instrument to remind you to implement functions.

 

As I type this I think most of my issues stem from the fear of changing something in the future and not having compile-time checks to help me find all the references (and I saw the behaviour as a nice compile-time check (crutch?) that looked attractive).  This is probably a bigger and different issue all together that I need to learn more about within the Erlang ecosystem.  I am assuming it is some combination of supervision, Dialyzer, and really good tests but I am sure I will learn this quite quickly after deploying some real stuff in the wild...  I just would rather learn as much as I can before that time comes.

 

 

Thanks,

Brett

 

 

From: Stanislav Ledenev <s.le...@gmail.com>
Sent: Wednesday, August 25, 2021 4:04 AM
To: Brett Hemes <brh...@mmm.com>
Cc: erlang-q...@erlang.org

Stanislav Ledenev

unread,
Aug 26, 2021, 3:28:07 AM8/26/21
to Brett Hemes, erlang-q...@erlang.org
You are welcome!


>> I am still not all the way there yet, but at a much better place than a few days ago.  
>> My current impression at this point is that the topic is nuanced and
>> perhaps I should be thinking more along the lines of
>> "quit THINKING of behaviours in terms of interfaces" or more generally,
>> "quit THINIKING in terms of interfaces at all"?

I don't think we can put the idea of "interface" away at all. We are surrounded
by "interfaces" in real life. For instance, the power outlet and electrical plug
is the interface. I'd rather say that it is better to go away from OOP's definition
of interface to more practical one. I'd say that the set of exported functions in the module
is the interface too.

And of course to get rid of unnecessary layers of abstractions. 
As Joe Armstrong said: "The problem with object-oriented languages
is they’ve got all this implicit environment that they carry around with them.
You wanted a banana but what you got was a gorilla holding the banana and the entire jungle."


>> As I type this I think most of my issues stem from the fear of changing
>> something in the future and not having compile-time checks to help me find
>> all the references (and I saw the behaviour as a nice compile-time check
>> (crutch?) that looked attractive).  This is probably a bigger and different
>> issue all together that I need to learn more about within the Erlang ecosystem.  
>> I am assuming it is some combination of supervision, Dialyzer, and really good tests but
>> I am sure I will learn this quite quickly after deploying some real stuff in the wild...  
>> I just would rather learn as much as I can before that time comes.

I highly recommend you to go deeper into the Erlang ecosystem. The process of transition
from other languages to the Erlang is much steeper than with other languages but definitely
more rewarding.
And I would recommend spending more time with tools but be careful though.
For example, due to dynamic nature of the language you write in your code something
like this:
    foo() ->
        Result = hjsdfkjsdhfskjfhskhskhfskl:yutyrutyriuyiw(),
        ...
       
And it will compile. Because you can bring the module "hjsdfkjsdhfskjfhskhskhfskl" later
and the compiler doesn't know that. And the Dialyzer won't tell you anything...
But the "xref" tool may tell you that you are probably using an unknown function in an unknown module.

ср, 25 авг. 2021 г. в 18:33, Brett Hemes <brh...@mmm.com>:

Kostis Sagonas

unread,
Aug 26, 2021, 3:44:57 AM8/26/21
to erlang-q...@erlang.org
On 8/26/21 9:27 AM, Stanislav Ledenev wrote:
> And I would recommend spending more time with tools but be careful though.

Yes, both of these are good pieces of advice. The second is also
applicable to what one writes about the tools of the Erlang ecosystem.


> For example, due to dynamic nature of the language you write in your
> code something
> like this:
>     foo() ->
>         Result = hjsdfkjsdhfskjfhskhskhfskl:yutyrutyriuyiw(),
>         ...
>
> And it will compile. Because you can bring the module
> "hjsdfkjsdhfskjfhskhskhfskl" later
> and the compiler doesn't know that. And the Dialyzer won't tell you
> anything...
> But the "xref" tool may tell you that you are probably using an unknown
> function in an unknown module.


Surely Dialyzer will tell you that there is an unknown function in the
module that you analyze.

Other than speed, there is no reason to use 'xref' for this simple task.

Kostis

Stanislav Ledenev

unread,
Aug 26, 2021, 9:18:54 AM8/26/21
to Kostis Sagonas, erlang-questions
>> Surely Dialyzer will tell you that there is an unknown function in the
>> module that you analyze.

I guess you are talking about the -Wunknown option?
It is ok, but this option is turned off by default. 
At the beginning of learning Erlang's tools, in terms of  ease of use, the xref tool is more convenient (IMHO of course).

Anyway, Erlang has a bunch of really good tools and understanding them is a matter of time.




Tristan Sloughter

unread,
Aug 26, 2021, 12:01:49 PM8/26/21
to Erlang Questions
I don't remember details and haven't reread this, but I dug up this old discussion about rebar3 and dialyzer unknown option: https://github.com/erlang/rebar3/issues/1751

Apparently the dialyzer cli tool has `-Wunknown` on by default but the api doesn't?

Stanislav Ledenev

unread,
Aug 26, 2021, 5:06:03 PM8/26/21
to Tristan Sloughter, Erlang Questions
>> Apparently the dialyzer cli tool has `-Wunknown` on by default but the api doesn't?

"
-Wunknown (***)
Let warnings about unknown functions and types affect the exit status of the command-line version. The default is to ignore warnings about unknown functions and types when setting the exit status. When using Dialyzer from Erlang, warnings about unknown functions and types are returned; the default is not to return these warnings.

*** denotes options that turn on warnings rather than turning them off.
"

чт, 26 авг. 2021 г. в 19:01, Tristan Sloughter <t...@crashfast.com>:

Kostis Sagonas

unread,
Aug 27, 2021, 8:29:41 AM8/27/21
to Erlang Questions
The statement in the manual is indeed a bit confusing, but it clearly
states that enabling this option affects **the exit status** of the
dialyzer Unix command. But dialyzer reports (and always reported)
unknown functions, as you can see in the example it was mentioned in the
beginning of this thread:

$ dialyzer f.erl
Checking whether the PLT /home/kostis/.dialyzer_plt is up-to-date... yes
Proceeding with analysis...
Unknown functions:
hjsdfkjsdhfskjfhskhskhfskl:yutyrutyriuyiw/0
done in 0m1.41s
done (passed successfully)


Note the "passed successfully" exit status (since I did not use
-Wunknown). If I use it, I get:


$ dialyzer -Wunknown f.erl
Checking whether the PLT /home/kostis/.dialyzer_plt is up-to-date... yes
Proceeding with analysis...
Unknown functions:
hjsdfkjsdhfskjfhskhskhfskl:yutyrutyriuyiw/0
done in 0m1.02s
done (warnings were emitted)


I hope this settles it.


But, if anybody from OTP reads this, perhaps the phrase:
"; the default is not to return these warnings"
should be taken out from the manual.


Kostis


On 8/26/21 11:05 PM, Stanislav Ledenev wrote:
> >> Apparently the dialyzer cli tool has `-Wunknown` on by default but
> the api doesn't?
>
> Quote (https://erlang.org/doc/man/dialyzer.html):
> "
> -Wunknown (***)
> Let warnings about unknown functions and types affect the exit status of
> the command-line version. The default is to ignore warnings about
> unknown functions and types when setting the exit status. When using
> Dialyzer from Erlang, warnings about unknown functions and types are
> returned; the default is not to return these warnings.
>
> *** denotes options that turn on warnings rather than turning them off.
> "
>
> чт, 26 авг. 2021 г. в 19:01, Tristan Sloughter <t...@crashfast.com
> <mailto:t...@crashfast.com>>:
>
> __
Reply all
Reply to author
Forward
0 new messages