[CLJS-414] Implement specify, allowing instances to implement protocols Created: 30/Oct/12  Updated: 30/Dec/13  Resolved: 30/Dec/13

Status: Closed
Project: ClojureScript
Component/s: None
Affects Version/s: None
Fix Version/s: None

Type: Enhancement Priority: Major
Reporter: Herwig Hochleitner Assignee: Herwig Hochleitner
Resolution: Completed Votes: 1
Labels: None

Attachments: Text File 0001_1-CLJS-414-specify-and-specify-macros.patch     Text File 0001-CLJS-414-specify-and-specify-macros.patch     Text File 0002-CLJS-414-Implement-extend-type-in-terms-of-specify.patch     Text File 0101-CLJS-414-specify-and-specify-macros.patch     Text File 0102-CLJS-414-Implement-extend-type-in-terms-of-specify.patch     Text File 0103-CLJS-414-Test-specify-features-deprecation-nowarn-an.patch     Text File 0104-CLJS-414-Update-specify-to-allow-implementing-IFn-an.patch     Text File 0201-CLJS-414-specify-and-specify-macros.patch     Text File 0202-CLJS-414-Implement-extend-type-in-terms-of-specify.patch     Text File 0203-CLJS-414-Test-specify-features-deprecation-nowarn-an.patch     Text File 0204-CLJS-414-Update-specify-to-allow-implementing-IFn-an.patch    


Javascript objects are fully dynamic. Currently, ClojureScript has no mechanism to exploit that for protocol implementation.

CLJS-398 was a first attempt to implement an operation to extend single instances, but it fell short because it offered no control over which closures to attach onto the object (i.e. allocates every time). Also, specify is a much better name than extend-instance.

extend-type can be implemented in terms of specify (by specifying the .prototype), but extend-type has grown from its humble beginnings
to an monster of complexity
Currently it does

  • print warnings
  • optimize IFn impls
  • special case the Object "protocol"
  • special case js builtins

and all that in a single macro body. So this is also a good opportunity to do some refactoring.

specify should have an interface similar to extend-type. Additionally a lower level operation is needed to attach existing lambdas as protocol methods. It's called specify* in my current implementation. It takes a lambda for every specified protocol-method-arity, with a syntax loosely based on clojure.core/extend.

Comment by Herwig Hochleitner [ 30/Oct/12 11:21 PM ]

First patch implements specify, second switches extend-type to use it.

Comment by David Nolen [ 31/Oct/12 9:30 AM ]

This is a big patch so it's going to take time to review. First question, if we're going to follow the footsteps of extend what's the purpose of changing the syntax like having to specify a map of arities?

Comment by Herwig Hochleitner [ 31/Oct/12 10:32 AM ]

The reason is that I wanted specify* to do as little as possible. It's basically just an abstraction over the name mangling for protocol methods, which was completely hidden till now. One of the purported use cases is:

(def methA-1 [this] ...)
(def methA-2 [this x] ...)
(def methB-1 [this] ...)

(defn specify-to-P [o]
  (specify* o
    P {methA {1 methA-1
              2 methA-2}
       methB {1 methB-1}}))

Here the method bodies are not allocated every time specify-to-P is called, as opposed to what the more high-level specify would do. If we didn't know which arities are to be generated, there would be simply no way to infer it:

  • there is no simple way to know at compile time which arities an fn has
  • if there was we wouldn't be able to get the fn at compile time in all cases
  • defprotocol doesn't expose it's methods or their arities


As for using symbols instead of keywords: I could do this, since specify* is a macro, not a function.
Making specify* a function may have its own merits, which I frankly hadn't thought about. Actually, that might be sweet, I'll look into that possibility.

Comment by Herwig Hochleitner [ 31/Oct/12 12:59 PM ]

As for specify* being a function: It uses a map in its syntax and at the same time is used to define PersistentHashMap, so that wouldn't work.
EDIT: the real reason is name mangling, see comment below

What could conceivably be done is to have a core function (specify-method [proto method arity impl]).
EDIT: can't be done without reflection, same reason

That way we could support consumers that want to determine the set of implemented protocols at runtime, similar to extend. This can be a separate ticket where we work out whether it's :method 'method or "method".

As for specify* using symbols: My gut feeling tells me that since it will always be a compiler macro (or builtin) and takes the arities, there is no merit in making it superficially more similar to extend.

OTOH, it occurs to me that given specify-method, we could leave out specify* completely. I think I'll try that when I get home from work. Thoughts on that approach?

Comment by Herwig Hochleitner [ 31/Oct/12 7:59 PM ]

Actually, I had a rationale for using symbols in specify*, which I just now rediscovered while trying to implement aforementioned specify-method.
It uses symbols to highlight the fact, that the method names are subject to the same (gclosure) minification as every other name.

This is also the real reason specify* (or specify-method) can't be a function: It would need a reflective layer to go from [proto method arity] to the gclosure minified version of $proto$method$arity$a. We don't have that in clojurescript, since it would considerably add to the output size.

Considering that all, I think specify* in the current form is appropriate after all.

Nevertheless I found an unrelated issue in the patch where 'Object would be resolved (with an unused result), triggering a warning. I added the 0001_1 patch, which supersedes 0001 and is functionally equivalent modulo the warning issue. 0002 still applies.

Comment by Herwig Hochleitner [ 01/Nov/12 12:09 AM ]

Attached patch set 01** supersedes patch set 00**

The differences are

  • Make ^:skip-protocol-flag work
  • Added tests
Comment by Herwig Hochleitner [ 03/Nov/12 12:53 AM ]

Set up a design page http://dev.clojure.org/display/design/specify+i.e.+reify+for+instances

Comment by Herwig Hochleitner [ 04/Nov/12 10:06 PM ]

Attached patch 0104, applies on top of other 01* patches.
It adds capability to specify* IFn and allows specify* to take an expression too.

Comment by Herwig Hochleitner [ 03/May/13 7:30 PM ]

rebased to current master
see also https://groups.google.com/d/topic/clojure-dev/1UegnkLSwhE/discussion

Comment by Gary Trakhman [ 03/Dec/13 10:47 AM ]

I could see this being immediately useful to me, it would enable some experimentation with dynamic extension of clojure collection functions over JS types. Without it, it's hard to make those impls composable and exportable in a convenient way.

Comment by David Nolen [ 03/Dec/13 10:50 AM ]

This ticket needs serious cleanup. All the old patches should be removed and a new patch rebased to master submitted that follows all the conclusions arrived at on the design page.

Comment by Herwig Hochleitner [ 05/Dec/13 12:06 PM ]

I have published the branch from which the patches were generated on https://github.com/bendlas/clojurescript/tree/specify
It mostly implements the behavior specified on the design page, but needs rebasing to master and fixing one case relating to IFn.invoke vis a vis .call
I'd like to get back to it and plan to do so, when I can take a few days for working on clojurescript itself.

Comment by David Nolen [ 30/Dec/13 9:39 PM ]

fixed, https://github.com/clojure/clojurescript/commit/571e156d2daa223dcef273106827e932283e2f93

further enhancement should be addressed by smaller tickets

Generated at Fri Feb 15 17:12:54 CST 2019 using JIRA 4.4#649-r158309.