Haskell type classes in Ruby.
Current state:
Goals:
The gem is under development. Don't try to use it in production code.
To install type in terminal
gem install typeclass
or add to your Gemfile
gem 'typeclass', '~> 0.3.0'
To learn how to use the gem look at the examples.
The main goals of this project is to create statically typed subset of Ruby inside dynamically typed Ruby programs as a set of functions which know types of it's arguments. There is something like function decorator which checks if function is correctly typed after it is defined. Type declarations are needed for typeclass definition only. All other types are known due to type inference, so the code looks like normal Ruby code.
Of course there is a runtime overhead due to the use of type classes. Therefore another important goal is an optimiaztion which is possible because of the known types. It can be performed with bytecode generation at runtime. In this way the bytecode generated by Ruby interpreter will be replaced with the optimized code generated directly from the source. If the optimized bytecode can not be generated due to some reasons (no back end for the virtual machine, for example), the code can be interpreted in the usual way because it is still a normal Ruby code.
Please read this article if you are unfamiliar with Haskell type classes (understanding of Rust traits should be enough).
Let's look at the following example and realize which parts of the code can be statically typed.
Show = Typeclass.new :a do
fn :show, [:a]
end
Show.instance Integer do
def show(a)
"Integer(#{a})"
end
end
Show.instance String do
def show(a)
"String(#{a.dump})"
end
end
puts Show.show(5) #=> Integer(5)
puts Show.show('Qwerty') #=> String("Qwerty")
As you can see, that there is no annoying typesig's, typecheck's, sig's, and again typesig's. Definitions of type classes and instances, and function signatures looks like typical Haskell code. The functions, in turn, are just Ruby methods.
Nevertheless, the types of the arguments are known and can be checked
in Typeclass#instance
method after it's block is executed.
There are a few options how the statically and dynamically typed parts of code interact with one another.
Let's look at each separately.
Foo = Typeclass.new a: Object, b: Object do
fn :foo, [:a, :b]
end
class Bar
def bar(b)
# ...
end
end
Typeclass.instance Foo, a: Bar, b: Integer do
def foo(a, b)
a.bar(b)
end
end
In this case we can not know how method Bar#bar
uses it's arguments,
so we can only call the method without any checks and optimizations.
Foo = Typeclass.new s: Object do
fn :foo, [:s]
end
Typeclass.instance Foo, s: String do
def foo(s)
s + s.reverse
end
end
Typeclass.instance Foo, s: Symbol do
def foo(s)
(s.to_s + s.to_s.reverse).to_sym
end
end
Foo.foo 'abc' #=> "abccba"
Foo.foo :abc #=> :abccba
In the last two lines the function is called with arguments of two different types, so we have to choose the right typeclass' instance at runtime. This operation has a huge runtime overhead which can not be avoided.
But there is a solution. Sometimes the right instance can be definitely determined by the type of the first argument of a function. In this case the function can be turned into method of it's first argument. This is called infix function, and will be described in future versions of this document.
This is the most convenient option for optimizations. Presumably the code will be close to the machine code in execution speed and memory consumption.
The previously described model has great ability to optimize business logic only. This is absolutely pointless.
The gem aims to allow to effectively "crunch numbers" in Ruby, what means strongly optimized arithmetic. The main problem is that Ruby's standard library is written in Ruby and C, so we can not analyze it's code at runtime.
Nevertheless, it is a small problem. The Ruby's standard library is well-known.
We can assume it's properties. This should be enough for optimizations
of arithmetics (the result of 2 + 2
is evident). The Ruby's ability of
monkey-patching (when method Integer#*
is redefined to return something other
than result of integer multiplication, for example) can be ignored because this
is a terrible practice.