Carbonate
Carbonate is a Lisp dialect heavily influenced by Clojure. It is transpiled into Ruby code.
Carbonate tries to cover all of Ruby's functionality while giving a more concise form to the code.
Here's what it looks like:
(defclass User
(defmethod initialize [first-name last-name email]
(def @first-name first-name)
(def @last-name last-name)
(def @email email))
(defmethod full-name []
(join [@first_name @last_name]))
(defmethod each-name []
(each [@first-name @last-name] #([name] (@yield name)))))
The code above is equvalent to the following ruby:
class User
def initialize(first_name, last_name, email)
@first_name = first_name
@last_name = last_name
@email = email
end
def full_name
[@first_name, @last_name].join
end
def each_name
[@first_name, @last_name].each do |name|
yield name
end
end
end
Installation
gem 'carbonate'
$ bundle
Usage
Currently there are 2 ways to run Carbonate code: convert it to Ruby statically and use the resulting .rb
files as you normally would or evaluate Carbonate source files dynamically.
Converting from Carbonate to Ruby
The gem ships with a crb2rb
utility that converts Carbonate source code into Ruby source code. You can use it to convert a Carbonate file to a Ruby file:
$ crb2rb < source.crb > target.rb
$ crb2rb -i source.crb -o target.rb
Using Carbonate sources directly
Carbonate source code can be transpiled and instantly evaluated by Ruby code. This allows you to plug it in a Ruby application and use it right away.
Carbonate gives 2 functions to transpile and evaluate Carbonate sources: Carbonate.require
and Carbonate.require_relative
- they work exactly like their counterparts from Ruby's Kernel
but they are searching for a .crb
file instead of a .rb
one, and they transpile it to Ruby before evaluating.
Syntax
Literal values
Numbers in Carbonate look exactly like they do in Ruby:
127
-32
3.14
Strings are always double-quoted and support all the usual control characters like \n
and \t
. Double quotes have to be escaped with a backslash. String interpolation is not supported.
"Hello world!"
"Line 1\nLine 2\n\tIndented line"
"Yukihiro \"Matz\" Matsumoto"
Symbols look similarly to Ruby symbols but use dashes (-
) instead of underscores (_
):
:north
:user-name
:exists?
Regular expressions are written as double-quoted strings prefixed with a pound sign (#
). Like strings, they support control characters and require you to escape double quotes.
#"[A-Za-z]+"
true
, false
, and nil
are the same as in Ruby.
Arrays are enclosed within brackets ([]
) but do not require commas between elements. In fact, comma is treated as a whitespace character in Carbonate - you can use it but you don't have to.
[1 2 3]
["Yukihiro Matsumoto" "Rich Hickey"]
Hashes are represented by key-value pairs inside curly brackets ({}
). In contrast to Ruby, there are no delimiters between a key and a value. Separating pairs with commas can sometimes be useful to keep readability.
{:type :book
:title "SICP"
:authors ["Harold Abelson" "Gerald Jay Sussman" "Julie Sussman"]}
Sets are also enclosed within curly brackets but prefixed with a pound sign.
#{"one" "two" "three"}
Be sure to require 'set'
to use them - sets live in a standard library package in Ruby (read on to learn how to call methods like require
in Carbonate).
Ranges look exactly like in Ruby - values separated with two dots for inclusive ranges and values separated with three dots for exclusive ones:
"a".."z"
0...10
Constants are written down using the same CamelCase'd words as in Ruby but .
is used as a delimiter:
Carbonate.Parser
Explicit top-level constants are prefixed with .
(exactly like they are with ::
in Ruby):
.Hash
The current object known as self
in Ruby is written down as @
in Carbonate.
Calling functions/methods
In Lisp function calls are written down using prefix notation in S-expressions; basically this means that every operation is a list of elements enclosed within parentheses where the first element represents the function and all the other elements are it's arguments.
Here's some basic arithmetic:
(+ 2 2)
(- 2 1)
(* 2 3)
Of course S-expressions can be nested:
(/ (* 3 4) 2)
(** (- 4 2) 3)
Comparison operators also mirror the Ruby ones with an exception of equality - it is represented with a single =
:
(= x y)
(!= x y)
(< 1 2)
(> 2 1)
(>= 2 2)
(<= 2 2)
(<=> a b)
(=== a b)
Binary operators &
, |
, ^
, ~
, <<
and >>
follow their Ruby counterparts. Logic operators, on the other hand, are slightly changed:
(and (= x y) (!= x z))
(or (> x z) (> y z))
(! false)
Variables and assignment
Carbonate supports local and instance variables. They look like in Ruby but use -
separator instead of _
:
local-variable
@instance-variable
You can assign a value to a variable using def
keyword:
(def a 1)
(def @age 35)
This also works for constants:
(def DAYS-IN-WEEK 7)
Carbonate also supports the so-called conditional assignment (||=
in Ruby) using def-or
keyword:
(def-or name "Steve")
(def-or @city "NYC")
(This is not supported with constants for obvious reasons)
There are a few more special cases. First you can assign a value to an object's attribute (like you would do with user.name = 'John'
in Ruby):
(def user.name "John")
Second you can both read from and write to an array or a hash member using essentially the same syntax as in Ruby:
user[:name]
(def user[:name] "John")
Both attribute and collection member writers support conditional assignment with the aforementioned def-or
.
Conditional statements
Almost any code contains conditional execution - you won't go far without if
& unless
statements so here they are:
(if (> 2 1) "2 is greater" "1 is greater")
(if (= x 5) "x is 5")
(unless (>= age 18) "too young")
As you can see from the snippet above, if
can be used in 2 variations: if you pass it a condition and 2 more forms (S-expressions, literal values, variables or anything that returns a value) the first form will be used for "truthy" condition value and the second for "falsy". If you just pass one form it will be used for the "truthy" case.
unless
doesn't have a 2-form mode - it's a bad practice anyway - so the only form after the condition will be used for the falsy condition.
If you need to group several statements inside one if
or unless
statement you can use a do
statement - it allows you to join several S-expressions into one:
(if (valid? user)
(do
(save user)
(@puts (name user))))
Carbonate also has a case
statement:
(case x
1 "one"
2 "two")
Pretty self-explanatory. It also supports an else clause as the last form of the statement:
(case lang
"clojure" "great!"
"ruby" "cool"
"crap")
Loop statements
There are 2 main loop statements in Carbonate: while
and until
. Both of them take a condition as the first argument and the loop body as the second:
(while (< x 5)
(def x (+ x 1)))
(until (>= x 5)
(def x (+ x 1)))
Calling methods
The most common construct in Ruby code is a method call. Carbonate allows you to call a method within an S-expression consisting of the method name and the receiver object:
(name user)
This is equivalent to the following Ruby:
user.name
(all Carbonate snippets are followed by equivalent Ruby snippets later on)
If you need to pass some arguments to a method you do so after the receiver:
(include? [1 2 3] 4)
[1, 2, 3].include?(4)
Carbonate supports splat arguments - if you have some Enumerable
collection you can pass it to the method as several separate arguments. Ruby uses *
for that goal, Carbonate uses &
(note that &
and the argument are separated by a space):
(add-tags article & tags)
article.add_tags(*tags)
Class methods are invoked a little differently - the method name is prefixed with the class name separated with /
, and all following elements are method's arguments:
(User/count)
(User/find-by {:first-name "John"})
User.count
User.find_by(first_name: 'John')
Method calls without an explicit receiver (which are implicitly called on self
) are written with a method name prefixed by @
:
(@attr-reader :first-name)
attr_reader :first_name
Carbonate offers a special syntax for class constructor calls - it looks like a class name followed by a dot:
(User. {:name "John"})
User.new(name: 'John')
Another special case is super
- a call to parent class' respective method:
(super)
(super "parameter")
super()
super('parameter')
The tricky part here is a call to super
with implicit parameters - as you may know, calling super
without parameters and without parentheses in Ruby actually passes it all the parameters passed to the enclosing method, and if you need to force super
call without parameters you have to write super()
. The latter is written as just (super)
in Carbonate and the former is (zsuper)
(Zero-arity super).
Like in Ruby, you can pass a block to a method - it is enclosed within parentheses prefixed with #
, and the first element inside the parentheses is the block parameters list:
(map users #([user] (upcase (name user))))
users.map do |user|
user.name.upcase
end
You don't have to specify an empty parameters list if your block has no parameters - just type your block body inside #(...)
:
(times 5 #(@puts "Hello"))
5.times { puts 'Hello' }
If you want to use the Symbol#to_proc
trick you can just pass the symbol after #
(like &
, it needs to be separated from the following element with a space):
(map users # :name)
users.map(&:name)
This works with plain procs too:
(each users # some-proc)
users.each(&some_proc)
Defining methods
A method definition consists of the defmethod
keyword, then the name of the method, then the parameters list and the method body. As usual, all the identifiers (method name, parameters and variables) use -
instead of _
. It looks like this:
(defmethod full-name []
(join [@first-name @last-name]))
def full_name
[@first_name, @last_name].join
end
The parameters list supports splat arguments and blocks with basically the same syntax as in method invocation:
(defmethod iterate [a b c & d # block]
(@puts a b c d)
(@yield a)
(@yield b)
(@yield c)
(each d # block))
def iterate(a, b, c, *d, &block)
puts a, b, c, d
yield a
yield b
yield c
d.each(&block)
end
Carbonate also supports default parameter values - just put the parameter name with a default value inside the brackets:
(defmethod int-to-string [int [base 10]]
(to-s int base))
def int_to_string(int, base = 10)
int.to_s(base)
end
A return
clause is used like this:
(defmethod nothing [] (return))
(defmethod name [] (return "John"))
def nothing
return
end
def name
return 'John'
end
Exception handling
If you need to use a rescue
clause inside your method you can just put it at the end of the method body:
(defmethod read-file [path]
(File/read path)
(rescue Errno.ENOENT e
(@puts "No file found.")))
def read_file(path)
File.read(path)
rescue Errno::ENOENT => e
puts 'No file found.'
end
(you can have as many rescue
clauses as you want - just be sure to keep them at the end of method body)
The ensure
clause is available as well, you can put it after all rescue
clauses (or at the end of the method body if you don't need to rescue anything):
(defmethod read-file [path]
(File/read path)
(rescue Errno.ENOENT e (@puts "No file found."))
(ensure (@puts "Tried to read a file.")))
def read_file(path)
File.read(path)
rescue Errno::ENOENT => e
puts 'No file found.'
ensure
puts 'Tried to read a file.'
end
There are cases when you need to intercept an exception from an arbitrary piece of code, not just method; a try
statement can help you with that:
(try (File/read path)
(rescue Errno.ENOENT e
(@puts (message e))
(@raise)))
begin
File.read(path)
rescue Errno::ENOENT => e
puts e.message
raise
end
try
allows you to put any number of rescue
clauses and/or an ensure
clause at the end, just like method bodies.
Lambdas
Lambda definitions look a lot like method definitions - they have a parameters list and a body:
(-> [user] (email user))
(-> [user] (@p user) (save user))
-> (user) { user.email }
-> (user) do
p user
user.save
end
If the lambda doesn't have parameters you can omit them altogether:
(-> (@puts "Hello world!"))
-> { puts 'Hello world!' }
Classes and modules
No serious Ruby application can exist without classes and modules. Carbonate allows defining classes with defclass
keyword:
(defclass User
(defmethod initialize [first-name last-name]
(def @first-name first-name)
(def @last-name last-name))
(defmethod full-name []
(join [@first-name @last-name])))
class User
def initialize(first_name, last_name)
@first_name = first_name
@last_name = last_name
end
def full_name
[@first_name, @last_name].join
end
end
If you need to specify the parent class you can use an already familiar syntax:
(defclass User < Base
(@include Naming))
class User < Base
include Naming
end
Similarly a module can be defined with defmodule
:
(defmodule Naming
(@attr-reader :first-name :last-name)
(defmethod full-name []
(join [first-name last-name])))
module Naming
attr_reader :first_name, :last_name
def full_name
[first_name, last_name].join
end
end
When you want to open up an object's singleton class you need <<-
:
(<<- user
(defmethod name []
@name))
class << user
def name
@name
end
end
Acknowledgements
This project would not be possible without these wonderful libraries:
Roadmap
- improve parser performance
- add meaningful stack traces
- add macros support
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake[ spec]
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at 7even/carbonate.
License
The gem is available as open source under the terms of the MIT License.