Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scoped imports #4439

Closed
chocolateboy opened this issue May 20, 2017 · 87 comments
Closed

Scoped imports #4439

chocolateboy opened this issue May 20, 2017 · 87 comments

Comments

@chocolateboy
Copy link
Contributor

chocolateboy commented May 20, 2017

Crystal doesn't currently provide a way to control the scope of imported bindings e.g.:

cmd = import "command"
cmd.run "echo 'Hello, world!'"

The workaround pollutes the global namespace e.g.:

require "command"

Command.run "echo 'Hello, world!'"

This is fine in many cases, but can lead to surprises, not all of which are caught by the compiler:

Many languages and platforms provide a way to limit the scope of imports (as well as other conveniences such as renaming them to avoid collisions) e.g.:

  • Groovy
  • Kotlin
  • Node.js (CommonJS)
  • Python
  • Scala

In addition, Ruby provides refinements as a way to mitigate this problem.

A method for importing exported values has been proposed before, but the discussion quickly devolved into a defence of require and the Ruby culture of monkey patching. As a result, such proposals have (correctly, IMO) been rejected on the grounds that:

we won't change the require system

But there is no need to break/change require. The two concepts are not mutually exclusive e.g. one can do both in Node.js:

const _ = require('lodash') // scoped import
require('shelljs/global')   // global monkey-patch

I'd like to propose a separate/new method which supports scoped imports, and which complements (i.e. does not replace or override) require. If this proposal is going to be rejected, it should be rejected in its own right.

Implementation wise, there could be a new (file-scoped) hook:

main.cr

  cmd = import "command", shell: "zsh"

command.cr

private module Command
  class Runner 
    # ...
  end
end

macro imported(shell = "bash")
  Command::Runner.new(shell: shell)
end

See Also

@asterite
Copy link
Member

Yes, I think the current require system is far from ideal. I like ES6 import, and the way you import things in Elixir, Rust and Go. Crystal and Ruby seem to be more like C where you #include stuff and everything gets copy-pasted.

Please provide a detailed proposal of how to solve this, what changes are needed, and a working PR with it.

@RX14
Copy link
Contributor

RX14 commented May 20, 2017

Here's the problems as I see it:

  1. You now have two ways of requiring things. If I import something that requires something else, what happens? If we have it so that requires in imports don't show up, we now have a stack of namespaces instead of a single global namespace.
  2. This stack of multiple namespaces which fall back to looking up the above namespace increases the complexity of the compiler, and is likely to have some corner cases which are confusing for the programmer too
  3. Allowing whole libraries to exist in a namespace where I can't reference them makes dealing with libraries which may accidentally export types which I then can't access a pain. Sure I could require the library but that defeats the whole point
  4. If this is implemented, libraries or methods will probably pop up which allow users to import the same library in multiple versions. Those incompatible types will then end up accidentally being exported to me as a user and hell will ensue.

@chocolateboy
Copy link
Contributor Author

chocolateboy commented May 20, 2017

1. If I import something that requires something else, what happens?

The same thing that happens if you require something that requires something else. This proposal doesn't change the behaviour of require. There are no changes to namespaces or namespacing.

2. This stack of multiple namespaces.

See 1.

3. Allowing whole libraries to exist in a namespace where I can't reference them makes dealing with libraries which may accidentally export types which I then can't access a pain.

Not sure what this means. Can you give an example?

4. Those incompatible types will then end up accidentally being exported to me as a user and hell will ensue.

Isn't this what (potentially) happens with require (see the examples I mentioned above)? Avoiding this is what distinguishes scoped imports from global imports.

@chocolateboy chocolateboy changed the title Scoped exports/imports Scoped imports May 20, 2017
@RX14
Copy link
Contributor

RX14 commented May 20, 2017

@chocolateboy

The same thing that happens if you require something that requires something else.

This proposal namespaces libraries but not the libraries' dependencies as long as the libraries use require and not import (which is a fair assumption just after this feature is released, features that require adoption to work often don't get adoption, see refinements in ruby). That seems a bit counter-productive if the aim it to reduce namespace pollution.

Not sure what this means. Can you give an example?

Often libraries will themselves use types of another library in their API. If the type that this library "exports" is imported instead of required, you can't access the name of the type that this library uses without requiring it yourself. So if you need to use this transitive type dependency, you need to require the original library which can be painful. Furthermore, if we used your original syntax proposal directly, it seems like you'd have to require the library that you're importing just to access it's types.

Those incompatible types will then end up accidentally being exported to me as a user and hell will ensue.

With require, you can only realistically have 1 version of a given library at the same time, using import with multiple namespaces would relax that restriction and you end up with incompatibilities in types between multiple different versions of a library.

@chocolateboy
Copy link
Contributor Author

chocolateboy commented May 20, 2017

This proposal namespaces libraries

