Notes from last week's DEP meeting

131 views
Skip to first unread message

Bob Nystrom

unread,
Oct 20, 2015, 1:00:41 PM10/20/15
to Dart Core Development, General Dart Discussion

John Messerly

unread,
Oct 20, 2015, 2:35:04 PM10/20/15
to Dart Core Development, Florian Loitsch, Gilad Bracha

> Now the big missing piece is analyzer support. Before we can even try it out, we at least need phase 0 support—it needs to be able to parse the syntax without freaking out.

Not sure if one of the Analyzer folks is signed up already, but I don't mind helping out with this either.


> Personally, I think it should not warn. I think it's OK for the static analysis to not cover all things users can express dynamically. I don't think we'll get good end user feedback that configured types are important if the tools try to scare them away from even trying them.

> Florian and Gilad feel it's safer to start conservatively. If the warning is getting in their ways, users can let us know and we'll consider that a signal that configured types are desired.

Okay, I'm letting you all know: the warning will get in my way. The very first time I try and use it. :)

package:html needs configurable classes, not just functions.

I've mentioned this numerous times before. But I'm happy to continue brining this up. JavaScript frameworks can easily do server side rendering, Dart frameworks basically cannot. It's a quite popular feature, too (http://angularjs.blogspot.com/2015/09/angular-2-survey-results.html).
 


--
For other discussions, see https://groups.google.com/a/dartlang.org/
 
For HOWTO questions, visit http://stackoverflow.com/tags/dart
 
To file a bug report or feature request, go to http://www.dartbug.com/new

To unsubscribe from this group and stop receiving emails from it, send an email to misc+uns...@dartlang.org.

Brian Slesinsky

unread,
Oct 20, 2015, 2:48:40 PM10/20/15
to John Messerly, Dart Core Development, Florian Loitsch, Gilad Bracha
Well, you can if you abstract away all dependencies on dart:html with a virtual DOM. (I did that for TagTree.) But I agree, a server-side implementation of dart:html would be a big win.

- Brian


--
You received this message because you are subscribed to the Google Groups "Dart Core Development" group.
To unsubscribe from this group and stop receiving emails from it, send an email to core-dev+u...@dartlang.org.

Bob Nystrom

unread,
Oct 20, 2015, 3:12:22 PM10/20/15
to John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
+analyzer folks since they are the ones who will ultimately implement this.

On Tue, Oct 20, 2015 at 11:34 AM, 'John Messerly' via Dart Core Development <core...@dartlang.org> wrote:

> Now the big missing piece is analyzer support. Before we can even try it out, we at least need phase 0 support—it needs to be able to parse the syntax without freaking out.

Not sure if one of the Analyzer folks is signed up already, but I don't mind helping out with this either.


> Personally, I think it should not warn. I think it's OK for the static analysis to not cover all things users can express dynamically. I don't think we'll get good end user feedback that configured types are important if the tools try to scare them away from even trying them.

> Florian and Gilad feel it's safer to start conservatively. If the warning is getting in their ways, users can let us know and we'll consider that a signal that configured types are desired.

Okay, I'm letting you all know: the warning will get in my way. The very first time I try and use it. :) 

package:html needs configurable classes, not just functions.

I've mentioned this numerous times before. But I'm happy to continue brining this up. JavaScript frameworks can easily do server side rendering, Dart frameworks basically cannot. It's a quite popular feature, too (http://angularjs.blogspot.com/2015/09/angular-2-survey-results.html).

+1. I wouldn't have added configured types to the proposal if I didn't think it was important. If we can't provide detailed static checking for them (yet: until we implement phase 2), I think we should at least allow them without yelling at the user.

A warning that is basically "Warning: The Dart team hasn't implemented this yet." isn't helpful to users.

– bob

Brian Slesinsky

unread,
Oct 20, 2015, 3:26:53 PM10/20/15
to Bob Nystrom, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
On Tue, Oct 20, 2015 at 12:11 PM, 'Bob Nystrom' via Dart Core Development <core...@dartlang.org> wrote:

A warning that is basically "Warning: The Dart team hasn't implemented this yet." isn't helpful to users.

Not sure what you mean? When a user is attempting to use a feature that we haven't implemented yet, I think it's helpful to tell them what's up rather than let them struggle with it on their own. Perhaps the question is how to deliver the message?

John Messerly

unread,
Oct 20, 2015, 3:39:11 PM10/20/15
to Brian Slesinsky, Bob Nystrom, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
Brian: it's the static validation that wouldn't be implemented. The feature would still work at runtime. It's up to you to match up your configuration specific types carefully.


Brian Slesinsky

unread,
Oct 20, 2015, 3:58:57 PM10/20/15
to John Messerly, Bob Nystrom, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
But the user would still be wondering why their IDE isn't warning them about stuff.

It's disconcerting when static analysis mysteriously fails; it makes the IDE look flaky and/or broken.

- Brian

Bob Nystrom

unread,
Oct 20, 2015, 4:19:58 PM10/20/15
to Brian Slesinsky, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
On Tue, Oct 20, 2015 at 12:58 PM, Brian Slesinsky <skyb...@google.com> wrote:
But the user would still be wondering why their IDE isn't warning them about stuff.

I think by the time we get this feature in front of real end users, we'll want to have a complete static analysis story that covers the things we want to support.

The question is what is our incremetal implementation plan for the experiment? If halfway through the implementation we add a warning to code that we can't statically check but that does work dynamically and does exercise a use case users have repeatedly, insistently, clearly, even in this very thread, requested, I think we're just spending effort making our own lives worse.

It's disconcerting when static analysis mysteriously fails; it makes the IDE look flaky and/or broken.

It's not failing, it's just one kind of static analysis that doesn't exist. There's an infinite number of static analyses we could imagine adding (null checking, spelling errors, unnecessary use of async/await, etc.). If we don't do one, I don't think that looks like a breakage, it's just a thing that's not there.

– bob




Erik Ernst

unread,
Oct 21, 2015, 3:56:04 AM10/21/15
to Bob Nystrom, Brian Slesinsky, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
The issue is whether you want a language mechanism that introduces overly complex semantics.

Just allowing for configurable imports of non-type values (such as functions, but not classes) ensures that the static analysis and the semantics is easy. You can use factory constructors etc. to make this mechanism quite powerful (hint: today you can say `new List..`, but you will actually get an instance of a configurably imported list class).

Allowing for configurable imports of classes and other types implies that every expression in the importing library will potentially have a significantly different meaning in every possible configuration, and that means that programmers must have every one of those meanings in mind whenever they read or write any code. For instance, a given identifier could be a global const in one configuration and an inherited getter in another configuration.

