What & Why
Philosophy
Atomy is a language designed to grow. It achieves this goal by having a very simple grammar, much like Lisp. Rather than statements, everyting in Atomy is an expression. These expressions are not merely written, they are designed. Readability, concision, and flow are the primary goal, and this goal is most easily achieved by keeping the grammar small, and building everything up from there with macros and metaprogramming.
Time after time new languages are introduced, which are often snapshots of what we needed when it was designed. These languages have so many features built-in as syntax that countless libraries and applications hinge on version numbers solely to get past the parsing stage. Ruby and Python are examples of these.
Atomy instead hopes to introduce these features as libraries. The grammar in 1.0 should either be "the end" of its progression, or later generalized in a way that is backward-compatble.
Idioms & Freebies
Platform
Atomy's platform of choice is the Rubinius VM. Its syntax already scales upward to encompass every common Ruby construct (with its own flavor), and Ruby libraries can be used transparently, as long as they run on Rubinius.
It's Algebraic!
An integral part of Atomy's core is pattern-matching dispatch. This gives you immense expressive power, with a form of multiple dispatch. Atomy's style favors declaring your data structures first and defining methods in a functional style in terms of all of their "roles" (the receiver and the message's arguments).
To show this, I'll go through part of the the Doc
system, used for pretty-printing, which exemplifies this beautifully.
Here we define each type of Doc
there is, structurally. This creates classes and subclasses recursively, as well as constructors and accessors for the data they specify:
data(Doc):
Empty
Beside(@left, @right, @space?)
Above(@above, @below, @overlap?)
Text(@value)
One thing we'll want to do with a document is compute its width. This is trivial; just a single message sent to the Doc
with no arguments. Note that we're defining each method alongside one another; not inside of a class body. This allows us to group related methods by meaning, not by their receiver. This better shows the relationships between each method:
Empty width := 0
Text width := @value size
Beside width :=
if(@space?)
then: 1 + @left width + @right width
else: @left width + @right width
Above width := [@above width, @below width] max
A great example of pattern-matching and multiple dispatch comes into play when composing two documents with the <+>
operator. This positions two Doc
s beside each other, separated by a space, unless either are Empty
:
(l: Doc) <+> (r: Doc) := Beside new(l, r, true)
(d: Doc) <+> Empty := d
Empty <+> (d: Doc) := d
(l: Doc) <+> (a: Above) := do:
first = l <+> a above
rest = a below nest(l width + 1)
Above new(first, rest, a overlap?)
This is opposed to Ruby-style, where you open classes to add methods to them, and then handle the arguments from within the single method definition, often with the case
statement. This form is also supported, but it is much more verbose.
A Handful of Shiny Toys
- a syntax-oriented macro system
Atomy has a small grammar independent from its language semantics. For example, there is no "message send" primitive node, and there aren't class/global/instance variable primitives. These are defined using Atomy's all-powerful macro system.
You may be familiar with macros from Common Lisp, Scheme, or Clojure. In these languages a macro is a special named function that takes its arguments unevaluated, and builds another form for evaluating later. You may also know them from C/C++ as primitive text rewriting mechanisms.
Atomy goes somewhere neither really cover: its macros aren't named anything; they expand arbitrary expressions. I probably shouldn't be showing you this, but...
Example:
> macro(2 + 2): 5 #<Rubinius::CompiledMethod _expand_+ file=wrapper> > 2 + 2 5
- macro-quotes
Generalized string quotation. Examples include regular expressions, raw strings, and word-lists. It's easy to create new quoters for things other languages would have as literals.
Example:
> r"[a-z][\p{L}]*"(u) /[a-z][\p{L}]*/u > raw"s\up?" "s\\up?" > w"foo bar baz" ["foo", "bar", "baz"]
- explicit mutation
Atomy has two "assignment"-ish operators;
=
and=!
. The former will always pattern-match and introduce locals to the current immediate scope. The latter will also pattern-match, but it will only mutate existing locals, never introducing them.This keeps your locals local and makes it clear when you really want to mutate something in-place.
Example:
> a = 0 0 > do: a = 1, a 1 > a 1 > do: a =! 1, a 1 > a 1
- syntax as macros
Where Ruby has global, instance, and class variable syntax, Atomy actually implements these as prefix macros. The same goes for for symbols (
.to-a
), particles (.foo(1, _)
, splats (*args
), block-passing (&foo
), and a few others. If something can be a macro or regular method, it will be. This has been applied very broadly.- particles
Like Atomo, Atomy has a concept of "partial messages" which can act as ad-hoc data structures. They respond to
call
andto-proc
, so you can use them as block shorthand.Example:
> [1, 2, 3] collect .(* 3) [3, 6, 9]
You can also pattern-match them, so they are often used for simple data structures, similar to how atoms and tuples are used in Erlang.
Example:
> .ok(x) = .ok(1) .ok(1) > x 1
In a particle,
_
acts as a placeholder. Sendingcall
will fill these values in order with the values you pass, and then send the message.Example:
> .is-a(_)? call(1, Integer) true
Note that they have very similar syntax as
Symbol
s; in fact, Atomy also definescall
onSymbol
so you can generally use them interchangeably.Example:
> .odd? call(1) true > .odd? call(2) false
- extensible pattern-matching
If you don't mind digging down into Rubinius bytecode, you can define your own pattern-matchers. It's very easy to do, and there is a very broad spectrum of pattern-matchers built-in because of this. See pattern-matching.
Influences & Thanks
- Common Lisp & Clojure
extensive macro system, gensyms (via
names
), powerful string formatting system, conditions & restarts, thread-local dynamic environment (viadynamic
), controlled evaluation (viaevaluate-when
andfor-macro
),let-macro
, dynamic variable -based Input & Output, Clojure-style namespaces- Haskell
pattern-matching, algebraic data definition (via
data
), functional-style method defining, pretty-printing system, parens slaying with$
, list comprehension macros- Ruby
functionality baseline, with desired improvements (selector namespacing, safer alternatives to
instance_eval
for DSLs, likeonto
)- Slate & Atomo
multiple dispatch (via pattern-matching, like Atomo; Slate doesn't work that way), particles, pseudo-keyword-messages via macros and pattern-matching
- Potion & Poison
colon block syntax + commas
- Erlang
message-sending concurrency