Skip to content

Instantly share code, notes, and snippets.

@wilkie
Last active August 29, 2015 13:58
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save wilkie/10002742 to your computer and use it in GitHub Desktop.
Save wilkie/10002742 to your computer and use it in GitHub Desktop.
Disrupting ruby naming

Disrupting Ruby Naming

In the source files below, 'slash' means it is in a subdirectory. subdirectories are not allowed in gists. Find the source here

Let's say you, a native Spanish speaker, wanted to write some code and use a library, but you wanted it to be equally functional to a library that already exists, but have Spanish names. So far, our community has been focused on and expecting that everybody learn English. This is enforced in several places, but primarily in our programming languages. Let us be code librarians and ask ourselves: shouldn't we change that?

C

At first, I terrorized C's standard library by replacing the function names with those readable in Spanish. It was a good time.

C is actually rather easy. It has a linking stage! This means it has a preprocessing step of turning code into machine language where it is placed into permanent, static, relative positions. It is like when typesetting a book... you put the chapters in order, you know what pages they fall on to make the table of contents. At the end of the day, why consult the table of contents every time? Just replace references to chapters with page numbers. "Refer to 'Chapter 2: Introducing Linear Algebra'" can just be "Go to page 52." That's what a compiler ultimately does. But, in the intermediate step, it uses the names to know how to organize and lay out the code mostly because humans read and write it and also because it doesn't know where these chapters/functions will ultimately end up.

So names get replaced with their positions anyway. So renaming a function (adding something redundant to the table of contents) is relatively easy. You either tell the linker (the program that does this) to map a new name to an existing address, which is how the above works, or you tell the compiler to generate new symbols when compiling the original file. Easy. Basically: computers hate names, so you can override them.

Ruby

Interpreted/dynamic languages are a different story. They let you alter the table-of-contents structure that maps names to code while the program is running. If you change the name of something, other pieces of code won't know how to look it up by its old name! We got away with it above because that lookup never changed.

If you don't like the names somebody else has chosen for you, well, what are you going to do? If you alias their name, then somebody else's code who likes/tolerates that name will suddenly not work with your code. That's not good. Collaboration is important, regardless of how bad/political the function names of a library happen to be.

So, you have to embrace the dynamic nature in order to support renaming/reorganization. I've focused here on simply renaming methods, but for the rubyists out there: renaming classes is similar (in ruby, classes are constants defined within modules... so theoretically a similar process would be perfectly fine!)

So now, when you call a renamed method on a class, it does a lookup and decides, based on where you've called it (the source file's location,) which methods are actually available. It keeps a mapping of source files to name lists and knows how to map those names to the original functions. The original functions are not available in a different namespace, for example "Spanish."

I've basically completely hijacked the language's method dispatcher.

So, if you have a program 'moo_en' and it is using the engUS version of Foo, then it can only call 'say' and not 'decir' which is available for the spaES version of Foo. Yet, if moo_en uses Carol's code which is written in Spanish, it will not conflict, in spite of ruby's dynamic nature. Neat.

Below is such a situation. moo_en.rb uses the English renaming of the Foo class (see: original/foo.rb vs engUS/foo.rb) and moo_es.rb uses the Spanish renaming, so it only responds to Foo.decir. Yet moo2.rb doesn't specify. So it uses the original programmers' names. Even though both moo_en.rb and moo_es.rb use moo2.rb for their own use, this doesn't conflict at all. moo2.rb is agnostic to that choice and works fine in both environments.

You could ultimately replace the entirety of ruby's libraries with your own names and abstractions without disrupting other libraries. And you could write code within the same environment as others, but your environment looks completely different (in a different language, in this case)

