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

Handling Clojure/ClojureScript protocol differences and other platform discrepencies #49

Closed
lynaghk opened this issue Mar 6, 2012 · 10 comments
Labels

Comments

@lynaghk
Copy link

lynaghk commented Mar 6, 2012

How should we handle language runtime and platform between Clojure and ClojureScript?

One option for handling platform differences is to duplicate namespaces across clj and cljs.
In maths.cljs:

(ns maths) 
(def sqrt (.-sqrt js/Math))

and in maths.clj

(ns maths) ;;maths.clj
(defn sqrt [x] (Math/sqrt x))

Then use Clojure as usual and in ClojureScript just let the cljs version of the namespace get picked up (don't list maths in the crossovers).

That feels like a lot of unnecessary juggling for a LISP though; perhaps we should take advantage of metadata to mark platform-specific functions and provide alternatives all in the same file?

I've also run into problems with differences in the language runtimes.
For instance, in Clojure:

(reify
  clojure.lang.ILookup (valAt [k v] ...))

but in ClojureScript:

(reify
  ILookup (-lookup [k v] ...))

Is there anything we can do to crossover Clojure code like this automatically within lein-cljsbuild?
It's probably easier to make the changes in ClojureScript itself, but maybe there is a reason ClojureScript's protocols have slightly different names than Clojure's (pinging @how should we handle language runtime and platform between Clojure and ClojureScript?

One option for handling platform differences is to duplicate namespaces across clj and cljs.
In maths.cljs:

(ns maths) 
(def sqrt (.-sqrt js/Math))

and in maths.clj

(ns maths) ;;maths.clj
(defn sqrt [x] (Math/sqrt x))

Then use Clojure as usual and in ClojureScript just let the cljs version of the namespace get picked up (don't list maths in the crossovers).

That feels like a lot of unnecessary juggling for a LISP though; perhaps we should take advantage of metadata to mark platform-specific functions and provide alternatives all in the same file?

I've also run into problems with differences in the language runtimes.
For instance, in Clojure:

(reify
  clojure.lang.ILookup (valAt [k v] ...))

but in ClojureScript:

(reify
  ILookup (-lookup [k v] ...))

Is there anything we can do to crossover Clojure code like this automatically within lein-cljsbuild?
It's probably easier to make the changes in ClojureScript itself, but maybe there is a reason ClojureScript's protocols have slightly different names than Clojure's (pinging @swannodette).

@pkamenarsky
Copy link
Contributor

I guess this is because there is no "standard library" in both clj and cljs. clj provides things like clojure.string but as far as I know, relies on Java interop for math for example.

Other things, like timers or logging could be unified too. However, the problem with different defprotocol methods remains and I think this is a result of the "cleaner" design of cljs. Maybe when Clojure in Clojure becomes reality we'll have unified protocol names accross both platforms. Until then, I don't know what a good solution would be.

@emezeske
Copy link
Owner

I can't really speak to the protocol issues; that sounds like a difficult problem.

I do like the idea of using metadata or something similar to provide differing implementations for clj/cljs. One thing that comes to mind would be implementing two new macros: defn-clj and defn-cljs. The defn-clj macro would be a no-op in the cljs library, and defn-cljs would be a no-op in clj. Thus, they would allow differing implementations to be specified.

I'm not sure if this would work, though; I worry that the reader would have problems with cljs symbols appearing in clj code. I feel like this could be solved with some careful quoting in the new defn-* macros, but I haven't played around with this yet so I don't know.

@pkamenarsky
Copy link
Contributor

To be honest, I don't really see a problem with having two different files for clj and cljs, it keeps the code clean.

I think it's really just a matter of making a clojure.stl library that can be easily included in current projects and referenced from cljsbuild. Isn't this already possible?

The protocol issues are a much harder problem though. Apart from some ungodly hacks like rewriting protocol names in deftype / defrecord or hacking the cljs / clj compiler I don't see how it can be done.

@emezeske
Copy link
Owner

I think overall I agree with keeping separate clj/cljs files when the implementations diverge. One problem with that, though, is with macros that need to generate different code depending on the language. It's easy enough to put those macros in different namespaces and include them differently from clj/cljs when the clj and cljs files are distinct, but it becomes more difficult when using the crossover feature to run the same code in clj/cljs. Right now it's possible to use the horrible CLJSBUILD-REMOVE hack to require the macros differently depending on the language, but I think this is very ugly.

The macros are a sore point in general for sharing code between languages, seeing as they have to be required differently (:require vs :require-macros). This can be worked around, again with the CLJSBUILD-REMOVE hack, but I'd like to find a better way.

@swannodette
Copy link

As far as protocols, the the problem is that Clojure on the JVM is largely defined on top of Java interfaces and not protocols. In a certain sense, ClojureScript is ahead of Clojure on the JVM here. I don't see this getting resolved until a much larger portion of Clojure on the JVM is written in itself.

@lynaghk
Copy link
Author

lynaghk commented Mar 12, 2012

@emezeske, I'm not too concerned about macros that need to generate different code for clj/cljs quite yet (i.e., meta-issue 49).

I feel like trying to keep clj/cljs for the same namespace separate manually is just bound to lead to diverging implementations and other sorts of trouble. What about having a cljx file that gets specially preproccessed by cljsbuild? Basically, walk through it and replace

(if-cljs 
  (my-javascripty-form js/xyz 1 2)
  (my-clojure-form xyz 1 2))

with the appropriate form and write the result to clj and cljs files. On a var-level, everything would be assumed to be shared unless specifically marked with metadata like ^:cljs.

If we have a hook inside lein cljsbuild for users to add their own munging functions to walk .cljx code, then people can take care of their own issues. E.g., I could start renaming protocols.

@pkamenarsky
Copy link
Contributor

I still like the idea of keeping both implementations in separate files; optionally compare top level public functions and their argument counts and print a warning when they differ.

However, to expand on @lynaghk's idea, I see one big advantage in keeping both platforms in a cljx file: the ability to define "meta-macros" which would directly macroexpand the cljx flie into a number of platform-specific implementations (not necessarily just clj or cljs ; this may also prove helpful when targetting different browsers, i.e. have cljsbuild produce cljs_ie8 cljs_gecko etc files which are then loaded by a small bootstrap script depending on the user agent, similar to GWT's deferred binding).

Every platform would need to define the same set of meta-macros. For example, the cljs if-cljs macro would expand to its first argument; conversely, the clj if-cljs macro would expand to its second argument, and so on.

This may also be simpler from an implementation point of view? Dynamically include the platform meta-macros, macroexpand and then directly write out the platform files?

edit: this may also aid in solving the defprotocol problems and the :require vs :require-macros dilemma?

@emezeske
Copy link
Owner

I may be starting to warm up to the .cljx file format. :) Using the (currently) unused file extension would prevent the files from being picked up as Clojure or ClojureScript, so they could reside in the classpath without breaking anything. Then they could be processed similarly to how crossovers are handled right now, except with far less ugliness.

If the code was rewritten as part of the copying process, I think it would in fact solve the :require vs :require-macros problem.

One interesting thought: this could be made very general if the cljx transformations were configured via a sort of middleware stack.

I'll be at Clojure West this week, maybe this is something to discuss over a beer!

@lynaghk
Copy link
Author

lynaghk commented Mar 13, 2012

I hadn't thought of it that way, but yeah---something like middleware is pretty much what I'd like.

I'll look for your amorphous fractal-green face at Clojure/West.

@lynaghk
Copy link
Author

lynaghk commented May 26, 2012

Implemented my (very simple) take on this issue here: https://github.com/lynaghk/cljx

@lynaghk lynaghk closed this as completed May 26, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants