# `using` considered beneficial There has been talk lately of the idea that having `using` in programs is a problem, because it leads to breakage as programs change. However, `using` is not the problem here, and in fact, `using` is a very beneficial and useful feature. Where the problem comes from is when the exports of package change, they may end up colliding with exports from another package, leading to the dreaded "imports must be qualified" error. Rather than making peopled worried their code now needs a PhD to be qualified, we want to give people a tool to avoid the issue with conflicting imports directly: API versioning. ## Declaring an API version We already have the concept of program versions, but currently there is no way to indicate that at the granularity of an individual export. This proposal aims to change that, giving an optional mechanism that people can use to add new exports without needing to analyze if this will break other code. The first part of this proposal thus is extending the vocabulary for exports. Currently this only lists symbols. In this proposal, the vocabulary for export is extended to list version ranges and alternative (internal) names. First lets look at some examples for my sample package `CoolCodes` which is at `v1.4` currently, then lets discuss what each piece means: ``` module CoolCodes export cool_one, cool_two, "1.2", cool_new, cool_new_also, "1.2-1.2", cool_idea_2_but_then_removed export "1.3", cool_three_added, "1.2 - 1.3", cool_old as cool_new_also, "0", CoolCodeStruct global cool_one, cool_two, cool_new_also, cool_idea_2_but_then_removed, cool_three_added, cool_old, CoolCodeStruct end ``` And in my other packages, one or more of these: ``` using CoolCodes: "1.4" using CoolCodes using CoolCodes: cool_new_also as cool_actually_old, "1.4", CoolCodes, cool_new_also # import CoolCodes: "1.4" # import CoolCodes: CoolCodes import CoolCodes: "v1.4", cool_new_also, cool_old ``` # Explanation of what was `export`ed above In the `export` statement, first we see the package has always exported `cool_one` and `cool_two`. Then there is a separator entry "1.2" that applies to all subsequent entries, until the next separator. The version numbers are always either 1 or 2 digits, since patch release do not add new APIs in semver. This version number should correspond exactly with the next version being released, to avoid user and tooling confusion. Thus we see that starting in v1.2, it exported `cool_new` and `cool_new_also`. It also exported `cool_idea_2_but_then_removed` in v1.2, but removed it again in v1.3 (per semver, that should have required a bump in the major version number, which was not followed in our example here however). Next in v1.3, the API `cool_three_added` was added. And finally, in v1.4, it retroactively changed the global being exported as `cool_new_also` from v1.2 and v1.3 and instead defined it to be `cool_old`. Note that v1.4 still has the `export cool_new_also` symbol and users that access symbols directly (with `.` rather than `using`) will get the new object. But users which declare an older API for support will get a reference to `cool_old` instead when they imported `cool_new_also`. The expectation here is that if a package wants to rework an existing API, then can keep a shim version of the old API around, with a different name, and reexport it to old users of the old API with the original name. But give new users the new API with the same original name. And lastly, the code removes the version restriction with an explicit "0" (meaning all versions) and exports `CoolCodeStruct` to all versions. It has the same meaning as "0.0" or "" (an empty string). # Explanation of what was `using`ed above With the explanation of the exports above, the behavior of each of the `using` or `import` calls is hopefully already easy to guess. The first one declares that this should (lazily) import the symbols corresponding to the v1.4 exports. The second one does the same, but gets the v1.4 from the `[compat]` section of the Project.toml file, by picking the oldest API declared to be compatible, and only using symbols that are declared exported from that API version. Lastly, the list form is shown, to illustrate that it shares the same syntactically parsing behaviors as the `export` list, for (eager) imports. The behavior of `import` is similar to that of `using`, except the first 2 cases have no meaning (as there are no lazy imports) so are disallowed. The last case will import the symbol corresponding to that name in that version. Note also that it can also import a private symbol (`cool_old`) which is not present in any version. The package can make this annoying by declaring `export not_allowed as cool_old`, which would cause attempts to `import` the symbol `cool_old` to instead try to access `not_allowed` and not succeed. This is possible to work around (`const cool_old = CoolCodes.cool_old` or `get_cool_old() = CoolCodes.cool_old`), so this should be seen largely as an accidental obscurity than a API-control feature. In this example, it also means the Package.toml file would likely have needed to contain a `[compat]` entry specifying `v1.4-`, since `cool_old` was only added in v1.4. But the `CoolCodes` code v1.4 can specify that is has compatible API support back to v1.0 so that it can be used simultaneously by a package that requires an old version, and this code that requires the new version of the API. Because the Pkg resolver should have already solved for `[compat]` issues, there is no error for importing a later API than declared in the `exports`, say "1.14". The result will be the latest API that is declared in the current source code. That might not work out as well as desired. However, if the information provided to the Pkg resolver was correct, then code that explicitly imports a later API but declares compat with an earlier API implies it knows that any version in that range is okay. # Additional clarifications One important clarification to make is that `CoolCodes.cool_new_also` is not the same as the versioned import. Only symbols specified with `using` are handled this way. Other places where `getglobal` or `getfield` or any other way of accessing a binding is used will work as before. This ensures that reflection and other operators continue to work correctly as before. The user may freely access _any_ binding (public or private) from any module, as before, but they will lose the stability guarantees they can get from plain `using` to bring the correct symbol into scope. The user should be especially aware of this when they want to access a newer API than they declare compat with, as doing `isdefined` checks to directly access that functionality and bypass the version support proposed here may give misleading answers and make their package unreliable. # Future thoughts We may want to add an ability to express that the `using` is permitted to fail (maybe only on particular older versions of a package?), resulting in importing a special internal const binding with an undef value instead. This would provide a mechanism to allow existence-tests to continue to work. For example, to indicate that `cool_old` was optional before `1.4` it is first imported from `_`. This special syntax would import the binding `cool_old` if it exists, but if not would instead import the special binding `Core.:var"_"` (which is uniquely const but undef). Then to indicate it is required in new versions, it can be imported as `_` from new versions. This has no side-effects, since it is imported as the placeholder `_` and discarded, but throws an error if CoolCodes declares v1.4 but doesn't provide `cool_old`. ``` using CoolCodes: _ as cool_old "1.4", cool_old as _ @isdefined(cool_old) && println("CoolCodes has cool_old.") ```