README.md 5.57 KB
Newer Older
Loic Nageleisen's avatar
preview  
Loic Nageleisen committed
1 2
# Pak - A namespaced package system for Ruby

Loic Nageleisen's avatar
Loic Nageleisen committed
3 4
Providing namespace isolation using import semantics.

Loic Nageleisen's avatar
preview  
Loic Nageleisen committed
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
## 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`:

Loic Nageleisen's avatar
Loic Nageleisen committed
27
```ruby
Loic Nageleisen's avatar
preview  
Loic Nageleisen committed
28 29 30 31 32 33 34 35 36 37
HELLO = 'FOO'

def hello
  'i am foo'
end
```

Importing `foo` will make it accessible to `self`:

```ruby
Loic Nageleisen's avatar
Loic Nageleisen committed
38
import('foo', to: :method)
Loic Nageleisen's avatar
preview  
Loic Nageleisen committed
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
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
Loic Nageleisen's avatar
Loic Nageleisen committed
161
```
Loic Nageleisen's avatar
preview  
Loic Nageleisen committed
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214

## 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