I’ve held an introductory talk on Category Theory today.
The slides have notes with additional information.
If you don’t understand something feel free to contact me.
I’m happy to help you and enhance the slides.
After reading Avdi Grimm’s book “Confident Ruby” and my work on CallBaecker and defp I had an Idea for an implementation of soft typing in Ruby.
My goal was to build a shorthand for ruby’s conversion methods syntactic similar to other languages.
By relying on ruby’s conversion methods I preserved the initial flexibility of ruby. Furthermore I’ve added extended Error messages to ease debuging code. And you can add custom types.
So I’m happy to publish my take on soft typing in ruby.
[# Type , explicid, implicid[String,:to_s,:to_str],[Integer,:to_i,:to_int],[Array,:to_a,:to_ary],[Complex,:to_c,:to_c],[Float,:to_f,:to_f],[Hash,:to_h,:to_hash],[Rational,:to_r,:to_r],[IO,:to_io,:to_io],[Proc,:to_proc,:to_proc],[Symbol,:to_sym,:to_sym],[Thread,:join,:join]]
In the future I want to add the possibility to use default values with a specified type.
If you have any additional types, ideas or enhancements, feel free to open a pull request, issue or leave a comment :)
Defp is a pattern matcher implementation written for & in ruby.
It is build on top of CallBaecker, which adds callbacks to methods.
A before hook assigns the arguments of the called method to an instance variable.
Defp tests the provided pattern against this instance variable.
If the pattern matches, defp will throw a specific signal to CallBaecker.
This signal causes CallBaecker to terminate the method and return the provided value from the throw.
What we’ll learn from this
This article exposes the inner workings of Defp and its dependency CallBaecker.
You can expect a lot of metaprogramming.
Why
I’ve spend a rather long time with Haskell over the past couple of weeks.
After I returned to Ruby I’ve noticed the absence of a few techniques.
One of them is pattern matching. In ruby it is possible to match against a pattern,
but for complex patterns it is a mess. So I wanted something resembling Haskell.
First I altered the code a bit:
* Indent the code normally.
* The underscore is a method that returns false. I will remove this syntactic sugar.
* ‘==’ is an instance method on the return value of defp. I will change it to call.
By now the code has become much clearer.
How does defp access the arguments of the ‘example’ method?
CallBaecker modifies the ‘example’ method to store its arguments in an instance variable.
CallBaecker relies on method_added(name). This method is invoked for each method which is added to the receiver.
When method_added is called it will define a new method.
This method saves its arguments into the instance variable __last_args.
Afterward comes a catch and inside its block the original method is invoked.
Now throw can be used in defp to terminate the ’example’ method without a return statement. This is due to the fact tha catch doesn’t care about its static scope.
Now we need to make sure that when we call our method we execute the one with the callback.
The ‘example’ method with an callback is now defined as ‘example_with_before_each_method’.
To be able to invoke ‘example_with_before_each_method’ when ‘example’ is called. I leverage from the alias_method for this.
First I create an alias for the original method to the ‘without’ callback name.
And a second Alias for the ‘with’ method name with the initial method name.
Then I make sure that method_added won’t be triggert for the callback methods with an guard.
This works only for instance methods, but how does it for class methods?
Its similar. Three things need to be replaced:
* method_added with singleton_method_added
* define_method with define_singleton_method
* alias_method(name, with) with singleton_class.send(:alias_method, :sym0, :sym1)
This is straight forward. If the pattern matches it instantiate a new Between object otherwise a NullBetween object.
NullBetween and Between implementation looks as follows:
If ‘==’ is send to NullBetween nothing will happen. On the other hand Between.new == arg throws CallBaeckerDone with the provided argument.
This will terminate the method and return the provided value.
How does the matching work?
First the pattern arguments are zipped with the last method argumenst, which will result in this data structure:
123
[[pattern_args[i],last_args[i]]]
Then the patterns are selected that aren’t false.
Then the type of the argument is tested.
If the argument is a hash we reject every value that is false.
Then pattern_args[i], last_args[i] are tested against each other.
If all pattern matches it returns true.
123456789101112131415
defmatches_pattern?(*args)# arg[1] == called_by_method_args# arg[0] == pattern_argsargs.zip(@__last_args).select{|arg|arg[0]}.all?do|arg|# TODO cleanupifarg[0].is_a?Hashpattern_args=arg[0].reject{|k,v|!v}pattern_args.keys.all?{|key|arg[1][key]==arg[0][key]}elsearg[0]==arg[1]endendend
Defp includes and extends itself to have a class and an instance method of defp and _ available.
The Defp module needs to be extended before the CallBaecker module is included.
So that the defp methods won’t be modified by it.
Conclusion
Ruby is a flexible language. So flexible that we can add new behaviour with ease and it is short. I am happy with the result and I hope at least someone learned something new about ruby :)