I'm not sure what you mean. Private modules and classes are already supported in Crystal as of #3280. The implementation I've sketched here, which, of course, is subject to change, discussion and refinement, proposes a hook which allows a compilation unit to return a value in response to being imported e.g.:

main.cr

cmd = import "command"

command.cr

private class Command
  # ...
end

macro imported(*args)
  Command.new(...)
end

could desugar to something like (this is just a sketch):

main.cr

cmd = CompilationUnit["command"].imported

command.cr

private class Command
  # ...
end

class << CompilationUnit.current
  def self.imported(*args)
    Command.new(...)
  end
end

where CompilationUnit would, I'm assuming, be an internal class.

So if you need to use this transitive type dependency, you need to require the original library which can be painful.

I still don't follow. Can you give an example of how this issue manifests itself in, say, ES6 or CommonJS, which support monkey patching and scoped imports?

using import with multiple namespaces would relax that restriction and you end up with incompatibilities in types between multiple different versions of a library.

Again, this proposal doesn't affect or change namespaces in any way.

@faultyserver
Copy link

Sorry for the super-long post. I tried paring it down, but there were a few distinct points I wanted to make that kept it pretty long.

I'm all for implementing something like refinements from Ruby or using from C++, but I don't think a "scoped import" system is necessary or particularly beneficial.

The issue that I see with it is somewhat overarching of the points that RX14 brought up: there would be two distinct paradigms for loading code, and relying on 3rd-party libraries inevitably means that users will have to deal with both in their codebase.

Because there are advantages to both systems, creating a single, idiomatic approach for programmers to follow would be nearly impossible. People's opinions are unlikely to change, and as soon as one library decides to go the other way (use import instead of require, or vice versa), end users have to start remembering which libraries do what and how to include them.

In theory, it's nice to say that require and import should complement each other, but their usage is undoubtably intertwined, and I think most people would (at a glance) see them as interchangeable.

I think it should be the library's responsibility to namespace code, not the user's. Even then, it's possible (and easy) to resolve namespace collisions with a simple re-assignment as I'll show later on, so I don't see any explicit gain in having import.

tl;dr; opinion from the rest of this post: require and import come from two disparate ideologies about global namespaces and including code from other files, and there's nothing one system can do that the other can't. Making both of them available is more confusing than helpful.


Languages like Python, ES6 (I think. at least with the es6 module system), and Java only provide scoped imports. They don't have global imports to a single namespace in the way that require in Ruby or #include in C/C++ provide. For example, something like:

import os

doesn't import os to a global namespace; it is file-private and it's members are still bound to the os namespace. Similarly, using the from ... import syntax:

from math import floor

floor is still a file-private import, even though it has been hoisted from the math namespace.

That's one of the fundamental differences between (in particular) Python and Ruby. Python requires that you explicitly state all dependencies in every file they are used, while Ruby allows you to build up a global namespace that is available everywhere. Each method has its pros and cons, and debating them is pointless, since there will never be consensus (if you haven't gathered already, I'm biased towards the Ruby/C/C++ method).

I think it's important, though, to recognize that Crystal has adopted the require paradigm and the global namespace ideology that goes with it. Tacking on a non-global system seems counterproductive to me, since it doesn't provide anything that can't already be done. The idiomatic way to solve global namespace collisions is (or should be, imo) to have libraries use their own namespaces (modules). Take this ruby example:

# some_library.rb
module SomeLibrary
  class Thing; end
end

# some_other_library.rb
module SomeOtherLibrary
  class Thing; end
end

# no namespace collisions :)
require 'some_library'
require 'some_other_library'
SomeLibrary::Thing
SomeOtherLibrary::Thing

If (somehow) you still have namespace collisions, you can manually rename dependencies quite easily:

require 'some_library'
SomeRenamedLibrary = SomeLibrary
load 'some_library' # using load instead of require simply to reload the same file again

# still no namespace collisions
SomeRenamedLibrary::Thing
SomeLibrary::Thing