Allowing for that situation to establish itself as the de facto standard (that we can't get rid of again) is not a small issue. So there is a big difference between saying "you cannot configurably import types" and "go ahead, but we won't check whether you do it correctly".

  best regards,

--
Erik Ernst  -  Google Danmark ApS
Skt Petri Passage 5, 2 sal, 1165 København K, Denmark
CVR no. 28866984

John Messerly

unread,
Oct 21, 2015, 1:30:15 PM10/21/15
to Erik Ernst, Bob Nystrom, Brian Slesinsky, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
There is some risk. Maybe I mess up, and the command line configuration of HTML doesn't work for someone, because I botched an API somewhere. Is that really what we should be afraid of? Right now, the command line configuration of HTML doesn't work at all. How is that better for anyone?

The risks here are entirely to the package's authors and its users. If they are willing to accept the risk, because the value it provides, then that should be their choice.

Choosing not to support highly sought after use cases also carries huge risk. But that kind of risk is the existential kind. Seems much, much worse.

Brian Slesinsky

unread,
Oct 21, 2015, 2:19:07 PM10/21/15
to John Messerly, Erik Ernst, Bob Nystrom, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
On Wed, Oct 21, 2015 at 10:29 AM, John Messerly <jmes...@google.com> wrote:

There is some risk. Maybe I mess up, and the command line configuration of HTML doesn't work for someone, because I botched an API somewhere. Is that really what we should be afraid of? Right now, the command line configuration of HTML doesn't work at all. How is that better for anyone?

The risks here are entirely to the package's authors and its users. If they are willing to accept the risk, because the value it provides, then that should be their choice.

John, I know you want this feature but please don't try to minimize the impact like this. This is a *language change*. In general, every language feature affects every other and complexity multiplies. For any other new language feature, we will have to ask "okay, how does that work when you combine it with configurable imports?" Everyone who writes tools to work with Dart code has to support it. Everyone who learns Dart will have to understand it sooner or later. I'm not looking forward to explaining it.

- Brian

John Messerly

unread,
Oct 21, 2015, 2:23:06 PM10/21/15
to Brian Slesinsky, Erik Ernst, Bob Nystrom, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
Okay, but practically every language has a way to load different configurations of libraries at runtime. Sometimes without any static validation. See also, Java class loaders, .NET assembly loading, importing in Python/Ruby, various techniques in JS, etc, etc. This is a problem we created for ourselves.

Erik Ernst

unread,
Oct 22, 2015, 6:31:35 AM10/22/15
to John Messerly, Brian Slesinsky, Bob Nystrom, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
But the issue is not whether there will be a mechanism for configurable imports, everybody wants that. The issue is whether it should be allowed to provide several deeply different binding environments that a given piece of code is embedded into. (Concretely, in one configuration an occurrence of an identifier `foo` in the body of a method is a global const, in another configuration it is an inherited getter; and even if the type of `foo` is denoted `A` in both cases, that `A` could stand for different types in the two configurations). We can come up with a truckload of rules intended to make those binding environments "compatible", but that ain't going to be simple --- and it is exactly this source of complexity that I'm worried about.

Btw, the main problem is not that it is hard to write those rules, the main problem is that all programmers must understand the remaining leeway (that is, the extent to which the same piece of code can mean different things in different configurations) when they read or write any such code.

Here's an illustration that might be useful: If configurable imports are allowed to import types (and _especially_ if they are totally unchecked for "sameness") then the mechanism is similar to the following construct in C:

```
    #ifdef defined(CONFIGURABLE_IMPORT_A)
    #include <alternativeA.h>
    #elif defined(CONFIGURABLE_IMPORT_B)
    #include <alternativeB.h>
    ...
    #endif
```

and it is going to be hard work to make it behave much better than that. ;-)

Bob Nystrom

unread,
Oct 22, 2015, 12:03:38 PM10/22/15
to Erik Ernst, John Messerly, Brian Slesinsky, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha

On Thu, Oct 22, 2015 at 3:31 AM, Erik Ernst <eer...@google.com> wrote:
Here's an illustration that might be useful: If configurable imports are allowed to import types (and _especially_ if they are totally unchecked for "sameness") then the mechanism is similar to the following construct in C:

```
    #ifdef defined(CONFIGURABLE_IMPORT_A)
    #include <alternativeA.h>
    #elif defined(CONFIGURABLE_IMPORT_B)
    #include <alternativeB.h>
    ...
    #endif
```

and it is going to be hard work to make it behave much better than that. ;-)

I understand you're trying to make an argument against configurable types. But saying that the proposed feature lets users be as expressive as a feature in the world's most successful cross-platform language sounds pretty great to me.

– bob

Erik Ernst

unread,
Oct 22, 2015, 12:55:46 PM10/22/15
to Bob Nystrom, John Messerly, Brian Slesinsky, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
You'd expect C to be low-level: There are no guarantees about the heap and stack structure beyond bit vectors (if you find 4 bytes in memory they may or may not be a pointer to anything, or a meaningful integer value, etc). C style macros are similarly low-level, so they can mess around with the syntactic structure as much as you want (one macro could give you a starting '{' and the corresponding '}' could come from a different macro). `#ifdef` and textual include just fits into that picture: It is a textually based mechanism---in that sense it is simple---but its semantics is infinitely complex (just try to type check C code with `#ifdef`s, as opposed to running the pre-processor to eliminate the `#ifdef`s and then running the type checker on the output).

That is a perfectly useful niche for a programming language to inhabit, and C has been doing that successfully for many years.

But Dart is a less low-level programming language in so many ways, and I think it would fit badly to make it so low-level in this particular respect.

Brian Slesinsky

unread,
Oct 22, 2015, 1:11:18 PM10/22/15
to Bob Nystrom, Erik Ernst, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
Regarding configurable imports and classes, here's my take on it:

This is a feature that allows us to substitute one class for another. The feature it's most similar to is inheritance, which allows us to substitute one *instance* for another. They aren't the same, but to the extent possible, the rules for inheritance and class substitution should be similar. We should be able to explain class substitution with an analogy to inheritance.

Some differences:
 - no super calls or mixins
 - we also substitute constructors, static methods and static fields. (This is probably the biggest reason why we couldn't just use top-level factory functions and inheritance. The big win is that we don't need to deal with a weird factory API or dependency injection.)
 - there is no equivalent of noSuchMethod() for static members.

It seems like the goal for v1 should be to do static checking that's roughly similar to what we can do when checking a subclass against a superclass in Dart. It may not be perfect, but it shouldn't be worse.

This isn't such a high bar since Dart's static checks for inheritance are pretty loose. Perhaps we can use similar techniques?

- Brian

Gilad Bracha

unread,
Oct 22, 2015, 1:24:19 PM10/22/15
to Brian Slesinsky, Bob Nystrom, Erik Ernst, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch
Yes, since Dart types are unsound, we could apply the same rules.  It would be the most sensible thing in any case, but AFIK the proposal did not spell that out. 

Gilad Bracha

unread,
Oct 22, 2015, 1:26:32 PM10/22/15
to Brian Slesinsky, Bob Nystrom, Erik Ernst, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch
Note however that DDC wants to apply sound rules, and that won't work without a global check of the specific configuration deployed. That can be done too I believe. 

Bob Nystrom

unread,
Oct 22, 2015, 1:55:39 PM10/22/15
to Florian Loitsch, Gilad Bracha, Brian Slesinsky, Erik Ernst, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development

