Let's begin by explaining the problem. Let's assume that my project
needs a platform specific way to convert a int to a string:
On clojure-jvm:
(defn to-string [i] (.toString Integer i))
On clojure-py:
(defn to-string [i] (py/str i))
On Clojurescript:
(defn to-string [i] (.toString i))
We could do this inline (inside a common .clj file):
(for-platform "jvm"
(defn to-string [i] (.toString Integer i))
"python"
(defn to-string [i] (py/str i))
"js"
(defn to-string [i] (.toString i)))
But this gets ugly real fast.
My idea, would be to provide some sort of hook to the compiler, that
would be cross-platform. The compiler, when looking for a namespace,
would find all files in a given folder that match the required
namespace:
/test/to_string.clj
/test/to_string.cljpy
/test/to_string.cljs
/test/to_string.cljclr
These function hooks would then pick the best entry based on the
extension, defaulting to .clj if no better option exists. Since these
functions could be composable it would be possible to also dispatch on
platform versions:
/test/to_string.cljpy26 ; Python 2.6 code
/test/to_string.clj7 ; JVM 7 code
It seems to me that this would be a very clean way to allow clojure
programs to be cross-platform. The side-effect is that it would force
non-standard code out into separate namespaces where they can be
easily maintained. If someone wanted to port the project to a new
platform, he would need only to find existing platform overrides, and
create new entries.
Thoughts?
Timothy Baldridge
> --
> You received this message because you are subscribed to the Google
> Groups "Clojure" group.
> To post to this group, send email to clo...@googlegroups.com
> Note that posts from new members are moderated - please be patient
> with your first post.
> To unsubscribe from this group, send email to
> clojure+u...@googlegroups.com
> For more options, visit this group at
> http://groups.google.com/group/clojure?hl=en
First thing which comes to mind is to use metadata for this purpose. Something like(defn ^{:platform :jvm} to-string [x] ...)
What we need is even more defined then that, however, Consider this
snippet from core.match:
(ns clojure.core.match
(:refer-clojure :exclude [compile])
(:require [clojure.set :as set])
(:import [java.io Writer]))
how do we use metadata to restrict/include java.io.Writer vs CLR's
System.IO.TextWriter? If we go with the meta-data route, then we're
forced to put ^{:platform :jvm} 40 times in a single file instead of
just putting the code in a different file and not needing to clutter
the code with extra meta-data. Not to mention that defmacro, deftype,
defprotocol, etc. would all need to check for this metadata before
executing.
cljx is a lein plugin...I'm trying to figure out how to do this on the
compiler level. It seems that it would be good to have a way to to
this in clojure-clr, clojure-scheme, etc.
Timothy
> First thing which comes to mind is to use metadata for this purpose.
> Something like
>
> (defn ^{:platform :jvm} to-string [x] ...)
I don't think that's too practical. The reader sees the metadata only
after it already started to read the form it should then ignore, if the
platform doesn't match.
> Another option is to use reader macros, like in Common Lisp:
>
> #+ :jvm (defn to-string ...)
Yet another option is to use plain macros, and arbitrary test
expressions instead of just keywords. For example, I have some jvm
clojure code where I use the ForkJoin classes if available and fall back
to executor services if not. One could consider a clojure.platform
namespace with standard, frequently used predicates.
Something along the lines of:
(cond-compile
(platform-jvm? ">=1.7.0") ...
(platform-jvm?) ...
(platform-clr?) ...
:else (throw (Exception.
(format "Sorry, frobnification not supported on %s" *platform*))))
Bye,
Tassilo
What we need is even more defined then that, however, Consider this
snippet from core.match:(ns clojure.core.match
(:refer-clojure :exclude [compile])
(:require [clojure.set :as set])
(:import [java.io Writer]))how do we use metadata to restrict/include java.io.Writer vs CLR's
System.IO.TextWriter?
If we go with the meta-data route, then we're
forced to put ^{:platform :jvm} 40 times in a single file instead of
just putting the code in a different file and not needing to clutter
the code with extra meta-data.
I don't think that's too practical. The reader sees the metadata only
after it already started to read the form it should then ignore, if the
platform doesn't match.
>> I don't think that's too practical. The reader sees the metadata
>> only after it already started to read the form it should then ignore,
>> if the platform doesn't match.
>>
> As I understand it happens before compilation (lein plugin extracts
> the appropriate forms and then compiles it),
Such a feature shouldn't depend on a specific tool, it should be part of
the language.
> so it shouldn't be a problem, no? Am I wrong?
It's doable, but not very elegant and unlispy. Reader macros or plain
macros are a better match here.
Bye,
Tassilo
Such a feature shouldn't depend on a specific tool, it should be part of
the language.
>> Such a feature shouldn't depend on a specific tool, it should be part
>> of the language.
>
> Yeah, you're right - I haven't thought about the fact that leiningen
> isn't available on the platforms other than jvm and js.
And even if it was, why shouldn't I be allowed to build my clojure
projects with ant (like clojure itself) or maven?
Bye,
Tassilo
And even if it was, why shouldn't I be allowed to build my clojure
projects with ant (like clojure itself) or maven?
kind regards
-1 for metadata+1 for file extensions or paths.I don't think metadata or in-file conditional compilation is the right tool for this.My reasons are as follows1. I don't think it is a good idea to mix multi-platform definitions in one file. it gets complicated and confusing very quickly. You cant see the wood for the trees.2. What about adding a new platform - do you edit the existing files or add a new one.3. Different platform specifics could be maintained by different teams with only the port code required. Most people will not care about every platform.4. Separating them means that you can very easily see what is platform specific and port. You can also easily produce separate source distributions if you so wish.5. If you add metadata - then the complier must read the file in order to decide whether it needs to look process the contents or not. This will increase build times substantially as you add platforms.
I don't know about platform versions - that could get very complicated.If following the extension route, you would also require an extension for generic clojure code vs jvm platform code in addition to the others.I like the idea of separate, parallel source paths:e.g.src/clj/blah.clj ;; "pure" clojuresrc/jvm/blah.clj ;; for the jvmsrc/py/blah.clj ;; etcsrc/clr/blah.cljsrc/cljs/blah.clj
> I think all your concerns can be addressed by carefully using the
> metadata solution?
>
> ^{:platform :jvm} (load "jvm/lib")
> ^{:platform :python} (load "python/lib")
But why would that be better than a reader macro
#+:jvm (load "jvm/lib")
#+:python (load "python/lib")
or a usual macro? IMO, metadata is good for metainformation that
someone (a human, the compiler) wants to access later, not for
consumption by the lisp reader. Or would the non-matching forms still
be read but not compiled afterwards? In that case, you get problems
with
^{:platform :jvm} 1
^{:platform :python} 0
because metadata cannot be attached to numbers.
Bye,
Tassilo
^{:platform :jvm} (load "jvm/lib")
^{:platform :python} (load "python/lib")
But why would that be better than a reader macro
#+:jvm (load "jvm/lib")
#+:python (load "python/lib")
>> But why would that be better than a reader macro
>> #+:jvm (load "jvm/lib")
>> #+:python (load "python/lib")
>
>
> I don't think it's "better", this two options are rather equivalent.
> The only difference is that #+ would require new reader entities,
> while variant with metadata leverages already existing clojure
> facilities.
That there's metadata already doesn't mean the reader would not need to
be adapted. And # is already a reader dispatch character, so there is
probably not much difference implementation wise.
> Although, another thing is that it looks like metadata would be more
> flexible; for example, one could write
>
> ^{:platform :jvm, :version "1.7"}
>
> or maybe even something like
>
> ^{:platform :jvm, :version "[1.7,)", :implementation "sun"}
But my own programs already use :platform and :version metadata with
completely different semantics. (Well, no, they don't, but it could
be.)
> Of course, it's possible to make #+{:version ...} work, but then I don't
> see how is it really differs from meta.
I'd still prefer a more general plain-macro version for conditional
compilation with arbitrary tests instead of hardcoded platform and
version keys. For example, I might want to be able to test if a certain
program is installed on the machine, or if a certain lib is in the
CLASSPATH.
Bye,
Tassilo
(cond-require {:platform :jvm :version 7} 'core.platform.jvm.mymodule)
(cond-require {:platform :clr :version 4.5} 'core.platform.clr.mymodule)
With a single modification to core.clj we could then add this to the ns macro:
(ns clojure.core.match
(:refer-clojure :exclude [compile])
(:require [clojure.set :as set])
(:cond-require {:platform :jvm :version 7}
[core.platform.jvm.mymodule :as mymodule])
(:cond-require {:platform :clr :version 4.5 :arch :x64}
[core.platform.clr.mymodule :as mymodule]))
In this way we kind-of get the best of all worlds. Code is broken out
into seperate files, we can use metadata to give extra platform
specific info to the loading routines, and we don't end up with any
magic strings. Users can organize code however they wish, if someone
wants /src/platform/jvm/foo.clj and someone else wants
/src/jvm/foo/platform/foo.clj they are free to do so.
I guess the only thing I don't like about this is that when porting to
a new platform you can't simply find for .cljclr and copy/modify all
the found files. Instead you have to grep the contents of all .clj
files, and figure out what needs to be done, but if the library
maintainers do their job, that should be eaiser. They can simply put
all platform files in a separate directory
Timothy
> I'm starting to like the hybrid approach as well. We could create a
> new version of "require" that's a conditional load:
>
> (cond-require {:platform :jvm :version 7} 'core.platform.jvm.mymodule)
> (cond-require {:platform :clr :version 4.5} 'core.platform.clr.mymodule)
>
> With a single modification to core.clj we could then add this to the
> ns macro:
Basically, I think to split out platform specific code into their own
namespaces is appropriate and should be encouraged for all the good
reasons you mentioned. But there might be places where it also had its
drawbacks.
For example, you have a single, rather complex function that uses
platform specific code at various places. With the cond-require
approach, you have to essentially clone it for each platform. If the
original version is buggy, all of them are buggy. If you start
refactoring your design, you'll need to do the same modifications to all
of them.
Alternatively and probably better, you just create wrapper functions for
every occurence of platform specific code, may it only be a constructor
call. Well, that's a bit against the clojure principle of "don't wrap
your host platform", but probably that principle is becoming out of
fashion at least for libs and programs intended to be run on many
platforms.
Bye,
Tassilo
But my own programs already use :platform and :version metadata with completely different semantics. (Well, no, they don't, but it could
be.)
I'd still prefer a more general plain-macro version for conditional
compilation with arbitrary tests instead of hardcoded platform and
version keys.
>> I'd still prefer a more general plain-macro version for conditional
>> compilation with arbitrary tests instead of hardcoded platform and
>> version keys.
>
> I agree, although nothing prevents us from something like :condition
> (some-expr) being evaluating for such complex cases. By the way, do
> you have any clue why in Common Lisp they use #+ #- instead of macro
> approach, which seems a way more powerful?
Reader macros are quite common in CL, and they are short and handy when
you use them on individual forms inside functions, i.e., when you don't
split specifics into separate files. And of course, CL implementations
are usually not hosted on some other VMs, so interop with host platforms
doesn't occur. You rather deal with little things like clisp
interpreting the common lisp spec about file system access slightly
different than SBCL, or with non-standardized extension libs.
With respect to plain macros: that's almost trivial to do in both CL and
Clojure, and I'm sure that's frequently done here and there. For
example, that's a quick and dirty macro I'm using somewhere in my code:
--8<---------------cut here---------------start------------->8---
(defmacro compile-if
"Evaluate `exp` and if it returns logical true and doesn't error, expand to
`then`. Else expand to `else`. Example:
(compile-if (Class/forName \"java.util.concurrent.ForkJoinTask\")
(do-cool-stuff-with-fork-join)
(fall-back-to-executor-services))"
[exp then else]
(if (try (eval exp)
(catch Throwable _ false))
`(do ~then)
`(do ~else)))
--8<---------------cut here---------------end--------------->8---
It would just be nice if there was a standard facility for doing stuff
like that, for example in the form of a platform namespace with a
conditional compilation macro plus various commonly needed predicates
that can be used as test expressions. (I would not object to a reader
macro in addition.)
Bye,
Tassilo
Korma is a library that is probably exceptional in its avoidance of platform specific features in most namespaces. Porting something like swank would not have been so easy. In general, I would say that for this library, I would prefer to use the platform specific folder approach - i.e having two namespaces korma.platform.db and korma.platform.util in src-jvm/ and src-clr/ folders. Then there could be a CLR specific project.clj (maybe project.clr.clj) sitting next to the JVM project.clj (and eventually maybe a project.py.clj). Each project.clj would reference the correct platform specific folder in addition to the shared src/ folder. Just off the top of my head, the reasons for supporting this approach are:
While I can see why it might be nice to be able to quickly add some metadata to distinguish this platform specific code while keeping everything in one file, as other have mentioned, I would encourage people to think about the downsides of this. Say, I had just taking korma.db and put some conditionally compiled CLR code alongside the JVM code. The file doubles in size. Would I then want the py team to do the same? Also, for the one .indexOf -> IndexOf change, say there are little changes like that all over the place. Would a team adding a new port want to go through the whole source tree and find every instance of this? Without modifying the clojure compilers we have now, the platform specific directory approach will work and is probably cleaner. It just requires team consensus on adopting this approach.
One thing, I would propose is adding some build tool awareness of this so that maybe an NLein or PyLein could use the same project.clj and just see :platform-clr, :platform-py, :platform-jvm attributes. Or this could be done by having a shared project.shared.clj included by each platform specific project file. These types of approaches might be simpler and more maintainable in the long run.
Aaron
1. Tagged literals
2. Compiler-as-a-service
Tagged literals do not have the drawbacks of metadata, or of macros.
They are ideal for indicating the semantics of a piece of data. If a
piece of data needs to be tagged as representing clojure,
clojurescript, clojure-py, or for that matter any language whatsoever,
it can be done.
Lets give it the polyglot namespace:
#polyglot/clj (foo bar)
#polyglot/cljs (foo bar)
#polyglot/clj-py (foo bar)
...
#polyglot/js "foo(bar);"
...
#polyglot/common-lisp (foo bar)
Now the code can be reliably manipulated as data, and transmitted to
compiler services as needed.
For the purposes of the previous discussion we need a compile-time
macro to pick code corresponding to the current platform, something
like
(pick-current-platform [#polyglot/clj impl1 #polyglot/cljs imp2])
For some projects it makes sense to segregate things into files for
different languages, but that could be a higher level of organization
built on top of tagged literal source code representations.
Furthermore, powerful things can be achieved using the tagged literal
model. It becomes possible to nest languages within one another, to
build polyglot systems.
For the sake of argument, suppose that tagged code turns into
datatypes that implement some useful interfaces. In addition to code
compilation, they could also implement Callable.
So you could write something like
#polyglot/cljs (+ 1 ( #polyglot/clj #(+ % 1) 2)
--> 4
(assuming that the compiler/evaluation service has been configured to
allow communication between the different hosts)
One detail I haven't worked out yet is if the tagged code literals
should be quoted, or not.
Let the file suffix correspond to the tagged literal with which the
file contents are interpreted.
so foo.clj would get read as #clj <file contents>
and so forth. (non-official releases would use namespace-qualified
suffixes, though the "/" would have to get munged approrpiately)
This has the benefit of being systematically extendable, and useable
by all kinds of systems that need to read data from disk, in addition
to source code.