

Clojure Protocols can extend final Java classes (like java.lang.String) - swannodette
http://clojure.org/protocols

======
zachbeane
Reminds me of jwz's Java doc on the topic of final variables, and how the
access is restricted by compiler convention, not in the JVM:

    
    
        System.in, out and err (the stdio streams) are all final
        variables. They didn't used to be, but some clever applet-writer
        realized that you could change them and start intercepting all output
        and do all sorts of nasty stuff. So, the whip-smart folks at Sun went
        and made them final. But hey! Sometimes it's okay to change them! So,
        they also added System.setIn, setOut, and setErr methods to change
        them!
    
        ``Change a final variable?!'' I hear you cry. Yep. They sneak in
        through native code and change finals now. You might think it'd give
        'em pause to think and realize that other people might also want to
        have public read-only yet privately writable variables, but no.
    
        Oh, but it gets even better: it turns out they didn't really have to
        sneak in through native code anyway, at least as far as the JVM is
        concerned, since the JVM treats final variables as always writable to
        the class they're defined in! There's no special case for
        constructors: they're just always writable. The javac compiler, on the
        other hand, pretends that they're only assignable once, either in
        static init code for static finals or once per constructor for
        instance variables. It also will optimize access to finals, despite
        the fact that it's actually unsafe to do so.

~~~
bobbyi
> some clever applet-writer realized that you could change them and start
> intercepting all output and do all sorts of nasty stuff.

Can someone explain how this worked? What does he mean by "intercepting all
output"?

~~~
btilly
Replace System.out with an object that responds to all of the same methods,
but which does something clever with them. For instance one that adds
timestamps on output, logs stuff somewhere, filters, etc.

This is a very powerful trick. However the downside is that it is in the
category of things where only one person gets to be clever. Because the second
person to replace that object breaks the first. So they decided to discourage
that.

------
swannodette
As simple as this:

    
    
      (defprotocol Foo
        (bar-ify [this]))
    
      (extend-type java.lang.String
        Foo
        (bar-ify [this] (.concat this "Bar")))
    
      (bar-ify "Hello world!")
    

Everything can be redefined interactively at the REPL. Also due to Rich
Hickey's mad skills you get pretty much Java method dispatch performance.

EDIT: There is a gross misunderstanding about how this works. The following is
invalid and will throw an exception, please take the time to understand this:

    
    
      user> (ns baz)
      baz> (bar-ify "foo")
    

Your changes will not affect use of java.lang.String in other libs.

~~~
chousuke
I'd like to clarify that Protocols just generate a bunch of functions and a
Protocol object (and an interface, but that's an implementation/interop
detail). The functions are the interesting things in most cases. However,
they're also namespaced like everything else. You can do the following though:

    
    
      user> (defprotocol Foo (describe [object]))
      user> (extend-type java.lang.String
              Foo 
              (describe [string] "a String"))
      (ns bar)
      bar> (user/describe "String") -> "a String"
      bar> (extend-type java.lang.Object
             user/Foo
             (describe [obj] "an Object"))
      bar> (in-ns 'user)
      user> (describe 3) -> "an Object"
      user> (describe "Hello") -> "a String"
    

So there is potential for two libraries extending the same things and causing
a conflict if the semantics differ, but I'm not too concerned about that. It
would be rather rude to extend someone else's protocol to cover someone else's
java class without documenting that you do so.

------
mquander
Why's this worth noting? There's no reason you shouldn't be able to extend a
type with methods to support a new interface, no matter what that type's
author intended. It's not in any sense "dangerous" to existing APIs, as
changing final variables could potentially be. It seems like completely
natural behavior.

~~~
pohl
_It's not in any sense "dangerous" to existing APIs_

If your job is to deliver and maintain such an API, then by allowing
subclassing you have a larger contract you have to maintain when you want to
make changes to the underlying class: rather than just the public interface,
you also need to worry about the protected interface. If your API is new, you
may want to shepherd your users (by marking a class as final) to favor
composition over inheritance to maximize your ability to evolve the
implementation in ways that don't break.

~~~
jjs
_by allowing subclassing you have a larger contract you have to maintain when
you want to make changes to the underlying class: rather than just the public
interface, you also need to worry about the protected interface._

In other words, if you allow subclassing, then you also have to worry about
the interface that you have explicitly marked visible to subclasses.

Normally a non-problem, by definition.

 _final_ is useful when sealing a class that inherits from a parent class with
a protected interface, but even if "final" is reduced to an annotation (or
even a comment), its presence explicitly limits your end of the API contract.

~~~
swannodette
_There is no subclassing going on_. Take the time to actually read the
documentation provided by the link.

~~~
jjs
The italicized portion at the top of my comment was a quote from the parent
comment.

------
xtho
Does is actually extend the class or simply wrap it with a decorator? I think
Groovy takes this approach but I could be wrong.

~~~
chousuke
It does neither. The dispatch logic is on the protocol side, which can be
modified. When a class is extended, the protocol function is changed to have
an extra instance check. (I don't understand the details in full, though.
You'll need to ask Rich Hickey)

AFAIK there is also some kind of caching involved to improve performance.