On Thu, Oct 22, 2015 at 10:43 AM, Florian Loitsch <floi...@google.com> wrote:
The main reason for exporting classes is to extend "Element" from dart:html. Extending a class requires a lot of checks:
- are the invoked constructors generative? That is, if a subclass invokes super.foo, then there better be a generative constructor of that name. Otherwise the subclassing won't work.

Yes.
 
- we have to check that every member has a correspondent member in the config-class, and that the signatures are identical. Even minor changes like adding or changing optional arguments could yield a compilation error if they clash with subclasses.

Yes.
 
- when using exported types as mixins we furthermore have to know what the superclass of the exported type is.

Yes.

All of these make the compatibility checks tight, but I don't see how that makes them complex to specify and implement. Basically, two classes are compatible if and only if:
  1. They have the exact same public API. Same instance and static members with the same signatures. Same superclasses, mixins, implemented interfaces, type parameters, type parameter bounds.
  2. They have the same capabilities, where "capability" means the things a class implicitly supports or does not support:
    1. Can I be extended? True if the class has a generative constructor.
    2. Can I be mixed in? False if the class has a non-default generative constructor. (Or fields?)
There are probably a couple of capabilities and wrinkles I'm not thinking of, but that's the idea. We may be able to loosen some of these restrictions, but my intent is to start off highly restrictive but simple and then loosen if we see the need and see that it's safe.

– bob

Gilad Bracha

unread,
Oct 22, 2015, 1:56:01 PM10/22/15
to Florian Loitsch, Brian Slesinsky, Bob Nystrom, Erik Ernst, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development
Mismatches of arity, named/optional parameters will yield bad behavior (you won't override the right thing etc.).  Mismatched constructors will yield no such method.  Mixins are will lack appropriate constructors.   My preferred take on this is to use the standard rules and then do a global check on the configuration. This is very similar to what I want to do with first class libraries. It is not entirely trivial, as it has never been done before. One needs to establish that this is workable for both implementors and users.

Alternatively, we can impose much stricter and limiting rules. We should certainly decide that before moving forward with configurable types.

On Thu, Oct 22, 2015 at 10:43 AM Florian Loitsch <floi...@google.com> wrote:
I'm not sure I completely understand the analogy with inheritance, but I think that the rules for type-exports are more difficult (or stricter) than anything we have currently in the system.
The main reason for exporting classes is to extend "Element" from dart:html. Extending a class requires a lot of checks:
- are the invoked constructors generative? That is, if a subclass invokes super.foo, then there better be a generative constructor of that name. Otherwise the subclassing won't work.
- we have to check that every member has a correspondent member in the config-class, and that the signatures are identical. Even minor changes like adding or changing optional arguments could yield a compilation error if they clash with subclasses.
- when using exported types as mixins we furthermore have to know what the superclass of the exported type is.

These are just some things of the top of my head. There are probably others.

Gilad Bracha

unread,
Oct 22, 2015, 1:59:06 PM10/22/15
to Florian Loitsch, Brian Slesinsky, Bob Nystrom, Erik Ernst, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development
Just saw Bob's response. So, yes, we can go with much tighter rules. It's unfortunate that these rules differ from the normal subclassing rules, but they do make it easier to implement, and allow a kind of closed-form typechecking that is not possible in the general case. If that is the intent, we should specify these tighter rules and implement them.

Bob Nystrom

unread,
Oct 22, 2015, 2:04:22 PM10/22/15
to Gilad Bracha, Florian Loitsch, Brian Slesinsky, Erik Ernst, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development

On Thu, Oct 22, 2015 at 10:58 AM, Gilad Bracha <gbr...@google.com> wrote:
It's unfortunate that these rules differ from the normal subclassing rules, but they do make it easier to implement, and allow a kind of closed-form typechecking that is not possible in the general case. If that is the intent, we should specify these tighter rules and implement them.

Yes, my goal was always to have simple rules that could be checked without needing any kind of whole-program analysis. That does raise the burden on the user to write configured types that are very tightly matched to each other, but I think that's perfectly fine.

This is a powerful feature and I think it will mostly be used by sophisticated library developers who can handle the restrictions. Of course, we shouldn't go out of our way to make it hard to use, but if we give them tools that let them express exactly what they want, they'll be willing to do some grunt work to use them.

– bob

Leaf Petersen

unread,
Oct 22, 2015, 2:04:30 PM10/22/15
to Erik Ernst, John Messerly, Brian Slesinsky, Bob Nystrom, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
Here's an illustration that might be useful: If configurable imports are allowed to import types (and _especially_ if they are totally unchecked for "sameness") then the mechanism is similar to the following construct in C:

```
    #ifdef defined(CONFIGURABLE_IMPORT_A)
    #include <alternativeA.h>
    #elif defined(CONFIGURABLE_IMPORT_B)
    #include <alternativeB.h>
    ...
    #endif
```

As I understand the proposal, this is the wrong analogy to C entirely.  A closer C analogy is:

interface.h
// Define the interface here

implementation.c
#ifdef CONFIGURABLE_IMPORT_A
#include "alternativeA.c"
#else
#ifdef CONFIGURABLE_IMPORT_B
#include "alternativeB.c"
#endif
#endif

There is a single interface specified, with different implementations of that interface selected via configuration.  It's true that the runtime semantics do allow you to bypass the static checking (just as in C you can bypass the header and access the underlying exported symbols directly) - I think this is a BAD IDEA, but then I would think that, wouldn't I... :)  

Unless I'm missing something, this is very similar to C++ class declarations.  You declare a class in a header file, and provide implementations for the methods in the .cc file.  There's nothing deeply hard about checking that the implementation correctly implements the interface.

-leaf

Brian Slesinsky

unread,
Oct 22, 2015, 2:48:49 PM10/22/15
to Bob Nystrom, Florian Loitsch, Gilad Bracha, Erik Ernst, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development
Another wrinkle: we're dealing with a form of class substitution that needs to interoperate with class inheritance. How does that work exactly?

The interface library might have:

library portable_html_interface;

class Element {
}

class DivElement extends Element {
}

If we substitute html:DivElement for DivElement and html:Element for Element, their inheritance trees need to be "equivalent" using some graph comparison algorithm so that "is" checks work as expected. So we should check that html:DivElement extends html:Element and complain if the edge isn't there. I'm not sure off the top of my head how to define this algorithm when some libraries use configurable interfaces and other libraries don't.

- Brian

Erik Ernst

unread,
Oct 23, 2015, 11:30:01 AM10/23/15
to Brian Slesinsky, Bob Nystrom, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
Checking for an inheritance-ish relation among unrelated classes is already a new concept, but it could certainly be spelled out. The relationship between the "library interface declaration" T and the "implementation declaration" I (where I needs to conform to T in some sense) would be similar to the relationship between an instance and a class, but not identical:

If we want to enable configurable import of types then we would need to create support for typed first class classes, i.e., we would need to define a way to specify class-types such that any given class would conform or not conform to that class-type. The library interface would need to contain a set of types (ordinary types corresponding to top-level functions/getters/setters, and class-types corresponding to classes), and each of the configuration alternatives would be checked for conformance to that library interface.

