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 on null #43

Open
mlanza opened this issue May 5, 2023 · 24 comments
Open

Implement protocols on null #43

mlanza opened this issue May 5, 2023 · 24 comments

Comments

@mlanza
Copy link

mlanza commented May 5, 2023

I've been using a library I wrote for nearly a decade now where I implemented protocols as one of it's central premises, because I find them so useful in how I think about writing programs.

Since null doesn't actually inherit from a class and is its own thing, it was not possible to implement protocols on null in the same way as on types, but I did it. I basically have an exception (a loophole in my protocol processing) that recognizes null as a special circumstance and then defers to a Nil type I defined. On that custom type I implement the protocols null uses.

I can tell you firsthand that nil-punning (treating null as a type) is incredibly useful if you don't want to have to continually code around null (or use the Maybe monad), especially in situations where it doesn't matter, and from my experience that's often the case.

@ljharb
Copy link
Member

ljharb commented May 5, 2023

It should always be impossible to have a protocol on null (or undefined) because they can't have any properties.

@mlanza
Copy link
Author

mlanza commented May 5, 2023

I explained that I have a library where I implemented protocols on null. It's just trickery, all in the implementation. Not impossible.

And it's proven useful for a long while as Eric Normand explains.

@ljharb
Copy link
Member

ljharb commented May 5, 2023

Right - I didn't say it was impossible in a library, i said I believed it should be impossible in the language.

@mlanza
Copy link
Author

mlanza commented May 5, 2023

Heard. But I'm making the same cases Clojure makes for its usefulness. And this thread is about putting a voice to a position.

With protocols you can iterate over a sequence, whatever its nature (e.g. concrete type). So when Null is considered a proper type from a protocol's perspective, iterating over it means zero iterations. It's a workable default that makes having to code around null less of a problem, and without having to Maybe monad everything in a language that doesn't regularly use Maybe monads.

There are many other protocols where having a proper Null, and where the developer can decide how it should be handled by the protocol and just makes the fuss surrounding dealing with null go away. In my linked examples, I illustrate sensible defaults for null. These sensible defaults help simplify programming.

It's not like you, the programmer are left to wonder if some nullish behavior is good or bad. You decided what it is since you're the one implementing it via the protocol. And if you don't like it having a behavior, you don't implement it and you deal with your null reference exception some other way.

The behaviors in my code (linked) make those cases demonstrably clear. It's better to provide reasons beyond "I don't like it" when it comes to influencing the community to decide on some part of a proposal.

I linked Eric Norman's talk and there are others which explain the value add. The good is, as I said, making it possible to allow protocols to be implemented on null, doesn't force the matter with anyone. That's the beauty of protocols. They leave devs to decide behaviors.

@ljharb
Copy link
Member

ljharb commented May 5, 2023

I'm not sure why you'd need the protocol to be "implemented" on null? Whatever code interacts with the protocol could simply special-case null, no?

@mlanza
Copy link
Author

mlanza commented May 5, 2023

It's the having to put a conditional statement around everything which is the very problem protocols intend to eradicate. The point is the conditional is moved from the code and every instance you deal with types to a central location.

@ljharb
Copy link
Member

ljharb commented May 5, 2023

You'd need to handle undefined too, and it seems that ?? and ?. would both make that pretty ergonomic.

@mlanza
Copy link
Author

mlanza commented May 5, 2023

In my library I deal with undefined and null as a singular entity, as Clojure does, as it should've been implemented anyway. The existence of undefined and null is nonuseful and often confusing. And if I saw code that attempted to handle them as distinctly different values, I'd cringe.

