alpha_sapphire
is a toy programming language concept. Designed for minimal syntax and fully object-oriented expressiveness
a-la Smalltalk and Ruby.
Things trying to archieve:
- Everything should be an object
- Meta-programming should be easy
- Syntax shold be minimal yet expressive
- Should be inspired by Smalltalk and Ruby
- Should give the power to the programmer, not limit him
Use the concept of code-blocks
(lambdas) for expressive OOP power. They are a very important building block in the language.
This is an example of a class:
# `class` is a method of `identifier`
Foo.class do
# Attribute
bar = 'baz'
# Method
bar = do
# arguments is an array with all parameters passed to the block
puts arguments # In this case, it's []
end
# Methog taking named optional parameters
greet = do |name:, age: 18|
new_age = age + 1
"Hello, #{name}! You will be #{new_age} next year!"
end
end
a = Foo.new
a.bar # 'baz'
a.bar() # parens are not required if no argument is needed or is optional
a.greet(name: 'Mike') # They are mandatory otherwise
We could also define a class like this:
"hello_#{name}".class do
attr_reader('bar')
foo = do
@bar = 11 # instance variable
end
end
As long as the object implements to_identifier
. Classes can be re-opened as desired. Note that instance variables are defined
using @
like Ruby. attr_reader
could be implemented like this:
Object.class do
attr_reader = do |name|
# define instance method named `name`
# and return the variable at that position
# using self.variables()[name]
add_method(name) do
variables[name]
end
end
end
add_method
and variables
are magic interpreter-land functions already in Object
. Some more methods could be
self.classname
, self.methods
, self.as_class.add_method
, self.as_class.variables
, self.as_class.methods
and super
.
Classes have an instance_context
, each instance gets it's own copy. On the other side, they all share the same class_context
. You
can access the class context from an instance as such: my_obj.as_class
. That's the "static" stuff.
Inheritance:
Foo.class do end
Bar.class(Foo) do
# I inherit from Foo
end
A mixin is a set of transformations to a class and/or instances of that class.
Foo.class do
include(SomeModule)
end
SomeModule.module do |instance_context|
instance_context.define_method('some_method') do ... end
instance_context.as_class.define_method('some_static_stuff') do ... end
end
Note that we are passed the instance_context
of the Class, all instances will have a copy of this. They all
share the class_context
which is accessed from instances as instance_context.as_class
or simple self.as_class
.
Methods should allow named optional arguments like Ruby.
Foo.class do
bar do |name:, age: 18|
"You are #{name} and you are #{age} years old."
end
end
o = Foo.new
o.bar(name: 'Mike') # "You are Mike and you are 18 years old."
o.bar(name: 'Mike', age: 22) # "You are Mike and you are 22 years old."
o.bar() # ArgumentError: parameter `name` not specified.
Minimal constructs means that things like while
and for
can be expressed with primitives rather than "embedded" into the language.
They can be replaced with just methods and code-blocks. For example:
oranges.each do |orange|
do_something_with(orange)
end
In that case, each
is just a method that takes a code-block as argument and calls it once per orange.
You can define your own iterators, standard functional-programming collection manipulation methods should be included, they are very expressive. Things like: each, map, select, collect, reduce, etc.
alpha_sapphire gives you a lot of power. It tries to make as little distinction between the language and the programmer as possible.
Conditionals, most language's if
statement, could be used like this:
bar = false
bar.false? do puts "Hello!" end
bar.true? do puts "This will not run" end
And could be implemented as such:
Boolean.class do
true? do |block|
value && block.call
end
end
&&
is short-circuit, so it will not call the block unless it's truthy (the only things which are false is an instance
of False
and an instance of Nil
.
The reason why I prefer &&
over and
is that the first one is more mathy. We are making a boolean operation afterall!
So if I wrote a story and say: "Mary and Mike" it feels like I'm grouping them, but if I do "Mary && Mike" I know that given the two
of them, I want a single result (that didn't sound very good). I'm performing an operation, not grouping things.
Exceptions work like this
Foo.class do
bar do
blah blah blah
rescue(SomeError) do |e|
# e is an instance of SomeError
end
end
In that case, rescue
is a special keyword which will be called by the interpreter if there's an Exception. The rescue
keyword can only be unsed inside a block. There's also finally
. The full syntax is below:
Foo.class do
bar do
blah blah blah
rescue(SomeError) do |e|
# e is an instance of SomeError
rescue(OtherError) do |e|
# another error
finally do
# always executed
end
end
Dictionaries
a = { a: 1, b: 2, f(x): g(y), do 2 end: do "bar" end }
Lambdas
lambda = do |x| x + 1 end
two = lambda.call(1) # 2
# short-form
two = lambda(1)
Look from the block, up. If inside a class, look in instance context if not found in block. Travel up the hierarchy
until Object.
If the name is @foo
then it's an instance variable so start looking in the instance context.
To access class variables use self.as_class.my_variable
, it looks in class context and up the hierarchy.
- Boolean (Use false instead of nil)
- Object (All classes inherit from this implicitly)
- Block (A block of code, maybe?)
- Integer
- Float
- String
- Symbol/Identifier
- Array
- Hash/Dictionary
Follow Ruby's: http://stackoverflow.com/questions/21060234/ruby-operator-precedence-table
! (unary NOT)
** (exponentiation)
- (unary minus)
* / % (multiplication, division, remainder)
+ - (addition, substraction)
&& (binary AND)
|| (binary OR)
Foo.class do
val = 1
op_+ do |other|
@val + other.to_i
end
end
foo = Foo.new
# sugar for foo.op_+(2)
foo + 2 # 3
foo.send(:message, [arg1, arg2])
foo.methods # show all direct methods
foo.methods(:all) # show all, even inherited
Optional dots?
"Hello, world!" print
# Same as
"Hello, World!".print