# This file implements 'Aliaser' that when included into a class allows one to
# rename methods implemented in that class within a selectable context.
# When a source file selects a context, the renamed methods and not their
# originals will be available.
# The methods are in english, yet the code to map the names could be machine
# generated. So, the 'define' and 'rename' methods might as well be
# randomly generated names.
class Object
module Aliaser
# When included into modules, this will allow classes to rename functions
# but only for the files that include the renamed class.
def self.included(m)
super
m.send(:extend, ClassMethods)
end
module ClassMethods
# Define the 'tag' such as :engUS, :spaES, etc
# When one does a 'require' it will require the interface from a subdir
# for that tag.
def define(tag)
@@__name_sets ||= {}
@@__name_sets[:original] ||= {}
@@__name_sets[tag] = {}
@@__defining = tag
end
# Renames a method for the current tag
def rename(a, b)
@@__name_sets[@@__defining][b] = :"__#{a}"
@@__name_sets[:original][a] = :"__#{a}"
class<<self
self
end.class_eval do
alias_method :"__#{a}", a
remove_method a
end
end
# Handle 'respond_to' for the current tag
def respond_to?(m, *args)
set_name = Kernel.__set
names = @@__name_sets[set_name]
if names
f = names[m]
if f
return true
end
end
super(m, *args)
end
# Call the correct method for the current tag
def method_missing(m, *args)
set_name = Kernel.__set
names = @@__name_sets[set_name]
if names
f = names[m]
if f
return send(f, *args)
end
end
super(m, *args)
end
end
end
end
# When a class we know about is loaded, note the calling
# source file and note which name set it wants to use
module Kernel
alias_method :__require, :require
alias_method :__require_relative, :require_relative
# Get the current tag
def __set()
caller.each do |c|
source = c[/^([^:]+):/,1]
set = @@__sets[source]
return set if set
end
:original
end
# When we require, we can select a tag by passing a symbol:
# require :engUS
# Which means all subsequent requires for THIS module will
# require from INCLUDE_PATH/engUS/*
# When we require a file, the default tag for that file is
# applied as :original which means it may require files from
# the base INCLUDE_PATH
def require(file, *args)
source = caller[0][/^([^:]+):/,1]
@@__sets ||= {}
if file.is_a? Symbol
set_name = file
@@__sets[source] = set_name
else
set_name = @@__sets[source]
if set_name
begin
return __require(File.join(set_name.to_s, file))
rescue
end
else
$:.each do |p|
path = File.join(p, file)
if File.exists? path
file = path
break
end
end
@@__sets[file] = :original
end
__require(file, *args)
end
end
# Require_relative does the same as require except also searches the base path of the
# caller.
def require_relative(file, *args)
paths = $:
paths.push(File.realpath(File.absolute_path(File.dirname(caller[0][/^([^:]+)/,1]))))
paths.each do |p|
path = File.join(p, file)
path = "#{path}.rb" unless path.end_with? ".rb"
if File.exists?(path) || File.exists?(path + ".rb")
file = File.realpath(path)
break
end
end
@@__sets[file] = :original
__require_relative(file, *args)
end
end
class Foo
def self.outputstr
puts("HELLO")
end
end
require 'foo'
class Foo
include Aliaser
define :engUS
rename :outputstr, :say
end
require 'foo'
class Foo
include Aliaser
define :spaES
rename :outputstr, :decir
end
require 'foo' # imports code at original/foo.rb
Foo.outputstr # Works!
require :engUS
require 'foo' # imports code at engUS/foo.rb
puts Foo.respond_to? :say # true
puts Foo.respond_to? :decir # false
puts Foo.respond_to? :outputstr # false
# HELLO
Foo.say
require_relative './moo2'
require :spaES
require 'foo' # imports code at spaES/foo.rb
puts Foo.respond_to? :decir # true
puts Foo.respond_to? :say # false
puts Foo.respond_to? :outputstr # false
# HELLO
Foo.decir
require_relative './moo2'
# The arguments to ruby could be automatically aliased
# so as to always be there on any call into ruby
$ ruby -r'./aliaser' -Ioriginal -I. moo_es.rb
true # respond_to? :decir
false # respond_to? :say
false # respond_to? :outputstr (original method not available)
HELLO # Foo.decir
HELLO # moo2.rb output
$ ruby -r'./aliaser' -Ioriginal -I. moo_en.rb
true # respond_to? :say
false # respond_to? :decir
false # respond_to? :outputstr (original method not available)
HELLO # Foo.say
HELLO # moo2.rb output (uses original methods)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment