Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement protocols as functions rather than methods #46

Open
mlanza opened this issue Jun 16, 2023 · 0 comments
Open

Implement protocols as functions rather than methods #46

mlanza opened this issue Jun 16, 2023 · 0 comments

Comments

@mlanza
Copy link

mlanza commented Jun 16, 2023

Following is a case for why I feel protocols are better realized using functions instead of methods.

TLDR: Methods rely on classes/inheritance and provide static polymorphism, protocols dynamic polymorphism. It's behavior from within vs. without. While it's unusual to extend types (monkey patch them) at runtime, it's not unusual to implement protocols against new types at runtime. (I use the term "protocols" to match how Clojure defines them since it's implementation well solves the expression problem. What some languages call protocols are more akin to interfaces.)

I already explained if you implement a protocol using the function interface, this makes them truly first class since they can be passed around. They can be passed into higher-order functions who are none the wiser about the flavor of the function.

The bind operator is just syntax to make functions look like methods.

Take Array#slice. At one point or another you've encountered

const args = Array.prototype.slice.call(arguments):

which is not as concise as one would hope.

What happened here is slice was implemented as a method, but it's being used (in a new context) as a function. The peculiarity of the call is required because this appears within. Had an explicit self held the first argument position, none of this would've been necessary.

This would've worked:

const args = slice(arguments);

I get it. Because JS leans more OOP than FP, methods make sense most of the time. But, from my experience, methods are about dealing with concretions. Or to put it another way, if you have a function which is tailor made for one thing, it might as well be a method on the prototype. But protocols, as I explained (#45), operate against abstractions since they target potentially many types.

slice, for example, deals with the abstraction of an iterable. The arguments object is not an array but it has a similar interface so slice just works.

Now take an environment where someone extends arguments with slice. So now slice can be called on arguments and arrays directly without the hubbub shown in the first example.

const args = arguments.slice();

And since both arrays and arguments have the same named method one infers the same behavior.

Now you're writing a program. In that program you write a function which expects some object implementing slice. Different objects are passed into that function. And everything works when those objects all abide the "slice contract"—but there are no guarantees.

This is duck typing in a nutshell. The trouble is when a duck-typed invocation executes it may not have done the right thing since the existence of a method named slice doesn't guarantee it abides the intended behavior of the slice contract. Still, it continues on whereas when a protocol executes it's guaranteed to have done the right thing or otherwise fail at the call site and not somewhere further along. Protocols make guarantees duck typing can't.

When you invoke a method on an object, the object holds the high ground. The object is in charge of what that method actually does.

When you invoke a protocol, the protocol holds the high ground and it guarantees the object against which it is invoked will, if it implements the protocol, meet its contractual obligation and provide the expected behavior. Under the hood, symbols are used as implementation details to reach into the object and see if it satisfies the protocol. But the point in implementing it with symbols is to enforce the integrity of the protocol.

Safely dictating behavior from the outside is what protocols are about.

Consider SugarJS. It didn't like that many seemingly necessary methods were omitted from various objects, like arrays. So it went ahead and implemented them anyway, despite all the wisdom published that you never extend natives. In doing so, some of what the T39 committee decided about certain proposals had to choose different method names to avoid breaking sites using Sugar.

The issue is that your version of slice and mine might entail two different things and my simply extending a prototype with the version I consider the most useful, might end up breaking an app using both libraries. Since both methods share a name and reside at the same address, things will break.

This is the class of problem protocols eliminate. They're tailor-made to safely, dynamically extend existing types while others are doing the same while ensuring no name collisions. We can both implement a protocol, coincidentally named ISliceable, and have zero issues to contend with.

Methods reside on objects and prevent separate third parties from using the same name. That protocols operate at a higher level of abstraction is what distinguishes them from methods/interfaces. This is lost if they're implemented as methods.

It's true a function can be implemented to have the veneer of a method by dropping the self argument to this (as with slice), but this is done (potentially with the bind operator) only to achieve a certain syntax. It doesn't properly express that the ownership of the behavior comes from without (as with protocols) and not within (as with methods and inheritance chains).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant