mirror of
https://github.com/lloeki/package-ruby.git
synced 2025-12-06 01:54:41 +01:00
preview
This commit is contained in:
commit
32b99b62bf
20 changed files with 644 additions and 0 deletions
211
README.md
Normal file
211
README.md
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
# Pak - A namespaced package system for Ruby
|
||||
|
||||
## Why, oh why?
|
||||
|
||||
If you are:
|
||||
|
||||
- sick of side effects when requiring?
|
||||
- think writing namespaces when you're already nested deep in directories is
|
||||
not quite DRY?
|
||||
- want easier reloading/dependency tracking/source mapping?
|
||||
|
||||
You've come to the right place.
|
||||
|
||||
## A quick note about require, monkeys, and use cases
|
||||
|
||||
I have no issue *per se* with monkey-patching, require being side-effectful WRT
|
||||
namespacing by design, and classes (modules, really) being open. The trouble
|
||||
is, most of the time it's not what we need, and more often than not it gets in
|
||||
the way in terrible ways. Hence, this, taking inspiration from Python and Go.
|
||||
|
||||
## An example, for good measure
|
||||
|
||||
Take a module named `foo.rb`:
|
||||
|
||||
````ruby
|
||||
HELLO = 'FOO'
|
||||
|
||||
def hello
|
||||
'i am foo'
|
||||
end
|
||||
```
|
||||
|
||||
Importing `foo` will make it accessible to `self`:
|
||||
|
||||
```ruby
|
||||
import('foo', as: :method)
|
||||
p foo #=> #<Package foo>
|
||||
p foo.name #=> "foo"
|
||||
p foo.hello #=> "i am foo"
|
||||
p foo::HELLO #=> "FOO"
|
||||
```
|
||||
|
||||
To avoid pollution (especially with `main` being a special case of `Object`),
|
||||
import defines a `.foo` method on the caller's `class << self`, so that `foo`
|
||||
may not become accessible from too much unexpected places.
|
||||
|
||||
```ruby
|
||||
class ABC; end
|
||||
p ABC.new.foo # NoMethodError
|
||||
```
|
||||
|
||||
You can import under another name to prevent conflict:
|
||||
|
||||
```ruby
|
||||
import('foo', as: :fee)
|
||||
p fee #=> #<Package foo>
|
||||
p fee.name #=> "foo"
|
||||
p fee.hello #=> "i am foo"
|
||||
p fee::HELLO #=> "FOO"
|
||||
```
|
||||
|
||||
Alternatively, you can import as a const:
|
||||
|
||||
```ruby
|
||||
import('foo', to: :const)
|
||||
p Foo #=> #<Package foo>
|
||||
p Foo.name #=> "foo"
|
||||
p Foo.hello #=> "i am foo"
|
||||
p Foo::HELLO #=> "FOO"
|
||||
```
|
||||
|
||||
And if that doesn't suit you, you can import as a local:
|
||||
|
||||
```ruby
|
||||
qux = import('foo', to: nil)
|
||||
p qux
|
||||
puts qux.name
|
||||
puts qux.hello
|
||||
puts qux::HELLO
|
||||
```
|
||||
|
||||
From a package `bar.rb`, you can import `foo`...:
|
||||
|
||||
```ruby
|
||||
import 'foo'
|
||||
|
||||
HELLO = 'BAR'
|
||||
|
||||
def hello
|
||||
"i am bar and I can access #{foo.name}"
|
||||
end
|
||||
```
|
||||
|
||||
...all without polluting anyone:
|
||||
|
||||
```ruby
|
||||
import('bar')
|
||||
p bar
|
||||
p bar.name
|
||||
p bar.hello
|
||||
p foo # NameError
|
||||
```
|
||||
|
||||
Note that `foo` as used by `bar` is visible to the world:
|
||||
|
||||
```ruby
|
||||
p bar.foo
|
||||
p bar.foo.name
|
||||
```
|
||||
|
||||
Packages can be nested. Here's a surprising `foo/baz.rb` file:
|
||||
|
||||
```ruby
|
||||
HELLO = 'BAZ'
|
||||
|
||||
def hello
|
||||
'i am baz'
|
||||
end
|
||||
```
|
||||
|
||||
You can guess how to use it:
|
||||
|
||||
```ruby
|
||||
import('foo/baz')
|
||||
p baz # #<Package foo/baz>
|
||||
p baz.name
|
||||
p baz.hello
|
||||
p foo # NameError
|
||||
```
|
||||
|
||||
Importing a package will load the package only once, as future import calls
|
||||
will reuse the cached version. Loaded packages can be listed and manipulated,
|
||||
allowing a reload for future instances.
|
||||
|
||||
```ruby
|
||||
foo.object_id #=> 70151878063900
|
||||
p Package.loaded #=> {"foo.rb"=>#<Package foo>, ...}
|
||||
# old_foo = Package.delete("foo")
|
||||
old_foo = Package.loaded.delete("foo.rb")
|
||||
import 'foo'
|
||||
foo.object_id #=> 70151879713940
|
||||
```
|
||||
|
||||
`foo` in `bar` will be reloaded once bar itself is reloaded. The logic is that
|
||||
while you *may* want new code to be reloaded by old code sometimes, you'd
|
||||
rather not have old code call new code in an incompatible manner. So, to
|
||||
minimize surprise, global (i.e unscoped const) reload is declared a bad thing
|
||||
and module scoped reload is favored.
|
||||
|
||||
```ruby
|
||||
bar.foo.object_id == old_foo.object_id #=> true
|
||||
```
|
||||
|
||||
Dependency tracking becomes easy, and reloading a whole graph just as well:
|
||||
|
||||
```ruby
|
||||
bar.dependencies #=> 'foo'
|
||||
bar = bar.reload! # evicts dependencies recursively and reimports bar
|
||||
|
||||
## Wishlist: setting locals directly
|
||||
|
||||
I hoped to be able to have an implicit syntax similar to Python or Go allowing
|
||||
for real local variable setting, but this seems unlikely given how local
|
||||
variables are set up by the Ruby VM: although you can get the
|
||||
`binding.of_caller`, modifying the binding doesn't *create* the variable as a
|
||||
caller's local. As such, you can guess how being forced to do `foo = nil;
|
||||
import 'foo'` is not really useful (and entirely arcane) when compared to `foo
|
||||
= import 'foo'`.
|
||||
|
||||
See how [`bind_local_variable_set`][1] works on a binding, defining new vars
|
||||
dynamically inside the binding but outside the local table, resulting in the
|
||||
following behavior (excerpted form Ruby's own inline doc):
|
||||
|
||||
```ruby
|
||||
def foo
|
||||
a = 1
|
||||
b = binding
|
||||
b.local_variable_set(:a, 2) # set existing local variable `a'
|
||||
b.local_variable_set(:b, 3) # create new local variable `b'
|
||||
# `b' exists only in binding.
|
||||
b.local_variable_get(:a) #=> 2
|
||||
b.local_variable_get(:b) #=> 3
|
||||
p a #=> 2
|
||||
p b #=> NameError
|
||||
end
|
||||
```
|
||||
|
||||
A good way to look at the local table is to use RubyVM ISeq features:
|
||||
|
||||
> puts RubyVM::InstructionSequence.disasm(-> { foo=42 })
|
||||
== disasm: <RubyVM::InstructionSequence:block in __pry__@(pry)>=========
|
||||
== catch table
|
||||
| catch type: redo st: 0002 ed: 0009 sp: 0000 cont: 0002
|
||||
| catch type: next st: 0002 ed: 0009 sp: 0000 cont: 0009
|
||||
|------------------------------------------------------------------------
|
||||
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, keyword: 0@3] s1)
|
||||
[ 2] foo
|
||||
0000 trace 256 ( 13)
|
||||
0002 trace 1
|
||||
0004 putobject 42
|
||||
0006 dup
|
||||
0007 setlocal_OP__WC__0 2
|
||||
0009 trace 512
|
||||
0011 leave
|
||||
=> nil
|
||||
|
||||
That's because, IIUC, the local variables table is basically fixed and cannot
|
||||
be changed, so the binding works around that with dynavars, but it doesn't
|
||||
bubble up to the function local table.
|
||||
|
||||
[1]: https://github.com/ruby/ruby/blob/6b6ba319ea4a5afe445bad918a214b7d5691fd7c/proc.c#L473
|
||||
Loading…
Add table
Add a link
Reference in a new issue