The requirements on class-types depend on the intended usage. (1) If they are only intended to allow the first class classes to be used as type annotations then we could use an ordinary class C as the class-type and 'D conforms to C' for any given first class class D would simply mean 'D is a subtype of C'. For greater flexibility, we could introduce a notion of structural equivalence, and then we could say that it means something like 'D is a structural subtype of C' which might mean "it would not cause any warnings to add `D` to the `implements` clause of C". With the former approach we would be able to trust checked mode data transfers (assignments to variables / passing of parameters / returning of values) to succeed except for the failing downcast that we can always have; so if you use a "proper value" in these cases, you'll be OK. With the latter approach (structural subtyping), checked mode would fail even in the cases where we provide an instance of the declared type, because the actual type will not be related to the declared type at all, it is just "similar" in structure. So we might want to introduce structural subtyping into the dynamic semantics of checked mode execution if we were to use the structural approach for checking library interface conformance.

(2) If class-types are intended to allow the first class classes to be used in creation of new instances then 'D conforms to C' must also imply that the set of constructors defined in D enable at least the same set of instance creation expressions as the ones implied by the class-type C. For instance, D must have a set of constructors that covers the same set of names, and the parameter list of each of them must "cover" the parameter list of the corresponding constructor in the C. For this purpose it may not be necessary to require that the constructors have any particular kind (generative or not).

(3) If class-types are intended to allow the first class classes to be used as supertypes then we need negative constraints: The set of abstract members in any D must not exceed the set of abstract members in C, and D can only itself be abstract if C is abstract. Unless we settle for "exactly the same set of declared names and signatures" we would need to find a way to say (in a class-type declaration) that certain members do not exist.

(4) If class-types are intended to allow the first class classes to be used as superclasses then we need to ensure that super-initializers in constructors in a given subclass of D will be well-formed, which means that C must specify not only the constructors and their argument lists, but it must also constrain some of them to be generative.

(5) If class-types are intended to allow the first class classes to be used in class-types in another library ... etc.etc. ;-)

It should be noted that the potential non-conformance between any given class D and a class-type C extends to the transitive closure of types referred by the interface of D, which means that each of those types will have to have their own class-type and their own conformance check.

If we decide that this all gets a tiny bit involved we may try to plug the hole early, and simply require that "it's all identical".

But if we require that you can only make a choice between several identical libraries then it ain't doing much to solve the configurability problem. ;-)

As far as I can see, this brings us back to a model that hasn't been discussed for a while: We could settle for an approach where the configurability support is exclusively concerned with filling in missing method implementations (that is, something very much like the current patch files).

That would, by the way, be a kind of natural extension of the "import only values" (such as functions, but not types) proposal that I've been advocating for a while: We would be configurably importing a number of functions (that is: method implementations).

Erik Ernst

unread,
Oct 23, 2015, 11:54:36 AM10/23/15
to Leaf Petersen, John Messerly, Brian Slesinsky, Bob Nystrom, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
On Thu, Oct 22, 2015 at 8:04 PM, Leaf Petersen <le...@google.com> wrote:
Here's an illustration that might be useful: If configurable imports are allowed to import types (and _especially_ if they are totally unchecked for "sameness") then the mechanism is similar to the following construct in C:

```
    #ifdef defined(CONFIGURABLE_IMPORT_A)
    #include <alternativeA.h>
    #elif defined(CONFIGURABLE_IMPORT_B)
    #include <alternativeB.h>
    ...
    #endif
```

As I understand the proposal, this is the wrong analogy to C entirely.  A closer C analogy is:

I must admit that the correspondence between C and Dart can't be perfect. So we'll have to "translate" the model in some sense.
 
interface.h
// Define the interface here

implementation.c
#ifdef CONFIGURABLE_IMPORT_A
#include "alternativeA.c"
#else
#ifdef CONFIGURABLE_IMPORT_B
#include "alternativeB.c"
#endif
#endif

There is a single interface specified,

But I was arguing that it is dangerous to let the plan "don't check anything about imported types, and let people build lots of code relying on that" is dangerous, so there is _no_ single interface.

We'll simply try to compile the chosen variant, and there will be no hints whether the other variants are the same or different with respect to name resolution and the meaning of type annotations, etc.

Of course, Dart doesn't syntactically separate the declaration of classes and their corresponding interfaces, so there is no concept corresponding to the (convention of having) corresponding *.h and *.c files, but importing *.h files and letting the linker create the underlying semantics seems to match quite nicely with the Dart semantics. Hence I saw no need to mention the *.c files.
 
with different implementations of that interface selected via configuration.

This would be the case if we were to have a library interface and a conformance check, but I was arguing that it is dangerous to totally omit such a check.

Besides, a C header file will allow you to abstract over function implementations (and "abstract over" global variable declarations, although there is no abstraction there) and to specify `struct` declarations, and that's sufficient to allow for interfacing with another C module. With Dart we would need abstraction over more complex entities, as mentioned above.
 
  It's true that the runtime semantics do allow you to bypass the static checking (just as in C you can bypass the header and access the underlying exported symbols directly)

In Dart we can use the type `dynamic` to access members like `x.y` or `x.y(..)` without any reference to evidence for the existence of a member `y`, not to mention checking its signature. That's actually somewhat similar to the ability to access declared entities in C via a locally postulated `external` declaration. Of course, the matter will be settled at link time in C and at runtime in Dart, but it's still bypassing the type system.
 
- I think this is a BAD IDEA, but then I would think that, wouldn't I... :)  

Of course, and thanks for that. ;-D
 
Unless I'm missing something, this is very similar to C++ class declarations.  You declare a class in a header file, and provide implementations for the methods in the .cc file.  There's nothing deeply hard about checking that the implementation correctly implements the interface.

C++ class declarations are much more like patch files---you provide (all the non-inline) method implementations in the *.cc file, but it is not supported to establish any kind of abstraction: You cannot add extra member functions or data members to the class in the *.cc file, you cannot declare them with a different signature, you cannot even change any given member function from "normal" to `virtual`. It only allows you to add implementation. So you don't actually get support for configurable imports of types, even if you create a configuration choice mechanism when you decide which *.o files to link together (hence: which *.cc file to use as the implementation of each *.h file).

  best regards,

Brian Slesinsky

unread,
Oct 23, 2015, 1:40:28 PM10/23/15
to Erik Ernst, Dart Core Development, John Messerly, Bob Nystrom
+core-dev, etc

On Fri, Oct 23, 2015 at 8:48 AM, Erik Ernst <eer...@google.com> wrote:
At this point I usually say "But when we want configuration-independent code it should have configuration independent types to work with", so we are trying to compensate for bad design: It should not be necessary to repeat all those types in an implementation-free and configuration independent setting, 'dart:html' should have been written like that in the first place.

I sort of agree that maybe it would be better to have written dart:html that way in the first place. However, I don't think it's realistic to design everything perfectly the first time to meet goals that didn't even exist at the time. Much better if we can allow class hierarchies to evolve in a decentralized way.