I've been nil punning with my library for almost a decade. The thing I've found about those who sometimes make counterarguments (I don't know you are doing this) is that they may not have experience with the thing they are arguing against. That is, it's hard to knock a thing you've never actually tried on and to think that because you learned one way (e.g. TypeScript, whatever) that the other way is not useful.

Remember, my suggestion is opt in not absolute. Like the contested default ES6 exports. Use 'em or don't.

@ljharb
Copy link
Member

ljharb commented May 5, 2023

Lots of code treats them as distinct values, including the very language itself - so while that's a fine position to hold as a dev, it's simply not one the language should ever be holding. Both values exist, and are distinct, and some features distinguish null and undefined while others lump them together as "nullish".

I've certainly not used this technique before - but I'm not debating its usefulness, I'm taking that as a given, I'm trying to understand how it could coexist in a language where null and undefined must remain distinct values with no properties on them, for which non-optional property navigation always throws.

@mlanza
Copy link
Author

mlanza commented May 5, 2023

This is an opt in suggestion, so it would allow me to go my way and you to go yours.

@ljharb
Copy link
Member

ljharb commented May 5, 2023

If it's opt-in, then I'm unclear as to why the implementations of the protocol, which you control, can't special-case null/undefined as you like?

@mlanza
Copy link
Author

mlanza commented May 5, 2023

Why don't you demonstrate what you're thinking with code and then I'll explain, because I'm not sure what you're thinking.

protocol ToString {
  tag;

  toString() { 
    return `[object ${this[ToString.tag]}]`;
  }
}

How would you implement that for null?

In my library it would look roughly like:

  function toString(self){ //`self` is null (or undefined)
    return "";
  }

  Protocol.implement(Nil, ToString, {toString});

And that would look identical to how it'd be implemented on a String or any other type:

  function toString(self){ //`self` is already a string
    return self;
  }

  Protocol.implement(String, ToString, {toString});

Keeping both of these implementations the same is what makes implementing protocols a pleasure. The conditional is abstracted away.

@ljharb
Copy link
Member

ljharb commented May 5, 2023

ah i get what you mean. you wouldn't, because null and undefined can't have methods, so indeed every caller always has to account for that, everywhere in the language. What you're asking for is a much much larger feature that would affect the entire language - I don't think protocols would be the thing to add it.

@mlanza
Copy link
Author

mlanza commented May 5, 2023

Just look at my implementation, which I've realized and used for years as a blueprint. It's trivial.

And I'm sure professional language implementers could do it even better than me.

@ljharb
Copy link
Member

ljharb commented May 5, 2023

Creating a Nil class is a major proposal on its own, and I suspect it'd be unlikely to advance. It wouldn't make sense to tie it to any other proposal.

@mlanza
Copy link
Author

mlanza commented May 5, 2023

In my case I defined a Nil type a substitute for the fact that one doesn't actually exist. It only exists to serve protocols, exactly as mine does. It's not actually used elsewhere by JavaScript—unless, someone sees a use case for having them become proper types, which I'm not actually promoting.

Use the same internal trickery I used for greater good.

Call it Protocol.Nil (or choose Protocol.Null and Protocol.Undefined if you like more granularity) if that clarifies its separateness. Or, add a generic hook that gets called when a protocol is called on something it can't understand (like undefined or null) and allow the programmer to handle this loophole however he wants.

Just make it possible to call protocols against anything including these untyped concepts because, if you don't, you're stuck handling nulls everywhere as an exception. And, again, that forfeits the gain that protocols give you (making everything homogeneously respond to a certain set of messages). I'm suggesting that excluding null/undefined weaken the benefit of protocols substantially since null is perhaps the most prevalent (non)value.

@mlanza
Copy link
Author

mlanza commented May 9, 2023

It's worth noting that the proposed implementation only considers protocols for objects, things which would have a this context within a scope.

protocol Ordered {
  compare;

  lessThan(other) {
    return this[Ordered.compare](other) === Ordering.LT;
  }
}

Assuming you want nulls to be comparable (they should be because it will happen!) this won't work. You'd never get your this in that context.

When I implemented protocols I wrote my implementations to explicitly pass the subject.

protocol Ordered {
  compare;

  lessThan(self, other) {
    return self != other && self == null || self[Ordered.compare](other) === Ordering.LT;
  }
}

Doing it this way you provide a way to special-case and handle nulls even if you don't bother deferring to a Nil type as previously mentioned. This subtle change allow both typed objects and nil and undefined to work.

Allowing for an explicit self makes it possible to include nulls as a comparable, which is useful if it's present in an array to be sorted. It allows the dev to specify how nulls sort.

@michaelficarra
Copy link
Member

@mlanza What prevents us from supporting null is our desired calling patterns. See the first presentation to committee. If we wanted to support null, we could, but it would mean that the only calling patterns we would support would be the functional style (or bind operator if that ever happened). We want to also allow the more familiar method call style that most JavaScript programmers would prefer. And at the moment, there's no facility in the language for allowing us to intercept that method call when the target is null (and there likely never will be).

@mlanza
Copy link
Author

mlanza commented May 10, 2023

@michaelficarra - I've been looking at the proposal and I see plenty of examples for implementing protocols, but none for actually invoking them. Has the invocation syntax not been determined?

Given Functor.map and an array how are you thinking the invocation would happen?

  const stooges = ["Moe", "Larry", "Curly"];
  Functor.map(stooges, stooge => stooge.toUpperCase()); //1
  Functor.map.call(stooges, stooge => stooge.toUpperCase()); //2
  stooges[Functor.map](stooge => stooge.toUpperCase()); //3

The first looks best to my eyes, but I can't find it demonstrated anywhere. This has a bearing on a follow-up question I have.

If determined, it might be good demonstrate it in the README with examples.

@nicolo-ribaudo
Copy link
Member

Protocols are just a way to:

  • define some symbols
  • validate that an object has a given set of symbols

Functions.map is a symbol, and the calling pattern would thus be stooges[Functor.map](stooge => stooge.toUpperCase());. It's similar to how you can use, for example, Symbol.iterator by doing myIterable[Symbol.iterator]().

@mlanza
Copy link
Author

mlanza commented May 10, 2023

That's awkward. I've called out some problems with it. I'll revisit this topic, when the newly-linked one reaches a consensus.

@hax
Copy link
Member

hax commented Jun 16, 2023

With extensions proposal (I plan to rename it to "dispatcher" proposal), it's possible to support null.

The initial proposed syntax is stooges::Functor:map(stooge => stooge.toUpperCase()), I'm also exploring other syntax options recently, especially stooges.Functor'map(stooge => stooge.toUpperCase()).

@michaelficarra
Copy link
Member

@hax We shouldn't need that if we go forward with #45.

@hax
Copy link
Member

hax commented Jun 16, 2023

@michaelficarra I think protocol should use method style. Even we use function style, developers still facing the chaining problem and would ask for pipeline operator.

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

5 participants