A Ruby platform for composition and collaboration.
The internet has caused a fundamental change in general computing, yet many programming languages are solidly centered in the Windows 3.0 era, providing their equivalent to the Windows for Workgroups add-on to enable networking. Unfortunately, Ruby is one of those languages.
To be relevant today, a programming language must provide simple yet powerful facilities for composition and collaboration. A language does not need general immutable state, purely pure functions, or complex type systems, no matter how inferred.
Rubinius X is an experiment in modernizing Ruby. Rubinius X can be imagined as a time machine that brings the future to the present, enabling us to write modern programs now.
The Rubinius X vision to enable composition and collaboration for programs, components, and systems is detailed below. These ideas will be tested through building useful concurrent and distributed applications.
The Shiny New
Promises and Non-blocking IO
Concurrency is essential for programs that collaborate or compose other components to efficiently use modern hardware. There are many approaches to concurrency. Promises compose well and are naturally represented in an object-oriented language.
Ruby presently only provides threads as a concurrency mechanism. Threads are a primitive construct and difficult to use well. They do not compose easily and can be too costly to use as a general solution to many computing tasks.
Persistent and Concurrent Data Structures
Modeling data well is the single most important concern for writing useful applications. Wherever collaboration is centered on a data structure in a program, that data structure must enable correct concurrent operation.
Much is made of immutability in some programming languages. But that immutability is often paper thin; just below the surface there is rampant mutation. Efficiently computing with complex structures as values requires this charade.
In Ruby, there is this fundamentally broken concept of frozen objects. Broken because its implementation requires littering code with brittle checks. It also violates the principle of pay for use, requiring all code to pay the penalty of checking whether an object is frozen.
In between a tyranny of immutability on the one hand and hacked together frozen objects on the other, there exists a simple, elegant domain of persistent, or purely functional data structures.
Mirrors and Object Capabilities
Composition is needed both between components and within components. The semantics of primitive operations and their composition must be clearly defined. Mirrors are a construct that provides structure for composition through encapsulation and separation of object and meta-object operations.
Object capabilities structure interactions to control collaboration. The ability to evaluate source code or invoke system operations should not be distributed willy-nilly throughout a program. Likewise, there is nothing inherently bad or dangerous about metaprogramming, but for maximum usability, it needs to be structured.
Code Loading
Code loading is the vital facility to enable combining components into a single running program. It needs to happen lazily, with clearly expressed dependencies, and the running program itself must be able to orchestrate it. In other words, it needs an API just like every other part of the system.
Primitives, Composition, and Consistency
Consistency is one of the most important characteristics of a system to aid
understanding and the ability to evolve to meet changing demands. In Rubinius
X, syntactic constructs that appear primitive simply delegate to the system.
For example, a Hash literal like { a: 1 }
simply delegates to Hash.new
. If
the system refines Hash.new
, it operates consistently everywhere.
The primitive operations on objects are defined and the more complex operations are consistent compositions of those primitive operations. Modifying the system has knowable consequences.
Immutable Strings and ByteBuffers
Strings of characters are the most fundamental exchange format on the internet. They may be human-readable strings or collections of bytes, but they are the currency of collaboration.
Making Strings immutable permits numerous simplifications and optimizations for collaboration and composition. Parsing and templated String construction are fundamental operations.
ByteBuffers provide the facility for IO operations to collect data over time before handing off a completed object. ByteBuffers also permit encoding information to flow through a system.
The Values nil and undefined
The singleton object nil
is useful. It's reasonably equivalent to the empty
set. Set operations with the empty set are well defined. For example, the
intersection of two or more sets may produce the empty set, and the
intersection of any set with the empty set is the empty set.
The singleton object undefined
means that no value is possible. The
undefined
object can be tested for identically equal to some_obj
but
cannot be sent any messages. If you get one and try to operate on it, an
exception is raised.
Behaviors
Ruby has a confusing idea of "representing an X" versus "becoming an X". For
example, generally, using obj.to_s
returns a representation of obj
as a
String while using obj.to_str
turns obj
into a String, or something. In
reality, it's very poorly defined in Ruby.
There is no need for this confusion. If we focus on behavior, then we merely
need a way to ask an object to provide something that behaves in a defined
manner. It need not return a different object, but if it doesn't, then after
requesting it behave a certain way, it should do so or return undefined
.
One of the most common places to describe behavior expectations is at method boundaries. In Rubinius X, a method definition like the following clearly expresses that String or Integer behavior is expected in the respective parameters:
def exclaim(message: String("hi"), times: Integer())
end
The operations String(message)
and Integer(times)
are automatically
performed before the method begins running so that message
and times
have
the expected behavior.
The preceding are not types. They are labels for behaviors. Where needed to disambiguate, they can be useful. They are rarely needed.
The Cleaning Up
Encoding
The fact that some encodings are not convertible into one another is not solved by permitting any object to have any possible encoding.
In Rubinius X, there is a single encoding for any running program. Transcoding facilities are available at IO boundaries and encoded bytes can be passed through the system using ByteBuffers.
Keywords and APIs
All APIs use keywords where necessary. There are no position-significant APIs like the following:
IO.readlines( portname, separator=$/ [, options])
IO.readlines( portname, limit [, options])
IO.readlines( portname, separator, limit [, options])
Numerics
Numbers are rather well-defined in mathematics, at least at the level of undergraduate mathematics. Concepts like the reals embedding the rationals are basic.
In Rubinius X, the numeric tower has Integer, Rational, Decimal, Complex and a defined coercion protocol based on the concept of embedding.
Purge the Perl
In general, anything inspired by Perl should be removed, but especially "magic
globals" like $_
or $1
. They are easily replaced by simply sending messages
to objects and providing objects as parameters.
Global Variables
Gone. Period.