With the current type system, nobody outside the Dart team is allowed to add supertypes to classes in dart:html (or any other dart: library). It would require getting the Dart team to agree on a dart:portable-html library where we can put all the interface types. That seems hard. Since there are many possible interfaces, it seems unlikely that we could do it in a way that satisfies everyone. It makes it hard to experiment.

So how about this? Suppose we have a way to inject a supertype *without* using an interface library? Something like this (never mind the syntax):

abstract class PortableDivElement
   optionally implemented by DivElement from "dart:html" {

  ... declare the stuff you need ...
}

So if dart:html exists (or is loaded later) then PortableDivElement gets injected as a supertype of DivElement. If it's not, it isn't. Different teams could define PortableDivElement (or whatever name they give it) in different ways, depending on what "portable" means for them. After some experience using it, perhaps we could all agree on something, but if not that's okay.

This doesn't really give us everything we need to make a drop-in replacement for dart:html since we've given up on constructors. But it does allow most of what you can do with Go interfaces.

I already know places where I could have used this in TagTree and places where we could use it with protobufs at Google. Even today I will be working on a sticky issue with a cyclic dependency between the protobuf, analyzer, observe, and watcher packages that would go away if we had something like this.

- Brian

Lasse R.H. Nielsen

unread,
Oct 26, 2015, 5:35:58 AM10/26/15
to Erik Ernst, Brian Slesinsky, Bob Nystrom, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
On Fri, Oct 23, 2015 at 5:30 PM, 'Erik Ernst' via Dart Core Development <core...@dartlang.org> wrote:
Checking for an inheritance-ish relation among unrelated classes is already a new concept, but it could certainly be spelled out. The relationship between the "library interface declaration" T and the "implementation declaration" I (where I needs to conform to T in some sense) would be similar to the relationship between an instance and a class, but not identical:

If we want to enable configurable import of types then we would need to create support for typed first class classes, i.e., we would need to define a way to specify class-types such that any given class would conform or not conform to that class-type. The library interface would need to contain a set of types (ordinary types corresponding to top-level functions/getters/setters, and class-types corresponding to classes), and each of the configuration alternatives would be checked for conformance to that library interface.

The requirements on class-types depend on the intended usage. (1) If they are only intended to allow the first class classes to be used as type annotations then we could use an ordinary class C as the class-type and 'D conforms to C' for any given first class class D would simply mean 'D is a subtype of C'. For greater flexibility, we could introduce a notion of structural equivalence, and then we could say that it means something like 'D is a structural subtype of C' which might mean "it would not cause any warnings to add `D` to the `implements` clause of C". With the former approach we would be able to trust checked mode data transfers (assignments to variables / passing of parameters / returning of values) to succeed except for the failing downcast that we can always have; so if you use a "proper value" in these cases, you'll be OK. With the latter approach (structural subtyping), checked mode would fail even in the cases where we provide an instance of the declared type, because the actual type will not be related to the declared type at all, it is just "similar" in structure. So we might want to introduce structural subtyping into the dynamic semantics of checked mode execution if we were to use the structural approach for checking library interface conformance.

(2) If class-types are intended to allow the first class classes to be used in creation of new instances then 'D conforms to C' must also imply that the set of constructors defined in D enable at least the same set of instance creation expressions as the ones implied by the class-type C. For instance, D must have a set of constructors that covers the same set of names, and the parameter list of each of them must "cover" the parameter list of the corresponding constructor in the C. For this purpose it may not be necessary to require that the constructors have any particular kind (generative or not).(Here, "call-compatible" basically means that any correctly typed call on the original's signature is also a valid call on the replacement function, and the return type is com).


(3) If class-types are intended to allow the first class classes to be used as supertypes then we need negative constraints: The set of abstract members in any D must not exceed the set of abstract members in C, and D can only itself be abstract if C is abstract. Unless we settle for "exactly the same set of declared names and signatures" we would need to find a way to say (in a class-type declaration) that certain members do not exist.

(4) If class-types are intended to allow the first class classes to be used as superclasses then we need to ensure that super-initializers in constructors in a given subclass of D will be well-formed, which means that C must specify not only the constructors and their argument lists, but it must also constrain some of them to be generative.

(5) If class-types are intended to allow the first class classes to be used in class-types in another library ... etc.etc. ;-)

Yeah, it's a rat-hole of details.

I guess is that the basic rule is that "replacing the original library with the replacement library will not introduce any static warnings/errors". If we can describe a relation between the original library and the replacement library that guarantees this, and which can be explained to humans, then I think we are set.

I don't think that's possible in general, so we should probably make one assumption:

