Skip to content

Protocols should double as first-class functions #45

@mlanza

Description

@mlanza

In #43 I asked how the syntax for invoking protocols would look.

The answer provided was:

stooges[Functor.map](stooge => stooge.toUpperCase())

This is unpleasant to look at. I find it awkward (and metaprogrammery) to reach inside an object with a symbol to pull out a method and invoke it. I agree it has to happen (this is similar to what I do in my library only that part is under the hood) but I don't want to write/read that aesthetically displeasing syntax. I want it to remain an implementation detail.

I'm not aware of any languages with first-class protocols necessitating this kind of chicanery. Most invoke them as ordinary functions. Having implemented and used protocols (in JavaScript!) for nearly a decade, this is how I invoke them:

Functor.map(stooges, stooge => stooge.toUpperCase()) //improved readability!

When something gets promoted into the language proper one of the usual gains is integrated syntax. Partial application and pipelines exemplify this. I use both regularly today with subpar syntax.

Records and tuples are possible today with no changes in the language:

const della = Record({
  name: 'Della',
  children: Tuple([
    Record({
      name: 'Huey',
    }),
    Record({
      name: 'Dewey',
    }),
    Record({
      name: 'Louie',
    }),
  ]),
});

But they are changing the language so that we can have:

const della = #{
  name: 'Della',
  children: #[
    #{
      name: 'Huey',
    },
    #{
      name: 'Dewey',
    },
    #{
      name: 'Louie',
    },
  ],
};

This further illustrates how syntax, good or bad, either spurs or hinders adoption.

The respondent to my original question states:

It's similar to how you can use, for example, Symbol.iterator by doing myIterable[Symbol.iterator]().

Reaching into an object with a symbol and then invoking it might be bearable if were only done occasionally, but protocols are not a once-in-a-while kind of thing. I use protocols interchangeably with functions (i.e. all the time!). I don't make a distinction between them. Both are just functions. I even export them as such:

export const map = Functor.map; 

so that I can

map(stooges, stooge => stooge.toUpperCase())

The beauty is you can start off by implementing a function and later, when the use case appears, promote it to a protocol. You don't have to prematurely decide you need a protocol for some situation. This follows the usual advice about not too quickly deciding that.

The dev importing your protocol need not even know it's anything but an ordinary function! He shouldn't have to care whether it's an ordinary function, a composition, a higher-order function, a multimethod, or a protocol. If it's a function it should interface and act like a first-class function. And it should be possible to pass it around as a value.

But when a protocol is invoked via symbol it no longer can be. It loses its first-class function status. This is a serious design mistake.

function groupBy(xs, f){
  //somewhere in the body `f(x)` is called.
  //but if `x[f]()` is required of protocols then protocols are not also first-class functions
}

If there were no distinction between flavors of functions (protocol v. ordinary) then f could be either and that's ideal. I mean, you wouldn't want to exclude a whole class of functions from being passed around as values.

Furthermore, you'd want to be able use the underlying flavors of functions interchangeably, e.g. to treat this as a private implementation detail.

Protocols are more akin to functions than methods, as their first-class nature shows. You'd absolutely want to be able to pass them around and use them in all the spots where you might pass in a vanilla function. TypeScript's types and interfaces are fine as methods as they're suited to OOP, whereas protocols are more suited to FP as I've previously elaborated.

Interfaces make the object the subject whereas protocols make the function the subject.

This is a key distinction.

//protocol
Functor.map(stooges, stooge => stooge.toUpperCase()) // kingdom of verbs

//method/interface
stooges.map(stooge => stooge.toUpperCase()) // kingdom of nouns

JavaScript (via TypeScript and/or duck typing) already has interface polymorphism. What it lacks is function polymorphism. Protocols can afford this and provide a different (FP-improved) style of programming, by allowing protocols to be treated as functions rather than methods. Clojure popularized protocols and this is how it uses them and from where much of their power is derived.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions