Closed FroMage closed 9 years ago
This would work, but I have a few remarks:
- Looks like the package names have to be the name of the platform?
According to the "indicative" implementation only. In truth I don't like that much and I think it might be better to have something based on annotations.
- Doesn't look like we can get backend-specific module imports without #499 as well
We already have them. And they already work, except for one little bug in the js runtime (it ignores optional
).
- Can you clarify how we find specific implementations? Name-conventions for declarations seems new in Ceylon.
Agreed, it would be better to have something based on annotations. Like, perhaps:
implementation("node.js", `class JsImpl`)
implementation("jvm", `class JavaImpl`)
interface Api {}
Or whatever.
- Can we have backend-specific values/methods too?
Backend-specific toplevel functions, sure. Values maybe, I would need to think about it.
- It's not clear to me how we can make the backends optimise the metamodel part away, when at compile-time we already know the implementations so we should be able to do a lot better.
I don't think it matters much. We can always memoize. It would be nice, but it's not a hard requirement for 1.1.
We already have them. And they already work
And they don't do what backend-specific imports do.
So let me suggest something else, which we can optimise properly:
interface Api{}
implementation("node.js", `class JsImpl`)
implementation("jvm", `class JavaImpl`)
native Api createApi();
Which would:
This would work for both methods and classes.
@gavinking This is a real breakthrough and the credit is to the team who understand the innards. The ramifications of this decision for scripting, binding browser libraries etc. are immense. I have some spare time these days and would be glad to help out wherever pointed.
Not sure if you people think it's important but none of these proposals allow for easy after-the-fact extension of platforms, the list of implementations is always somehow hard-coded.
Couldn't we put the annotation on the implementing class?
implementation("node.js", `interface Api`)
class JsImpl() satisfies Api {}
implementation("jvm", `interface Api`)
class JavaImpl() satisfies Api {}
(I agree this starts to look like CDI and therefore might not perform acceptably according to Stef's criteria)
@quintesse The higher the better. Usually, there will be a different developer for each implementation and the inner workings of the package or module may be different for each platform. Also, it is easier for the typechecker/compiler to ignore packages/modules than delve into the semantics of each class.
Multiple implementations will be needed mostly when infrastructure services provided by the VM are needed: storage, database, HTML5 local storage, cookies, networking etc. -- and may be radically different to warrant class-level granularity.
Not sure if you people think it's important but none of these proposals allow for easy after-the-fact extension of platforms, the list of implementations is always somehow hard-coded.
Well my justification for that is:
Now, OTOH, if we were trying to create a more general "service loader" facility, then yes, we would need extensibility. But I'm not sure if we really want to risk that this grows into a lame version of CDI...
@akberc Well the recent work on Android suggests that a new platform is not as uncommon as one might expect and in the mean time the creators of Api
might not want to have the burden of maintaining/including an unstable platform.
At the same time the people working on the new platform might want to easily test additional implementation of Api
without bothering anyone else (and without needing to use private forks of Api
).
Also this would still need the package/module work to provide the platform specific compilation, I'm just suggesting a change in the way the lookup of the implementation is done.
@gavinking
the number of platforms is small and enumerated
It's the enumerated I wonder about. If "platform" is what we say it is and we define the list (right now "jvm" and "js"... or is it "jvm", "browser" and "node.js"?) then the it is as you say (but like I say above, the people working on a new platform will have to maintain forks of native packages until the time we decide to merge into mainline).
Or platform is a more malleable concept and we allow people to define their own. For example "android" might be a platform with a fallback to "jvm" if a specific implementation for Android wasn't found. Likewise "node.js" and "browser" might be platforms with fallbacks to "js".
This solution avoids scanning, which would be needed for extensibility. But I'm not sure if we really want to risk that this grows into a lame version of CDI
Indeed, not fond of either.
Or platform is a more malleable concept and we allow people to define their own
Malleable, yes, with the compiler/runtime/CMR implementation deciding what to ignore or what to fall back to. The constraint should not be an enumeration, but a compiler/runtime/CMR combination. The cap on proliferation would be the steep effort required to implement compiler rules or compiler/CMR implementations This opens up avenues like script compiler, HTML template compiler. browser 'require' CMR, etc. Maybe I'm getting too enthusiastic :)
I would prefer more general solution, like mentioned "service loader", which is something, what we will need anyway.
This solution avoids scanning, which would be needed for extensibility.
Maybe we can generated some index of provided services, during compile time, to avoid scanning.
I would prefer more general solution, like mentioned "service loader", which is something, what we will need anyway.
OK, so I can certainly live with that. So I say we split this up into two separate issues:
platform
or compiler
annotation or whatever, which distinguishes only JS-specific and JVM-specific packages, and for the typechecker to validate package dependencies for sanity.ceylon.language
or the SDK to provide service loader functionality.These are really pretty separate things, though they of course need to work together. The service loader doesn't absolutely have to make it into 1.1.
I haven't heard any feedback on my proposal to use generated native methods to provide the implementation-specific instances rather than the metamodel. I still think this is the best approach and is otherwise the same as the latest proposal, with many gains and no losses.
@FroMage Well, it's fine for the limited case but what other people are saying is that they want something more generic that handles more platforms than just { jvm, js }. If that's the case then I don't see how generated methods would solve the more generic problem.
I haven't heard any feedback on my proposal
I don't like the hard-codedness of it to be honest, which is why I agreed more with @gavinking 's option and even suggested the more CDI-like behaviour.
But the list of supported platforms is known at compile-time no? The JVM backend supports a fixed set of platforms (currently JVM), and same for the JS compiler. If the list of platforms is known at compile-time and the implementations have to be provided at compile-time (for type checking), since they are in a platform-specific package of the current module, then I don't see how platforms could be added at runtime?
I think pluggability like CDI is something that is way outside the scope of backend-specific implementation for modules, even though we could make it more complex than it requires, but I don't see why we need all this complexity for this task.
@FroMage To me the list of platforms is not the same as the list of backend we compile to. The latter is indeed pretty fixed and any changes to that list will mean releasing a new distribution.
But how often would someone write backend-specific code that is platform-independent? There is some in ceylon.interop
for accessing Java internals like Class
. And ceylon.collection
I guess? It probably only uses Java classes that are also available on Android. But the part of the Java JRE that Android makes available in their compatibility layer is limited.
So personally I'd rather just have one official public system to handle platform specific code (where the 2 backends could be represented as "generic" jvm and js).
NB: It's just the hard-coding that I don't like. If it would be possible to have some special Xxxx createXxxx()
method that could combine "service loading" with method handles to get the best speed possible that would be great. But I see that almost as a generic system we could use for method handles retrieved from a service loader.
Edited for clarity:
Here are the current use cases, most of which we theoretically support but practically do not support yet:
I'm not sure I understand how we got from backends to platforms. We already have SDK modules like ceylon.math
which requires backend-specific code to be made cross-platform.
What do you call a platform then and why can't that work with backend-specific code like what we're trying to fix?
For example, JDK and Android may have different modules, but that would mean that we need certain module imports for JDK, and others for Android then. The problem is that the JDK and Android can't be imported at the same time, since they both provide the same set of packages, so we should create two .cars
?
We don't yet understand the issues related to Android modules well enough to reason about Android properly.
Do we have examples of different code for different JS platforms that would require different Ceylon implementations? With dynamic
and with the fact that ATM we can't import JS modules (non-Ceylon) in a type-safe way, pretty much any platform-specific code can be implemented by if
statements in the same JS backend-specific package.
As a developer, I would like my JVM-targeted Ceylon code to bind to JS-targeted Ceylon abstractions (and vice-versa) so that a simple compile results in HTML pages, module lookups and loading, and AJAX bindings to server-side code.
Whoah, let's not dive off the deep end! :) I do agree we want this, but I don't think this is part of the discussion on this issue.
Well, for example, resource loading works differently for a JS program running on node compared to a browser-based client.
can be implemented by if statements in the same JS backend-specific package.
Yes, which means hard-coding it, which is what at least some of us don't like. Because it means I either convincing others that my platform is worth supporting and it gets added to the official module or I need to maintain a fork. With service loading I could just say: I have an implementation for interface Xxx
for platform Y.
and with the fact that ATM we can't import JS modules (non-Ceylon) in a type-safe way
but we can wrap non-Ceylon JS modules in a Ceylon abstraction, .e.g. Twitter bootstrap and have a auto-stitched Ceylon require loader. It is the creation of these abstractions that is a hurdle at the moment.
The problem is that the JDK and Android can't be imported at the same time, since they both provide the same set of packages, so we should create two .cars
Yes, there is a choice to NOT allow multi-executable multi-platform modules and create a Maven-like classifier/extension/type artifact specification within a module. That may be the easiest and consistent way if the alternates are too complicated.
Well, for example, resource loading works differently for a JS program running on node compared to a browser-based client
OK but in both cases this is done via either the dynamic
block which the JS compiler can compile in both cases, using an if
statement, or via a ceylon.language
API that will just work.
With service loading I could just say: I have an implementation for interface Xxx for platform Y.
So you're talking about multiple modules here, right? This is again more complex than backend-specific code which would indeed be solved with backend-specific-packages. This also means that implementations are either not type-safe or at least not checked during the compilation of the main module.
BTW, I'm sure we can reconcile the two such that for the code generated on behalf of the platform-specific native
declaration will properly dispatch for all supported backends (known at compile-time by just being there in backend-specific packages), and typechecked so that the signatures and types match, all with a generated if
statement that falls back to whatever pluggable API will use the metamodel to look for alternative unknown (at compile-time) implementations using discover (whatever) if the platform was not supported at compile-time.
This way we get type-safety and speed for backend/platforms we support at compile-time (we solve all the current use-cases for the SDK), and we remain extensible for third-party implementations that we don't know about (no use-case we currently have).
Something like:
interface Api1 {}
abstract class Api2(Integer i){}
platform native Api1 createApi1();
platform native Api2 api2(Integer i);
With implementations in backend/platform-specific packages, and would generate the following code for the JVM:
public Api1 createApi1(){
if(os.platform == "JVM")
return Api1JVMImpl();
else
return ceylon.language.lookitup(Api1.class);
}
public Api2 api2(long i){
if(os.platform == "JVM")
return Api2JVMImpl(i);
else
return ceylon.language.lookitup(Api2.class, i);
}
Note that this also works for methods and attributes.
Not for 1.1.
So in fact, the biggest limiting factor is the IDE and what it can do, and it looks like it will have to be enhanced to support anything interesting.
Here's a list of requirements:
Here's a list of nice-to-have:
if
statements.The problem is mostly the IDE because most of the requirements have solutions that would be easier for the CLI. The only way the IDE can load code that is backend-specific ATM is under different modules that live in different projects. That's very restrictive, but is driven by the fact that if the IDE is set to compile for both backends, it will in fact only use the JVM typechecker, and ignore anything that comes from JS until it runs the compiler.
I think the easiest thing for us to do is to put backend-specific code in another module, then define which toplevel native declarations are backend-specific and let the compilers plug those in. The typechecker would then validate that the backend-specific toplevels are indeed provided for all backends. The modules would be cross-dependent (otherwise implementation cannot be validated).
Something like:
module ceylon.math {
jvm requireImplementation
import ceylon.math-jvm;
js requireImplementation
import ceylon.math-js;
}
shared interface Whole {}
shared abstract class Half {}
shared backendSpecific native Integer foo;
shared backendSpecific native Integer bar(Integer i);
shared backendSpecific native Half makeHalf(Integer i);
shared backendSpecific native Whole makeWhole(Integer i);
And then:
module ceylon.math-js {
implementationProvider
import ceylon.math;
}
import ceylon.math { Whole, Half, foo, bar }
shared implementation(`value foo`) Integer fooImpl = 2;
shared implementation(`function bar`) Integer barImpl(Integer i) { return 1; }
shared implementation(`function makeWhole`) Whole makeWholeImpl(Integer i) { return WholeImpl(1); shared implementation(`function makeHalf`) Half makeHalfImpl(Integer i) { return HalfImpl(1); }
The IDE will be able to validate that native implementations exist and that their schema is the same. The compiler will just plug them at compile-time differently for JS and Java. This is limited to toplevel values and functions which are easy to plug. It makes no sense for interfaces (though I expect they will be used to define what the methods will return) and is harder to plug for classes.
We can even extend it to suppose dynamic lookups later:
shared backendSpecific dynamicImplementation native Integer foo;
Which would be compiled for the JVM to:
public class foo_ {
public long getFoo(){
if(os.platform == "JVM")
return ceylon.mathJvm.foo_.getFoo();
else
return ceylon.language.lookupDynamicProvider(foo_.class).get(); // use metamodel for `value foo` for lookup
}
}
With a dynamic provider:
module stef.math {
implementationProvider
import ceylon.math;
}
import ceylon.math { Whole, Half, foo, bar }
shared dynamicImplementation(`value foo`) Integer fooImpl = 2;
Which would generate:
public class fooImpl_ {
static{
ceylon.language.registerDynamicProvider(foo_.class, fooImpl_.class); // use metamodel for `value foo` for lookup
}
}
The only things the IDE needs to do are:
requireImplementation
module imports exist, but not even import them or make them visible (that's a part where I'm not too happy with doing this on a module import, because we don't want the module import to be visible in the main module, so perhaps we should not call this import, but on the other hand, at runtime we do need the module import to exist for plugging to work, but only one of them. It's only the IDE which should not make it visible)ceylon.mathJs
only compile for JS, but no idea if that's hard or not.If it's hard to make the IDE do this, perhaps another solution is to make the IDE run two typecheckers for backend-specific modules, and make each ignore packages reserved to the other. That's a lot less efficient though. This also allows for backend-specific module imports but they would only be visible in backend-specific packages.
Thanks Stef for the detailed breakdown. I have three comments;
Here is a Ceylon-ish take on this: https://gist.github.com/akberc/01c8a492097a4564d79a
@quintesse has been working on this.
Closing this, what is implemented right now:
"You'd put your documentation here"
native
shared void test();
native("jvm")
shared void test() { print("A test for the JVM"; }
native("js")
shared void test() { print("A test for JS"; }
shared void run() { test(); }
for toplevel methods (as shown), attributes and classes.
The signatures of the declarations with the same name have to be identical.
The declaration that is simply marked native
is called the "abstraction" while the ones that have a native annotation with an argument are called "implementations".
The only allowed arguments to the native annotation are: jvm
and js
.
A declaration that is shared
must have an abstraction, otherwise it's optional (but the implementations still need to have identical signatures).
Method and attribute abstractions and members of class abstractions can leave out their implementation (if you specify them they will be ignored by the compilers).
There's no support for imports right now, this is ongoing for #499.
Btw, this doesn't implement exactly what was proposed in the original text of this issue. But for now this new implementation supersedes that idea.
@gavinking when you fix the error messages please give me a heads-up so I can fix the tests.
Fantastic, congratulations!
Sent from my iPhone
On 28 Apr 2015, at 7:26 pm, Tako Schotanus notifications@github.com wrote:
Closed #500.
— Reply to this email directly or view it on GitHub.
Great! So what does this mean for Java or JS declarations then? Can we still have them?
What are you referring to exactly @FroMage ?
Suppose I need to write a Java or JS class/function? In a .java
or .js
file?
@FroMage In principle, yes, because that's the case of just a native
header and no implementation. In practice I'm not sure if the JVM compiler can handle this anywhere outside of the language module. (The JS compiler does support it, I believe.)
Yes, you can do it like this for example (directly from my test module):
test.ceylon:
shared void run() {
testNative();
}
native("jvm") void testNative() {
myprintJvm(NativeCode().test());
}
native("js") void testNative() {
dynamic {
myprintJs(nativeCode());
}
}
and then have:
NativeCode.java:
public class NativeCode {
public String test() {
return "This is a native Java class";
}
}
and:
nativecode.js:
function nativeCode() {
return "This is a native JavaScript function";
}
(you'll need to have the latest code though, I just fixed a bug related to this)
Oh, so you can. How does the IDE behave in this case? I expect it not to see nativeCode
from js since the JS model loader is not plugged in the IDE, but will it see the NativeCode
class in Ceylon code? Will it not allow you to call it even outside native methods?
The IDE hasn't changed behaviour for what was already there.
So you can just call NativeCode
outside of native methods, you'll just not be able to enable the JS backend because you'll get errors.
The same with using dynamic
and trying to enable the JVM backend.
But you can enable both backends and then put those calls/dynamic in native methods and everything should work.
I say "should" because right now the IDE does have a problem still with JVM code. I'm looking into that.
Ok, the IDE problem is now fixed.
We need to figure out how to have certain files only compiled for certain backends. It could be as simple as saying
.java
files for the JVM backend and.js
files for the JS backend, but we may also want different Ceylon source files for different backends, as glue or something.