- We assume that the replaced library is replaced everywhere in the program and with the same replacement (so we don't replace the library in one import, but import it directly in another, and then assume that the types match). That is, *all* imports/exports of a library are conditional with the same tests and replacements libraries - which has the special case of not being conditional at all - no tests and no replacements.

This allows us to go from a local comparison of the relation between the original and the replacement to a global property of the program where the replacement happens. We are actually replacing *the library* with a replacement everywhere in the program.

Also, I don't necessarily think all warnings are bad, but for now, let's go with "introduces no warnings".

So, we want the property that  
  if library L' is a valid replacement for library L,
  then for all valid programs P with no static warnings, P with L' instead of L
  has no static warnings or compile-time errors.
and we want to describe restrictions on L' (relative to L) which ensure this

So:
- All visible members of L must have a corresponding "equivalent" member in L'.
- No further visible members are allowed in L', otherwise it may cause a new naming conflict in a library which imports L/L' without a prefix.

Classes must be replaced with equivalent classes, and a class equivalence is defined inductively: A class C in L is equivalent to the similarly named class C' in L' if 
- their superclasses are equivalent or identical (due to mixin restrictions, which suggests that the mixin restriction is bad since it prevents having a private superclass).
- for each interface, I, of the C, C' has an interface, I', that is equivalent to I (otherwise assigning of something of type C' to a variable of type I would warn).
- If C is non-abstract, so is C'.
- All public members of C have equivalent (same-named) members in C'.
  - Static functions must be call compatible
  - Constructors must be call compatible, and if generative in C they must
    also be generative in C'.
  - Instance members must have *exactly* the same parameter structure
    (same number of required and optional positional parameters, same
    number and names of named parameters), with equivalent parameter
    types and return type. Adding extra parameters may cause warnings
    if a subclass adds members of the same name.
- No new public static members! 
  The class must have exactly the same public static members (because
  it's a warning if a subclass has an instance method with the same
  accessible name as a static member of a superclass). 
  (I don't see a restriction against public constructors, though, and this
  warning is really something we should consider dropping).
- No new public instance members. 
  These can conflict with new members in subclasses.
  Any added private instance members must be non-abstract.
- If a member is non-abstract in C, it must also be non-abstract in C'.

Typedefs are equivalent if they have the same signature structure (number of required and optional positional parameters, number and names of named parameters), and the parameter types and return type are each equivalent types.

Top-level functions (getters and setters included) must be call compatible.

Being call-compatible means that any call on the function in L must be compatible with the signature of the corresponding function in L' - the one in L' must accept at least the same arguments (but may accept more) and the type parameters and return type must be type-equivalent.


So, the requirement to not introduce warnings induces some  strict requirements (instance members are "identical", modulo type replacement, no new public static members) derived from the static warnings required by the language specification. It's not "it's all identical", but it is for instance members.

It might actually be too much, but it would be a start.


It's not clear what we should do for programs where we do import the same library both conditionally and unconditionally, I guess it's a case of caveat-coder.

I could see myself doing that:
   import "foo.dart" 
      "foo-spec.dart" if specializedCase
and in foo-spec.dart:
   import "foo.dart" as foo;
   export "foo.dart" hide A;
   class A extends foo.A { specializedImpl(){} }
which would be prevented by the "must have same superclass due to mixins" restriction.

/L



It should be noted that the potential non-conformance between any given class D and a class-type C extends to the transitive closure of types referred by the interface of D, which means that each of those types will have to have their own class-type and their own conformance check.

If we decide that this all gets a tiny bit involved we may try to plug the hole early, and simply require that "it's all identical".

But if we require that you can only make a choice between several identical libraries then it ain't doing much to solve the configurability problem. ;-)

As far as I can see, this brings us back to a model that hasn't been discussed for a while: We could settle for an approach where the configurability support is exclusively concerned with filling in missing method implementations (that is, something very much like the current patch files).

That would, by the way, be a kind of natural extension of the "import only values" (such as functions, but not types) proposal that I've been advocating for a while: We would be configurably importing a number of functions (that is: method implementations).


On Thu, Oct 22, 2015 at 7:10 PM, Brian Slesinsky <skyb...@google.com> wrote:
Regarding configurable imports and classes, here's my take on it:

This is a feature that allows us to substitute one class for another. The feature it's most similar to is inheritance, which allows us to substitute one *instance* for another. They aren't the same, but to the extent possible, the rules for inheritance and class substitution should be similar. We should be able to explain class substitution with an analogy to inheritance.

Some differences:
 - no super calls or mixins
 - we also substitute constructors, static methods and static fields. (This is probably the biggest reason why we couldn't just use top-level factory functions and inheritance. The big win is that we don't need to deal with a weird factory API or dependency injection.)
 - there is no equivalent of noSuchMethod() for static members.

It seems like the goal for v1 should be to do static checking that's roughly similar to what we can do when checking a subclass against a superclass in Dart. It may not be perfect, but it shouldn't be worse.

This isn't such a high bar since Dart's static checks for inheritance are pretty loose. Perhaps we can use similar techniques?

- Brian




--
Erik Ernst  -  Google Danmark ApS
Skt Petri Passage 5, 2 sal, 1165 København K, Denmark
CVR no. 28866984

--
You received this message because you are subscribed to the Google Groups "Dart Core Development" group.
To unsubscribe from this group and stop receiving emails from it, send an email to core-dev+u...@dartlang.org.



--
Lasse R.H. Nielsen - l...@google.com  
'Faith without judgement merely degrades the spirit divine'
Google Denmark ApS - Frederiksborggade 20B, 1 sal - 1360 København K - Denmark - CVR nr. 28 86 69 84

Brian Wilkerson

unread,
Oct 26, 2015, 10:14:05 AM10/26/15
to Lasse R.H. Nielsen, Erik Ernst, Brian Slesinsky, Bob Nystrom, John Messerly, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
Lasse,

I guess is that the basic rule is that "replacing the original library with the replacement library will not introduce any static warnings/errors". If we can describe a relation between the original library and the replacement library that guarantees this, and which can be explained to humans, then I think we are set.

I don't think that's possible in general, so we should probably make one assumption:

- We assume that the replaced library is replaced everywhere in the program and with the same replacement (so we don't replace the library in one import, but import it directly in another, and then assume that the types match). That is, *all* imports/exports of a library are conditional with the same tests and replacements libraries - which has the special case of not being conditional at all - no tests and no replacements.

This allows us to go from a local comparison of the relation between the original and the replacement to a global property of the program where the replacement happens. We are actually replacing *the library* with a replacement everywhere in the program.

Agreed. But that's a problem for a tool like the analyzer because we don't have a notion of a single "root" library, and hence global properties tend to be prohibitively expensive to check for.

I think that the root of this problem is that we're defining a special kind of library (the interface library) without doing anything explicit in that library to mark it as being special. All of the information that it even is an interface library lives in another library.

But that isn't necessary. We could move the information about the configuration of the interface library into the interface library itself, making it impossible to violate this global property. I'm not proposing specific syntax, but for the purpose of illustration, this is what it might look like to do that (using the example from the DEP). The interface library would look something like the following:

// warn.dart
if (dart.library.io) 'warn_io.dart' // intentional lack of semicolon
if (dart.library.html) 'warn_html.dart';

String warn(String message, {bool severe}) {
  if (severe) message = message.toUppercase();
  showMessage(message);
}

void showMessage(String message) {}

and one of the configuration specific libraries would look like:

// warn_io.dart
implements 'warn.dart';

import 'dart:io' as io;

void showMessage(String message) {
  io.stderr.writeln(message);
}

Tools could know that
1. warn.dart was a library with configuration specific implementations,
2. there are exactly two such implementations, and
3. warn_io.dart should not be imported from anywhere other than 'warn.dart'.

It would also allow tools like the analyzer to be pointed at configuration specific libraries and be able to find all of the information needed to analyze them (including errors if they aren't compatible with the interface library).

Brian

Bob Nystrom

unread,
Oct 26, 2015, 3:50:36 PM10/26/15
to Lasse R.H. Nielsen, Erik Ernst, Brian Slesinsky, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
On Mon, Oct 26, 2015 at 2:35 AM, Lasse R.H. Nielsen <l...@google.com> wrote:
(5) If class-types are intended to allow the first class classes to be used in class-types in another library ... etc.etc. ;-)

Yeah, it's a rat-hole of details.

I guess is that the basic rule is that "replacing the original library with the replacement library will not introduce any static warnings/errors".

I think it's important to be clear that the proposal does the configuration at the import (or export) level, not the library itself. My earlier "external libraries" proposal tried to make the library itself configured, and I think that's actually a lot hairier.

 
- We assume that the replaced library is replaced everywhere in the program and with the same replacement (so we don't replace the library in one import, but import it directly in another, and then assume that the types match).

I believed we discussed this case on another thread a while back. My belief is that with the current proposal, you can directly import a library that is imported configurably elsewhere. The proposal doesn't go out of its way to prevent that.

Here's an example that, I think, covers all the cases:

import 'foo.dart' if (some.config) 'foo_other.dart' as foo_config;
import 'foo.dart' as foo;
import 'foo_other.dart' as foo_other;

main() {
  foo_config.Foo f1 = new foo.Foo();
  foo_config.Foo f2 = new foo_other.Foo();
  foo.Foo        f3 = new foo_other.Foo();
}

  • Line f1 has no static warning. It works in checked mode only if some.config is false.
  • Line f2 has a static warning. It works in checked mode only if some.config is true.
  • Line f3 has a static warning. It does not work in checked mode.
It might be nice if line f1 had a static warning too. I think it would be possible to change the proposal to allow that by saying that a reference to a library somehow takes the configuration URIs into account when canonicalizing so tha the first two imports here are not considered the same.

But I'm also OK with not doing that and keeping things simpler. This is a powerful feature and is mainly aimed at library/framework authors. I'm fine with it having a few pointy corners if it leads to something that is both simple and expressive.

 
So, the requirement to not introduce warnings induces some  strict requirements (instance members are "identical", modulo type replacement, no new public static members) derived from the static warnings required by the language specification. It's not "it's all identical", but it is for instance members.

It might actually be too much, but it would be a start.

I'm totally fine with the rules being strict and simple at first. We can widen them if we need to but I don't want to speculatively.
 
It's not clear what we should do for programs where we do import the same library both conditionally and unconditionally, I guess it's a case of caveat-coder.

Exactly right.

"Doctor, it hurts if I do this."

"Well, don't do that!"

– bob

Bob Nystrom

unread,
Oct 26, 2015, 4:00:18 PM10/26/15
to Brian Wilkerson, Lasse R.H. Nielsen, Erik Ernst, Brian Slesinsky, John Messerly, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
On Mon, Oct 26, 2015 at 7:14 AM, Brian Wilkerson <brianwi...@google.com> wrote:
I think that the root of this problem is that we're defining a special kind of library (the interface library) without doing anything explicit in that library to mark it as being special. All of the information that it even is an interface library lives in another library.

It actually isn't a special library. This is important because we want to use this with existing libraries like "dart:html". Configuration is a part of the reference to the library. That seems a little counter-intuitive, but I think it leads to a much simpler proposal.
 

But that isn't necessary. We could move the information about the configuration of the interface library into the interface library itself, making it impossible to violate this global property. I'm not proposing specific syntax, but for the purpose of illustration, this is what it might look like to do that (using the example from the DEP). The interface library would look something like the following:

// warn.dart
if (dart.library.io) 'warn_io.dart' // intentional lack of semicolon
if (dart.library.html) 'warn_html.dart';

String warn(String message, {bool severe}) {
  if (severe) message = message.toUppercase();
  showMessage(message);
}

void showMessage(String message) {}

and one of the configuration specific libraries would look like:

// warn_io.dart
implements 'warn.dart';

import 'dart:io' as io;

void showMessage(String message) {
  io.stderr.writeln(message);
}

This is pretty much what the external library proposal was about. It's a little more terse and intuitive, but I also think it's more complex in its semantics. You can run into weird questions around canonicalization, relative URIs, exports, etc.


Tools could know that
1. warn.dart was a library with configuration specific implementations,
2. there are exactly two such implementations, and
3. warn_io.dart should not be imported from anywhere other than 'warn.dart'.

In practice this is how we expect users to use config-specific imports, but the propsal doesn't require this. The static checking is really a facet of the configured directive. In other words, given two libraries:

// a.dart
someFunction(oneArg) { ... }

// b.dart
someFunction(two, args) { ... }

There are no static warnings here. Everything's great. But if the analyzer elsewhere sees an import like:

import 'a.dart' if (some.config) 'b.dart';

Then, that import leads to compatibility checking between 'a.dart' and 'b.dart'.

You can think of it sort of like structural types. They are unrelated types until the point that you try to "assign" one to the other (by doing a configured import), and it's the assignment that leads to the static checking.

There are even complex examples you can contrive where the same library is used in different configured imports in different ways:

import 'a.dart' if (config.a) 'a_and_b.dart' show a;
import 'b.dart' if (config.b) 'a_and_b.dart' show b;

Here, a_and_b.dart is "implementing" two different "interfaces".

Cheers!

– bob

Brian Slesinsky

unread,
Oct 26, 2015, 4:03:33 PM10/26/15
to Bob Nystrom, Lasse R.H. Nielsen, Erik Ernst, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
Seems like the problem is that it *doesn't* hurt when you do that? If there's no pain, it's harder to avoid injuring yourself accidentally. And it might not be you - someone might be affected weeks later and on a different team. Static warnings are all about providing early feedback.

- Brian

Lasse R.H. Nielsen

unread,
Oct 26, 2015, 5:17:29 PM10/26/15
to Bob Nystrom, Erik Ernst, Brian Slesinsky, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
On Mon, Oct 26, 2015 at 8:50 PM, Bob Nystrom <rnys...@google.com> wrote:

On Mon, Oct 26, 2015 at 2:35 AM, Lasse R.H. Nielsen <l...@google.com> wrote:
(5) If class-types are intended to allow the first class classes to be used in class-types in another library ... etc.etc. ;-)

Yeah, it's a rat-hole of details.

I guess is that the basic rule is that "replacing the original library with the replacement library will not introduce any static warnings/errors".

I think it's important to be clear that the proposal does the configuration at the import (or export) level, not the library itself. My earlier "external libraries" proposal tried to make the library itself configured, and I think that's actually a lot hairier.

 
- We assume that the replaced library is replaced everywhere in the program and with the same replacement (so we don't replace the library in one import, but import it directly in another, and then assume that the types match).

I believed we discussed this case on another thread a while back. My belief is that with the current proposal, you can directly import a library that is imported configurably elsewhere. The proposal doesn't go out of its way to prevent that.

True, and that is why the assumption is there - we check as if the assumption is true, because if it isn't, the type checking gets much worse.

However, I talked to Florian Loitsch and his idea of how to handle conditionally imported libraries does not have this problem. 
Basically, it's a "worst case" analysis - for any fixed library, the configured import *can* be different from that library, so assume that it is different. Each conditional library import/export is considered a unique library for static type checking. The types introduced by that import/export are not compatible with any other types, not even from another (conditional or not) import of the same libraries. It is as if the "conditional library choice" introduces a new library with the same "library signature" as the default library.
 
In practice, most conditional library choices will probably have a single conditional export in a library that everybody imports, keeping the configuration to a single place.

 

Here's an example that, I think, covers all the cases:

import 'foo.dart' if (some.config) 'foo_other.dart' as foo_config;
import 'foo.dart' as foo;
import 'foo_other.dart' as foo_other;

main() {
  foo_config.Foo f1 = new foo.Foo();
  foo_config.Foo f2 = new foo_other.Foo();
  foo.Foo        f3 = new foo_other.Foo();
}

  • Line f1 has no static warning. It works in checked mode only if some.config is false.

With the above approach, this is a static warning. The "foo_config" library is distinct from both "foo" and "foo_other" - because in practice it *can* be different from either, so the safe approach is to assume it is different from both. That will not introduce a new warning if we change the content to the "foo_other.dart" library because we already maximize the number of warnings.

It still works in checked mode if some.config is false, the above was only about static analysis.
 
  • Line f2 has a static warning. It works in checked mode only if some.config is true.
Agree
 
  • Line f3 has a static warning. It does not work in checked mode.
Agree.
 
It might be nice if line f1 had a static warning too. I think it would be possible to change the proposal to allow that by saying that a reference to a library somehow takes the configuration URIs into account when canonicalizing so tha the first two imports here are not considered the same.

That is one option, another is to not take them into account and consider each configured import/export as distinct from all other libraries.
 

But I'm also OK with not doing that and keeping things simpler. This is a powerful feature and is mainly aimed at library/framework authors. I'm fine with it having a few pointy corners if it leads to something that is both simple and expressive.

 
So, the requirement to not introduce warnings induces some  strict requirements (instance members are "identical", modulo type replacement, no new public static members) derived from the static warnings required by the language specification. It's not "it's all identical", but it is for instance members.

It might actually be too much, but it would be a start.

I'm totally fine with the rules being strict and simple at first. We can widen them if we need to but I don't want to speculatively.
 
It's not clear what we should do for programs where we do import the same library both conditionally and unconditionally, I guess it's a case of caveat-coder.

Exactly right.

"Doctor, it hurts if I do this."

"Well, don't do that!"

Or just consider them statically different. I think it could work.
/L

Bob Nystrom

unread,
Oct 26, 2015, 5:37:17 PM10/26/15
to Lasse R.H. Nielsen, Erik Ernst, Brian Slesinsky, John Messerly, Brian Wilkerson, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
On Mon, Oct 26, 2015 at 2:17 PM, Lasse R.H. Nielsen <l...@google.com> wrote:
However, I talked to Florian Loitsch and his idea of how to handle conditionally imported libraries does not have this problem. 
Basically, it's a "worst case" analysis - for any fixed library, the configured import *can* be different from that library, so assume that it is different. Each conditional library import/export is considered a unique library for static type checking. The types introduced by that import/export are not compatible with any other types, not even from another (conditional or not) import of the same libraries. It is as if the "conditional library choice" introduces a new library with the same "library signature" as the default library.

I considered that, but I was worried that two separate, identically-configured imports would be considered incompatible with each other, which seems annoying, but...
 
 In practice, most conditional library choices will probably have a single conditional export in a library that everybody imports, keeping the configuration to a single place.

Right, so it might be fine to do just what you suggest.

With the above approach, this is a static warning. The "foo_config" library is distinct from both "foo" and "foo_other" - because in practice it *can* be different from either, so the safe approach is to assume it is different from both. That will not introduce a new warning if we change the content to the "foo_other.dart" library because we already maximize the number of warnings.

If the analyzer folks like this, it sounds good to me too. At the very least, I'm for giving it a try and seeing how it feels to use it in practice.

Cheers!

– bob

Brian Wilkerson

unread,
Oct 26, 2015, 7:08:10 PM10/26/15
to Lasse R.H. Nielsen, Bob Nystrom, Erik Ernst, Brian Slesinsky, John Messerly, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
Lasse,

However, I talked to Florian Loitsch and his idea of how to handle conditionally imported libraries does not have this problem. 
Basically, it's a "worst case" analysis - for any fixed library, the configured import *can* be different from that library, so assume that it is different. Each conditional library import/export is considered a unique library for static type checking. The types introduced by that import/export are not compatible with any other types, not even from another (conditional or not) import of the same libraries. It is as if the "conditional library choice" introduces a new library with the same "library signature" as the default library.

Just to check, rather than assume...

If a single library contains both a conditional import and a conditional export of the same library with the same conditions, would they be considered to be the same library? (Or even two conditional imports with hide/show combinators and prefixes?) I'm expecting that the answer is "yes", and that we might even want to say that it's a static warning if the same URI appears in more than one conditional import/export (in a single library) and does not have the same conditions.

In practice, most conditional library choices will probably have a single conditional export in a library that everybody imports, keeping the configuration to a single place.

If every conditional import/export effectively introduces a new URI, then I think the pain caused by having multiple conditional exports will stop anyone from doing so. :-)

With the above approach, this is a static warning. The "foo_config" library is distinct from both "foo" and "foo_other" - because in practice it *can* be different from either, so the safe approach is to assume it is different from both. That will not introduce a new warning if we change the content to the "foo_other.dart" library because we already maximize the number of warnings.

The biggest disadvantage I can see is that the point at which the problem is detected can be very distant from the point at which the real problem occurred. I don't see any good alternative, though, and I think that analyzer can figure out the root cause of the problem so that we can report something useful (instead of just "the type Foo cannot be assigned to the type Foo").

Brian

Lasse R.H. Nielsen

unread,
Oct 27, 2015, 2:54:53 AM10/27/15
to Brian Wilkerson, Bob Nystrom, Erik Ernst, Brian Slesinsky, John Messerly, Paul Berry, Dan Grove, Dart Core Development, Florian Loitsch, Gilad Bracha
On Tue, Oct 27, 2015 at 12:08 AM, Brian Wilkerson <brianwi...@google.com> wrote:
Lasse,

However, I talked to Florian Loitsch and his idea of how to handle conditionally imported libraries does not have this problem. 
Basically, it's a "worst case" analysis - for any fixed library, the configured import *can* be different from that library, so assume that it is different. Each conditional library import/export is considered a unique library for static type checking. The types introduced by that import/export are not compatible with any other types, not even from another (conditional or not) import of the same libraries. It is as if the "conditional library choice" introduces a new library with the same "library signature" as the default library.

Just to check, rather than assume...

If a single library contains both a conditional import and a conditional export of the same library with the same conditions, would they be considered to be the same library?

From what I said: no. I can see how that is a problem.
So it's probably better to distinguish imports by their entire URI(condition URI)* sequence, and consider them equal if they have exactly the same conditions and URIs in the same order.
 
(Or even two conditional imports with hide/show combinators and prefixes?) I'm expecting that the answer is "yes",

So would a user, so maybe we should make it so.

There is still a workaround, which is to have the conditional library only once, as an export in a stand-alone helper library. In that case, everybody uses that same instance, so everything works.
 
and that we might even want to say that it's a static warning if the same URI appears in more than one conditional import/export (in a single library) and does not have the same conditions.

Maybe reasonable, but I can see cases where you want to import both the original type and possibly (but not always) a replacement implementation.
E.g.:
    import "someTypes.dart";
    import "someTypes.dart" if (logging) "loggingWrapperOfSomeTypes.dart" as impl;
    ...
          // may be logging wrapper class, may be orignal, always statically typed as original.
        SomeType get someType => new impl.SomeType();


In practice, most conditional library choices will probably have a single conditional export in a library that everybody imports, keeping the configuration to a single place.

If every conditional import/export effectively introduces a new URI, then I think the pain caused by having multiple conditional exports will stop anyone from doing so. :-)

Maybe that's a good thing. :)
 

With the above approach, this is a static warning. The "foo_config" library is distinct from both "foo" and "foo_other" - because in practice it *can* be different from either, so the safe approach is to assume it is different from both. That will not introduce a new warning if we change the content to the "foo_other.dart" library because we already maximize the number of warnings.

The biggest disadvantage I can see is that the point at which the problem is detected can be very distant from the point at which the real problem occurred. I don't see any good alternative, though, and I think that analyzer can figure out the root cause of the problem so that we can report something useful (instead of just "the type Foo cannot be assigned to the type Foo").

Yeah, that's never a good message. Using the same name about two different things in the same sentence means you have to qualify them. Just as in natural language: "Thomas didn't want to play with Thomas"  - won't fly there either :)

/L
Reply all
Reply to author
Forward
0 new messages