This is a contrived example, for sure, but (again, imo) anything that doesn't follow this pattern should be considered non-idiomatic code and refactored to follow it. It's similar to the whole using namespace debacle in C++ (where, fwiw, you still have to #include the code before you can using namespace it)


This hasn't been brought up directly, but I saw a mention of Elixir, so I figure I'll bring it up now. The way that Elixir loads modules is not based on import or require or anything like that. Instead Elixir simply loads all of the .beam files it can find into the runtime, meaning all modules, classes, etc. are always available, and namespacing is even moreso idiomatic there.

import is simply used as an aliasing mechanism, and require is only needed to load macros at compile time.

I didn't know this for a long time, and was confused by the apparent magical loading that Elixir did when I was able to do something like import Ecto.Query without directly giving a specific file to load. I still don't like this approach, because I think it makes reading a codebase more difficult, since Ecto.Query could be defined in one or more files, and I can't know where those files are just by seeing where it gets used.

A similar argument could be made against the Ruby/C/C++ system, but at least there I'll eventually find a file include that shows me where to go. Again, I don't think that's worth debating, it's just been my experience with Elixir.

This is somewhat unrelated, but comparing require and import in Elixir to what they refer to in this discussion would be incorrect, since they don't serve the same purpose.


To clarify what I think RX14 was getting at with the "This proposal namespaces libraries..." point: If a library is written using requires, and then the end user wants to import that library into a contained namespace, what happens with all of those requires from inside the library? Having them contained in that namespace seems extraordinarily complicated, as it essentially changes the semantics of require.

For example, assuming import is made a part of the language and returns a Module as the namespace for the included file:

# some_library.cr
require 'json'

class SomeThing
end

# end_user_code.cr
SomeLibrary = import "some_library"

What namespace does the JSON module get loaded into? Unless require is modified in some way, it would be loaded into the global namespace, in which case the entire purpose of having import (i.e., limiting global namespace pollution) is rendered somewhat moot.

Even if require where to change, how would it be changed to both make included files available in the global namespace and not do that if the code is imported in another file? In terms of code:

SomeLibrary = import "some_library"
# does this work? It would with the way `require` works now
JSON.parse("[]")
# or is it namespaced under SomeLibrary? `require` would need
# to change for this to work
SomeLibrary::JSON.parse("[]")
# or is it not available at all? Again, `require` would have to change
SomeLibrary::JSON # => undefined constant SomeLibrary::JSON

tl;dr again: require and import come from two disparate ideologies about global namespaces and including code from other files, and there's nothing one system can do that the other can't. Making both of them available is more confusing than helpful, and also makes the compiler a lot more complicated when the two can be interspersed in a codebase.

@bcardiff
Copy link
Member

@chocolateboy thanks for sharing the refinements talk. It really gives a refreshing look to that topic.

Using refinements in a file level lexical scope is something that could make sense IMO. But there is still "the issue" of the shared global namespace of where that refinement lives. In that sense a require system that is more explicit (and less fun) like ES6 might be more coherent. Finally as highlighted by the refinements presentation, since they are not so widely adopted it might no be clear still if they are a good enough solution.

@RX14
Copy link
Contributor

RX14 commented May 21, 2017

@faultyserver you absolutely nailed what I was getting at, thanks so much!

@asterite
Copy link
Member

One major concern about the current require system, or at least about the language, are top-level (global) methods and macros. These are not namespaced and so if a popular library choses to use one, then sorry for the other libraries, that name is already taken. And this is not just a theoretical concern: Kemal defines get, post, ws, error, content_for, CONTENT_FOR_BLOCKS, yield_content, render, halt, add_context_storage_type and probably others that I've missed. And I've seen other libraries happily use the top-level namespace for helper functions and macros.

To solve this, maybe we can remove top-level methods and macros altogether. That would force everyone to namespace their declarations. We can't make sure that people will use different module/class names, but chances of collision are now lower.

Now, to achieve something like being able to write:

get "/" do
end

which is nice and convenient, we can maybe have the top-level include make these methods and macros accessible in the current file. So one would have to do something like:

include Kemal::DSL

get "/" do
end

There would be an extra line in these files, which is not that cumbersome, but at least we can sleep without worries at night :-P

Because the standard library already defines some pretty common and useful top-level methods and macros like puts, pp and record, all files could implicitly include Kernel for this, or maybe not.

There's still the issue about reopening existing classes like String and adding methods to them, which can bring some conflicts. Maybe we'd need something similar for that: adding extension methods but scoped to a file (I don't think we need this scoped to a module).

Anyway, these are just thoughts...

@asterite
Copy link
Member

In fact, we could still allow methods and macros to appear at the top-level, but these will always be file-private. These are convenient for short scripts or specs.

Hmmm... I might try to implement this and see how it goes, shouldn't be that hard. Then we can really see if it's useful/convenient.

@chocolateboy
Copy link
Contributor Author

end users have to start remembering which libraries do what and how to include them.

I don't see how this is any different from using any library in any language.

I think it should be the library's responsibility to namespace code

If a library is written using requires, and then the end user wants to import that library into a contained namespace, what happens with all of those requires from inside the library? Having them contained in that namespace seems extraordinarily complicated, as it essentially changes the semantics of require.

This proposal has nothing to do with namespaces and doesn't change the semantics of require in any way. This keeps being brought up as an (abstract) objection, but it has nothing whatsoever to do with the (concrete) implementation outlined here.

Languages like Python, ES6 (I think. at least with the es6 module system), and Java only provide scoped imports.

ES6 supports both, which is why I mentioned it and asked for examples of conflicts between the two usages. Examples include:

import 'source-map-support/register'
import 'shelljs/global'
import 'core-js'

Making both of them available is more confusing than helpful.

Using both is entrenched in JavaScript, in which both monkey patching (i.e. polyfills) and scoped imports (pretty much everything on NPM) are widely used. I've never encountered any "confusion" between these uses. If someone has a concrete example of this, I'm all ears, but at the moment these objections are purely hypothetical and don't reflect real world usage.

makes the compiler a lot more complicated

I'm more than happy if someone else wants to take a crack at implementing this, but at the moment the proposed implementation doesn't add any complexity to the compiler that I'm aware of. It merely sugars something that can (almost) be done already.

@chocolateboy
Copy link
Contributor Author

chocolateboy commented May 21, 2017

In fact, we could still allow methods and macros to appear at the top-level, but these will always be file-private.

Hmmm... I might try to implement this

If you do this, please consider doing it against another ticket. This proposal deliberately avoids namespace-related changes, which are (traditionally) the domain of require.

It would be great if the scope of this discussion could be limited to this proposal and its suggested implementation, rather than going the way of #140, which quickly lost sight of its original topic. IMO, other proposals regarding refinements and namespace changes would be better served by their own separate discussions.

@asterite
Copy link
Member

@chocolateboy The proposed implementation doesn't work out of the box. If you return the Command::Runner module which is private, the other file shouldn't be able to access it. The compiler now has to know that this type was brought via an import of some sort.

I'd also like to see some real examples other than just a run method of a Command class. For example, imagine there's a redis library, or Kemal. How would you use these with import?

I think I'm also against having two ways of "importing" code. I understand that JS has two ways, but I believe that's for historical reasons.

@chocolateboy
Copy link
Contributor Author

The proposed implementation doesn't work out of the box. If you return the Command::Runner module which is private, the other file shouldn't be able to access it. The compiler now has to know that this type was brought via an import of some sort.

This was the first thing I tested, and it appears to work as expected:

test.cr

require "./foo/bar.cr"

test = Exported.imported
puts test
puts test.class
puts test.class.new
puts Command #=> Error

foo/bar.cr

private module Command
  class Runner
    def initialize(@shell = "bash"); end

    def run(command)
      "#{@shell} -c #{command.inspect}"
    end
  end
end

module Exported
  def self.imported
    Command::Runner.new
  end
end

output

#<Command::Runner:0x840f00>
Command::Runner
#<Command::Runner:0x840ee0>

What am I missing?

@chocolateboy
Copy link
Contributor Author

chocolateboy commented May 21, 2017

I'd also like to see some real examples other than just a run method of a Command class.

Sure.

For example, imagine there's a redis library, or Kemal. How would you use these with import?

Doesn't Kemal provide a DSL? If so, why would you use import? Again, this isn't a proposal to replace or augment require.

@RX14
Copy link
Contributor

RX14 commented May 21, 2017

@chocolateboy What if I want to use Command in a type restriction? As an instance variable? With your implementation it's impossible because there's no way to reference the type.

@asterite
Copy link
Member

It's just that I need a real-world example to consider this (at least me). Because in the previous example we can just have a MyLibrary::Command::Runner and then you can write:

runner = MyLibrary::Command::Runner.new(...)
runner.cmd(...)

Of course you have to be careful to namespace your command runner, but that's what you have to do in every Crystal library. And the amount of typing is more or less the same.

@chocolateboy
Copy link
Contributor Author

chocolateboy commented May 21, 2017

What if I want to use Command in a type restriction?

Use typeof.

As an instance variable?

Have you tried it?

test.cr

require "./foo/bar.cr"

class Test
  def initialize
    @test = Exported.imported
  end

  def test
    puts @test
    puts @test.class
    puts @test.class.new
    # puts Command # Error
  end
end

Test.new.test

output

$ crystal test.cr

#<Command::Runner:0x102aee0>
Command::Runner
#<Command::Runner:0x102aec0>

With your implementation it's impossible because there's no way to reference the type.

It's no more or less possible with this implementation since private modules and classes already exist and are used extensively in Crystal's standard library and specs:

$ rg 'private\s+(module|class|struct|lib|alias|[A-Z]+)' spec/ src/ | wc -l
234

If there are inconsistencies or other issues relating to private modules and classes, by all means raise them in another issue, but, as it currently stands, this proposal doesn't introduce anything relating to them that isn't already implemented.

@asterite
Copy link
Member

But here @test = Exported.imported is inferred from the call, which is typed. If we'd have import there would be no way to refer to that type name.

@RX14
Copy link
Contributor

RX14 commented May 21, 2017

@chocolateboy In addition to what @asterite said, using typeof() seems like a hack. However - regardless of implementation details - there would have to be a change in the way namespacing works so that certain files could reference certain types and not others.

@asterite I really like the idea of having the top level be always file-private, but how would that work with the stdlib? If you add a special modifier to make top-level things be public then we'll probably end up back where we started.

@chocolateboy
Copy link
Contributor Author

chocolateboy commented May 21, 2017

But here @test = Exported.imported is inferred from the call, which is typed. If we'd have import there would be no way to refer to that type name.

This is just a reified example of the implementation sketched above i.e.:

test = import "./foo/bar.cr"

would desugar to (something like):

test = CompilationUnitForFooBar.imported

Are you saying this isn't possible?

@RX14
Copy link
Contributor

RX14 commented May 21, 2017

@chocolateboy There are always times in which the type inference for class vars isn't possible, so you have to specify the type explicitly. It's often that you want to do this for explicitness. This proposal makes you use typeof() for that situation which is an ugly hack to a very common situation. To resolve this you'd have to change namspacing/requires/the compiler. And i'm not sure that the typeof hack would work in all situations with an unmodified compiler.

@chocolateboy
Copy link
Contributor Author

using typeof() seems like a hack.

This proposal makes you use typeof() for that situation which is an ugly hack to a very common situation.

Feedback on, and criticism of, private modules/classes belong elsewhere (e.g. on #3280), rather than here, since this proposal doesn't introduce or impact that functionality in any way.

I really like the idea of having the top level be always file-private, but how would that work with the stdlib?

By all means close this issue if it is to be superseded by a different implementation, but please take the discussion of that different implementation to a different issue. We already have an example of a derailed version of this discussion in #140. Let's please try to keep at least one version of it focused on the actual proposal.

@asterite
Copy link
Member

@chocolateboy Maybe it's possible. But foo = CompilationUnitForFooBar.imported doesn't exist yet, though it can be simulated, but the file has to be processed in a different way (I guess you don't want declarations in that file to pollute the global namespace).

I'm still looking for real examples where one library that you currently use with require would benefit with using import instead.

I'll soon write a proposal about changing the top-level include which will also improve the situation regarding types.

@chocolateboy
Copy link
Contributor Author

chocolateboy commented May 21, 2017

(I guess you don't want declarations in that file to pollute the global namespace).

No, that's a mischaracterisation spread in this discussion, but which has never been part of this proposal. I'm not proposing any changes to namespaces/visibility or require. It would be entirely up to the author whether Command (or whatever) is public or private. Returning a value from an imported file is orthogonal to that.

The only thing new in this proposal is the imported hook and the (internal) CompilationUnitForFooBar object/class it's called on.

@asterite
Copy link
Member

I mean, if you have:

# file: "one.cr"
class One
end

macro imported
  One.new
end

# file: "two.cr"
one = import "./one"

# This compiles fine, because I didn't say One was private
One.new

Is that expected behaviour?

I also find it a bit strange that a file just imports methods, where one would generally like to use types, and possibly refer to types inside that file. How would you handle that case?

@chocolateboy
Copy link
Contributor Author

Is that expected behaviour?

Yes. Just the same as JavaScript:

test.js

const fooBar = require('./foobar.js')

foobar.js

// FooBar.baz requires the Array.flatMap polyfill
Array.prototype.flatMap = function () { } // global side-effect

const fooBar = new FooBar(...) // local value
module.exports = fooBar

Though I doubt this would be the typical use case.

@faultyserver
Copy link

faultyserver commented May 21, 2017

I think the main reason that namespacing is being discussed here is the second line in the original post:

The workaround pollutes the global namespace e.g.:


But there is no need to break/change require. The two concepts are not mutually exclusive e.g. one can do both in Node.js:

Node's require is always scoped. It's similar to Python in that there is no global, shared context*. You can do a bare require without an assignment because there is an implicit or explicit module defined that the file's contents are inside of, and that module just gets assigned to the file scope. Assigning to a variable just acts as an alias for that module in the file scope.

Because Crystal does have that global, shared context, something about the require system would have to change to implicitly or explicitly modularize every file. That could be on the user to do, or it could be done implicitly by require. The latter seems impractical, because you already can do the former, which begs the question of why import is even necessary when you can already solve the problem with good, idiomatic code.

* there is some shared state with window and things in the standard library, which I find annoying to say the least.

@chocolateboy
Copy link
Contributor Author

chocolateboy commented May 23, 2017

No, it's a convention.

It's not a convention. Conventions are optional.

@RX14
Copy link
Contributor

RX14 commented May 23, 2017

@chocolateboy so is this feature

@asoffa
Copy link

asoffa commented May 23, 2017

@chocolateboy Wait a minute...if I define a private value inside a module/class, I can use it across files without polluting the global namespace, no? For example:

# in file test.cr

module Test
  private HIDDEN_VALUE = 1
end


# in file test_continued.cr

require "path/to/test"

module Test
  def self.print_hidden_value  # uses `HIDDEN_VALUE` while adding it neither to
                               # the global namespace nor to `Test`'s namespace
    puts HIDDEN_VALUE
  end
end

Anyone who reopens Test will still have access to HIDDEN_VALUE inside (and only inside) of Test, but users shouldn't be reopening a module/class without a good understanding of its internals anyway. Is there a use case not available via current functionality such as the above that you have in mind? For your command example, we would have

module MyLibrary
  private module Command
    def self.run(cmd : String)
      system cmd
    end
  end
end

# The following can be in a different file that `require`s the above:
module MyLibrary
  def self.echo_hi
    Command.run "echo hi"  # use `Command` without exposing it globally
  end
end

MyLibrary::Command.run "echo hi"  # error: `Command` is private
MyLibrary.echo_hi                 # works!

@chocolateboy
Copy link
Contributor Author

I can use it across files without polluting the global namespace, no?

No. Test and MyLibrary pollute the global namespace, just like Exported in this example.

@asoffa
Copy link

asoffa commented May 23, 2017

No. Test and MyLibrary pollute the global namespace, just like Exported in this example.

Right, but if I'm developing a library, I will be doing everything within some base module for that library, no? This is what @ysbaddaden mentions #4439 (comment).

And if I'm just writing a quick script, I don't need to care whether I'm polluting the global namespace, as the script won't be required, right?

EDIT: Ah, so the point is for library developers not to be forced to introduce a global module at all and instead give them at least the option of introducing nothing globally (except for their dependencies, which must still enter the global namespace if they don't provide import hooks or don't exclusively use private modules, classes, etc.). This is the 'why' :-)

The caveat in parentheses above seems to be the main reason why this proposal has been so confusing (at least for me) and controversial...trying to keep both those who prefer an optionally local import and those who prefer a necessarily global require happy has led to a unique proposal that is (remarkably) consistent with both strategies, but ideal for neither. Hence the complaints from both sides :-\

@asoffa
Copy link

asoffa commented May 23, 2017

Consider a bookstore-vs.-library analogy:
The import camp prefers to have dependencies be like books at a bookstore in that I have my own copy of a book I buy, and any monkey-patching I do to the book stays local to my book, not affecting anyone else's book. On the other hand, the require camp prefers to have dependencies be like books at a library in that there is only one book, and any monkey-patching I do to a book I check out applies to everyone who checks out the book thereafter. Finally, the combined strategy proposed here would be to have a library that also sells books with some books available only for check-out, others available only for sale, and still others available both for check-out and for sale, depending on different authors' choices for each book.

Except the import proposed here is really a require in disguise: monkey-patching I do to my own book does in fact affect everyone else's book, so there was only ever one book to begin with. In the end this proposal merely provides a new way to define a module/class (via a file itself rather than module Blah; end, etc. within a file) still entirely within the same require system...just with a global module/class name being replaced by a global file name.

I leave the decision on whether this would be a good or bad feature to have up to someone else :-)

@faultyserver
Copy link

@asoffa That's a really good summarization, at least from my understanding of both sides. Thanks for making it so clear :)

@asoffa
Copy link

asoffa commented May 23, 2017

@faultyserver I tried :-)

@faultyserver
Copy link

faultyserver commented May 24, 2017

Alright. I was bored and had a spare hour, tried to make something that simulates import, and came up with this abomination:

# main.cr
macro import(file_name, as container)
  {% raw_file = `cat #{file_name.id}`.stringify.lines %}
  {% body_started = false %}

  {% for line in raw_file %}
    {% if line.starts_with?("require") %}
      {{ line.id }}
    {% else %}
      {% unless body_started %}
        module {{container}}
        {% body_started = true %}
      {% end %}

      {{ line.id }}
    {% end %}
  {% end %}

  # Is there a better way to output `end` to the result of the macro?
  {{ "end".id }}
end


import "./other.cr", as: Library

puts Library::SOME_CONSTANT
thing = Library::Thing.new
thing.foo = "hi"
puts thing.foo
puts JSON.parse("[]")



# other.cr
require "json"

SOME_CONSTANT = 2

class Thing
  property foo : String?
end

which compiles just fine and outputs:

2
hi
[]

The output of the macro that gets substituted at the call site looks like this:

require "json"

module Library
  class Thing
    property foo : String?
  end
end

As you can see in the example:

  • other.cr has no containing module.
  • require "json" is respected and left available to the importer.
  • All definitions in other.cr are encapsulated by Library, a constant defined by the importer when calling import.
  • There is no onus on the author of other.cr to define any hooks, etc.
  • All of this is done at compile-time.

It's not exactly the same, but it's close enough imo, and shows that the language doesn't need to change to support this kind of functionality.

If people want it badly enough, this could easily be made into a shard and expanded to work better:

  • better require detection and re-ordering
  • better file resolution (paths, omitting .cr, folder globs)
  • nested imports
  • macro imported support for hiding private definitions (maybe via an intermediate file or a separate process, similar to how ECR is implemented?)*

In any case (actually more so now), I'm still of the mind that this doesn't need to be (rather, shouldn't be) added as part of the Crystal language/stdlib itself.

*EDIT: Actually, private definitions will be hidden by default (private to the containing module, inaccessible from anywhere else), so this point is kind of moot.

@faultyserver
Copy link

faultyserver commented May 24, 2017

inb4 "encapsulation is orthogonal to this proposal": everything I've seen about import so far is taking the "output value" of a source file and assigning it to a variable. What I wrote above is pretty much exactly that, just put into a type instead of a variable, and currently without support for privatized types.

Not only is that more semantically consistent, it's generally better, as the types defined in the imported file are now available at compile-time and as type restrictions for other definitions.

@asoffa
Copy link

asoffa commented May 24, 2017

@faultyserver Nice little concoction there!

One small addition to #4439 (comment) if we would want imports to be private by default:

macro import(file_name, as container, public=false)
  {% raw_file = `cat #{file_name.id}`.stringify.lines %}
  {% body_started = false %}

  {% for line in raw_file %}
    {% if line.starts_with?("require") %}
      {{ line.id }}
    {% else %}
      {% unless body_started %}
        {% if public %}
          module {{container}}
        {% else %}
          private module {{container}}
        {% end %}
        {% body_started = true %}
      {% end %}

      {{ line.id }}
    {% end %}
  {% end %}

And a check to disallow cyclic imports, which would fall under

  • nested imports

...but let's see if people are interested before going too far down the rabbit hole :-)

@chocolateboy
Copy link
Contributor Author

@faultyserver I don't see how that "solves" this issue as it merely moves the global pollution from the callee into the caller:

leak.cr

require "./main.cr"

puts Library

output

# ...
Library

One could just as easily define the Exported module in this example in test.cr and reopen it in foo/bar.cr.

the types defined in the imported file are now available at compile-time
What I wrote above is pretty much exactly that, just put into a type instead of a variable

Of course it's possible to "export" a type by polluting the global namespace. Feel free to pursue that in a separate issue or a shard, but it's not what's being proposed here.

@asoffa
Copy link

asoffa commented May 24, 2017

I don't see how that "solves" this issue as it merely moves the global pollution from the callee into the caller

Hence my private-by-default suggestion: #4439 (comment)

(though when I try this as-is with the private option, Library isn't actually made private, but hopefully this can be fixed)

@faultyserver
Copy link

merely moves the global pollution from the callee into the caller:

...to where the caller has 100% control over the naming of the module, so all naming collisions (one of the "surprises" from the OP, root cause of another one. Third has nothing to do with require or import semantics, it's an "issue" with compact namespace declarations in Ruby) are solvable, if not completely gone (e.g., got a collision? Just rename the import).


require "./main.cr"; puts Library

The logical converse of these statements:

The same thing that happens if you require something that requires something else.

Why would you want to import a value from a library which doesn't export a value?

Why would you require it if you don't want it available in the caller?

import "./main.cr", NotLibrary
puts Library

*Doesn't currently work because the nested imports aren't there (because the require detection is simplistic). I'll leave that as an exercise for the reader (cause I'm at work and lazy :). The solution is trivial, but not short.


Of course it's possible to "export" a type...[that's] not what's being proposed here.

That's part of the problem. Why would I want to get back an object instance that I don't actually have type information for? (Crystal doesn't run on a VM, typeof is not sufficient for most cases) What if I want a second instance of whatever I imported? Do I have to import it again?

Side note:

It's just that I need a real-world example to consider this (at least me).

Haven't seen one here yet (nor in my years of writing Ruby and C/C++) that isn't entirely avoidable with modularization or the import implementation I gave above.

P.S. Would still love to see a PR with a proper implementation of this so we can actually discuss what it does, not what we think it does.

P.P.S.: There's an open PR for a read_file macro that could make this easier: #2791. Would be the a direct copy of #include in C, so you could just paste code inside a private module or whatever.

@chocolateboy
Copy link
Contributor Author

@faultyserver You've already established that you don't have a use for this feature, which may explain why you keep proposing to solve a different problem and trying to pull this issue away from its stated goal. We've already been down that route with #140, but, as I say, feel free to explore those possibilities in a separate issue.

@faultyserver
Copy link

keep proposing to solve a different problem and trying to pull this issue away from its stated goal.

Hence my "would love to see a PR with a proper implementation".

Clearly no one else knows exactly what is being proposed here, so let's get an implementation that we can pragmatically test and discuss. Everything so far has been hypothetic.

@faultyserver
Copy link

Side note: can you clarify how exactly this proposal differs from #140? MyFoo = acquire('foo') seems pretty darn similar to thing = import "foo.cr".

@konovod
Copy link
Contributor

konovod commented May 24, 2017

MyFoo is in global namespace, thing isn't.

@RX14
Copy link
Contributor

RX14 commented May 24, 2017

Surely we could conceive that private MyFoo = import "foo.cr" could work. It could even be the default, as @asterite suggested in #4442. Importing a constant (class, module) is much more flexible than importing a single variable.

Not that i'm any more for the whole concept in the first place: having 2 ways of doing things is a bad idea, especially this late in the game.

@asoffa
Copy link

asoffa commented May 24, 2017

Quick summary, since this is now the 80th comment:
When using require, both the author and the user of a file are forced to have at least one global introduced/reopened (unless everything in the required file is private, in which case requiring it is useless). The problem to be solved is how to access/use code in another file without the author and/or user (see below) being forced to introduce/reopen a global.

Two solutions proposed so far:

  1. (@chocolateboy's solution) allow an imported hook in a file, which allows the file to return a value:
value = import "path/to/file"
# `value` now set to whatever the `imported` hook in file.cr returns
  1. (@faultyserver's solution) add an import macro that copies/pastes the code in a file into an optionally private module at the call site:
import "path/to/file", as: Library
# fresh copy of code in file.cr now loaded into `Library`

(1) allows the author (and only the author) to set up a non-global return from a file, whereas (2) allows the user to make the contents of a file non-global (independent of the author's public-vs.-private decisions). Both solve the above problem, but in different ways.

Sound about right?

@asoffa
Copy link

asoffa commented May 25, 2017

And finally, going in the opposite direction, a third solution to the problem of not being able to use code from another file without introducing/reopening a global referenced by @faultyserver in #4439 (comment):

  1. Add read_file macro method #2791: a more primitive version of require that literally copies/pastes code from another file so that private members can be still be accessed

The proposal here is only for (1) in the previous comment, but it's important to clarify the issue and discuss all options first before deciding on and implementing a solution to the issue

@faultyserver
Copy link

I think you covered everything pretty well, but I wouldn't suggest that either of my proposals be taken into the stdlib. They are proposals for not accepting the feature and working around that decision. Both are prone to random errors as they cheat around proper scoping and rely on assumptions that won't always hold.

@asoffa
Copy link

asoffa commented May 25, 2017

Agreed - if we can reduce the issue to a non-issue, we wouldn't (necessarily) want to implement a solution to that non-issue :-)

@asoffa
Copy link

asoffa commented May 25, 2017

And combining the ideas from (2) and (3) above, we arrive at

# other.cr
require "json"

SOME_CONSTANT = 2

class Thing
  property foo : String?
end
# main.cr

require "json"  # user responsible for explicit `require`s for `Library` below
# ...or not:
{{ `cat ./other.cr | grep 'require '` }}

# add some `import`ant functionality:
private module Library
  {{ `cat ./other.cr | grep -v 'require '` }}
end

puts Library::SOME_CONSTANT
thing = Library::Thing.new
thing.foo = "hi"
puts thing.foo
puts JSON.parse("[]")
# check.cr
require "./main"
puts Library  # error, as desired

The above is clean enough to allow for sufficiently easy use, yet unclean enough to discourage overuse. Looks like a win-win to me :-)

@asoffa
Copy link

asoffa commented May 25, 2017

In the end, most (all?) Crystal library developers will split their libraries across multiple files and use the current require system. Using a library then necessarily does still introduce a global variable if there is even one require in the required/imported file for a file within that library. So my point from above #4439 (comment) still stands: the above solutions allow a library author (and only the author) to avoid introducing a global if they so desire. You may view this as "good" or "bad," but the point is that we would need a paradigm shift in how most Crystal library authors write their libraries, expecting them to use the new non-require-based paradigm such as the imported hook of this proposal for such a feature to be useful beyond the libraries of a single author...and I don't see such a paradigm shift happening anytime soon, fortunately or unfortunately. The technique in the previous comment provides a relatively slick way using existing functionality for a library author to use code from another one of their files without introducing/reopening a global...does this suffice? I'm inclined to say yes if the requirement is not to "break" the current require system

@faustinoaq
Copy link
Contributor

Hi, I like Python import and i used a bit of ruby in my life.

When I discover Crystal and learn more Ruby I realize that I can do this:

# file src/helper.cr
struct Calculate
  def cube_sum(args : [] of Int32)
    args.map { |n| n ** 3 }.sum
  end
end
# file src/calc1.cr
struct MathCalc1
  def op_use_cube
    Calculate.new.cube_sum([2,3,7])
  end
end
# file src/calc2.cr
struct MathCalc2
  def op_use_cube
    Calculate.new.cube_sum([8,1,3])
  end
end
# file main.cr
require "src/*"
MathCalc1.new.op_use_cube
MathCalc2.new.op_use_cube

I don't need to require explicitly every file because main.cr call all.
As I read in comments above, seems that ruby require follows C/C++ #include way

@asterite
Copy link
Member

I'm closing this. If one day a different way to require code appears, it will come from the core team. Please don't send any more proposals related to this. Thank you.

@crystal-lang crystal-lang locked and limited conversation to collaborators May 25, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants