modules

A Ruby module loader inspired by the semantics of js modules.

View project on GitHub

modules

A Ruby module loader inspired by the semantics of js modules.

Gem Version Build Status

Motivation

One of Ruby’s greatest weaknesses is that it lacks a good mechanism for named imports and exports. When you require a ruby file, its constant declarations (like classes) get evaluated into your scope. In contrast, the node.js commonjs module system allows developers to explicitly declare exports that consumers can access on the loaded module container. Many prefer the explicit and isolated aspects of js modules. This module loader defines the primitives import and export necessary for a cleaner modules abstraction in Ruby while maintaining some interoperability with existing practices in the Ruby standard library and community packages.

Installation

bundle install modules

Usage within Ruby

require 'modules'
# Tell modules where it should look for code
modules.config(basepath: File.dirname(__FILE__))

# Now import and export are available globally throughout your program.
two = import('./two')

# import also hangs off of the modules global if you prefer
two = modules.import('./two')

export do
  two + two
end

Bootstrapping from the command line

You can run your program through the modules command-line interface via modules run <path/to/main.rb>. This invocation style doesn’t require your program to load the gem; main.rb can use import to load local modules defined with export as well as other ruby libraries.

API

export(&blk)

Declare a module (one per file) that other modules can load by calling import with a relative filepath. export takes a single block parameter whose return value is what gets exported from the module.

export(key, value)

Declare a single export from a module. This will make it so that your module exports a hash with {key => value}. You can call this multiple times in a module.

import(id)

Load another module. import works with local modules declared with export as well as other Ruby libraries that declare constants like classes and modules. In the latter case when loading external libraries, you need to provide the module name as the id parameter and import will return a Hash containing all constants declared. For instance

import('test/unit/assertions') => 

{
  "Test"=>#<Class:0x00000001d08f58>,
  "Test::Unit"=>#<Class:0x00000001ab38c0>,
  "Test::Unit::Assertions"=>#<Class:0x00000001ab29e8>,
  ...
}

modules.config(options)

Configure the loader with an options hash. Currently supports

(String) :basepath - base directory that the loader uses for filepath resolution

(Boolean) :save_the_environment - whether or not to clean up global constants for interop loading

modules.delete(id)

Clear a module previously declared with export(id) from the modules internal cache.

Example

### consts.rb
export :foo, 'foo'
export :bar, 'bar'

### foobar.rb
# Load the constants from consts.rb
consts = import './consts'

export do
  lambda { consts[:foo] + consts[:bar] }
end

### test.rb

require 'modules'
modules.config(basepath: File.dirname(__FILE__))

# load local modules defined with an amd-inspired syntax
foobar = import './foobar'
# compatible with external globals-style ruby modules
assert = import('test/unit/assertions')['Test::Unit::Assertions']

assert.assert_equal(foobar.call(), 'foobar')
# No global namespace pollution \o/
assert.assert_equal(defined? Test, nil)

Resolution algorithm

When import('bar/baz') is called from /home/lambdabaa/foo, modules will build /home/lambdabaa/foo/bar/baz (currently by composing File.expand_path and File.join) and check whether this file exists. If it does, modules will assume this is a local import. Otherwise, it will assume bar/baz isn’t a locally defined module and will try require('bar/baz').

Performance

One important consideration is whether loading code through modules incurs any performance penalties compared with other ruby mechanisms such as globally available constants, global variables, or instance variables on the main object. Preliminary testing suggests that modules v1 (essentially caching code loaded via Kernel#load in a hash by filename) is even faster than these other strategies!

Performance

The module loader was able to load 1000 modules in an average of 80ms. That’s 2x the speed of the next fastest method (global variables) and 2.5x the speed of the slowest method (ruby native modules / constants).

Wat?

I’m glad you asked! Under the hood we load code into the modules cache using Kernel#load with wrap=true. The interop layer’s implementation is currently a bit sketchy and relies on comparing snapshots of Module.constants before and after issuing a require for a core lib or external package.