How to simulate Java-like annotations in Ruby?
This is adapted from a piece of code I wrote in an answer to another question a couple of weeks ago, although it is of course hardly original. This is a well-known Ruby idiom, after all, which has been in use for many years, at least since rakes
's desc
method.
module Annotations
def annotations(meth=nil)
return @__annotations__[meth] if meth
@__annotations__
end
private
def method_added(m)
(@__annotations__ ||= {})[m] = @__last_annotation__ if @__last_annotation__
@__last_annotation__ = nil
super
end
def method_missing(meth, *args)
return super unless /\A_/ =~ meth
@__last_annotation__ ||= {}
@__last_annotation__[meth[1..-1].to_sym] = args.size == 1 ? args.first : args
end
end
class Module
private
def annotate!
extend Annotations
end
end
Here's a small example:
class A
annotate!
_hello color: 'red', ancho: 23
_goodbye color: 'green', alto: -123
_foobar color: 'blew'
def m1; end
def m2; end
_foobar color: 'cyan'
def m3; end
end
And of course no Ruby code would be complete without a testsuite:
require 'test/unit'
class TestAnnotations < Test::Unit::TestCase
def test_that_m1_is_annotated_with_hello_and_has_value_red
assert_equal 'red', A.annotations(:m1)[:hello][:color]
end
def test_that_m3_is_annotated_with_foobar_and_has_value_cyan
assert_equal 'cyan', A.annotations[:m3][:foobar][:color]
end
def test_that_m1_is_annotated_with_goodbye
assert A.annotations[:m1][:goodbye]
end
def test_that_all_annotations_are_there
annotations = {
m1: {
hello: { color: 'red', ancho: 23 },
goodbye: { color: 'green', alto: -123 },
foobar: { color: 'blew' }
},
m3: {
foobar: { color: 'cyan' }
}
}
assert_equal annotations, A.annotations
end
end
Access attributes/methods comments programmatically in Ruby
No, you cannot do this.
The whole point of comments is that they are not part of the program! If you want a string that is part of your program, just use a string instead.
In most Ruby implementations, comments already get thrown away in the lexer, which means they don't even reach the parser, let alone the interpreter or compiler. At the time the code gets run, the comments are long gone … In fact, in implementations like Rubinius or YARV which use a compiler, there is simply no way to store the comments in the compiled executable, so even if they weren't thrown away by the lexer or the parser, there would still be no way to communicate them to the runtime.
So, pretty much your only chance is to parse the Ruby sourcefile to extract the comments. However, like I mentioned above, you cannot just take any parser, because most of the exisiting parsers throw comments away. (Which, again, is the whole point of comments, so there's nothing wrong with the parser throwing them away.) There are, however, Ruby parsers which preserve comments, most notably the ones used in tools such as RDoc or YARD.
YARD is especially interesting, because it also contains a query engine, which lets you search for and filter out documentation based on some powerful predicates like class name, method name, YARD tags, API version, type signature and so on.
However, if you do end up using RDoc or YARD for parsing, then why not use them in the first place?
Or, like I suggested above, if you want strings, just use strings:
module MethodAddedHook
private
def method_added(meth)
(@__doc__ ||= {})[meth] = @__last_doc__ if @__last_doc__
@__last_doc__ = nil
super
end
end
class Module
private
prepend MethodAddedHook
def doc(meth=nil, str)
return @__doc__[meth] = str if meth
@__last_doc__ = str
end
def defdoc(meth, doc, &block)
@__doc__[meth] = doc
define_method(meth, &block)
end
end
This gives us a method Module#doc
which we can use to document either an already existing method by calling it with the name of the method and a docstring, or you can use it to document the very next method you define. It does this by storing the docstring in a temporary instance variable and then defining a method_added
hook that looks at that instance variable and stores its content in the documentation hash.
There is also the Module#defdoc
method which defines and documents the method in one go.
module Kernel
private
def get_doc(klass, meth)
klass.instance_variable_get(:@__doc__)[meth]
end
end
This is your Kernel#get_doc
method which gets the documentation back out (or nil
if the method is undocumented).
class MyClass
doc 'This method tries over and over until it is tired'
def go_go_go(thing_to_try, tries = 10)
puts thing_to_try
go_go_go thing_to_try, tries - 1
end
def some_other_meth; end # Oops, I forgot to document it!
# No problem:
doc :some_other_meth, 'Does some other things'
defdoc(:yet_another_method, 'This method also does something') do |a, b, c|
p a, b, c
end
end
Here you see the three different ways of documenting a method.
Oh, and it works:
require 'test/unit'
class TestDocstrings < Test::Unit::TestCase
def test_that_myclass_gogogo_has_a_docstring
doc = 'This method tries over and over until it is tired'
assert_equal doc, get_doc(MyClass, :go_go_go)
end
def test_that_myclass_some_other_meth_has_a_docstring
doc = 'Does some other things'
assert_equal doc, get_doc(MyClass, :some_other_meth)
end
def test_that_myclass_yet_another_method_has_a_docstring
doc = 'This method also does something'
assert_equal doc, get_doc(MyClass, :yet_another_method)
end
def test_that_undocumented_methods_return_nil
assert_nil get_doc(MyClass, :does_not_exist)
end
end
Note: this is pretty hacky. For example, there is no locking, so if two threads define methods for the same class at the same time, the documentation might get screwed up. (I.e.: the docstring might be attributed to the wrong method or get lost.)
I believe that rake
does essentially the same thing with its desc
method, and that codebase is much better tested than this, so if you intend to use it in production, I'd steal Jim's code instead of mine.
Is there a convention for memoization in a method call?
I would do it like this:
def filesize
@filesize ||= calculate_filesize
end
private
def calculate_filesize
# ...
end
So I'd just name the method differently, as I think it makes more sense.
Java-like annotations in C++
C++0x will have this feature, where you can explicitly specify whether a member function is meant to override a base class' function, use a default implementation generated by the compiler and much more.
Custom Hook/Callback/Macro Methods
Here's a solution that uses prepend
. When you call before_operations
for the first time it creates a new (empty) module and prepends it to your class. This means that when you call method foo
on your class, it will look first for that method in the module.
The before_operations
method then defines simple methods in this module that first invoke your 'before' method, and then use super
to invoke the real implementation in your class.
class ActiveClass
def self.before_operations(before_method,*methods)
prepend( @active_wrapper=Module.new ) unless @active_wrapper
methods.each do |method_name|
@active_wrapper.send(:define_method,method_name) do |*args,&block|
send before_method
super(*args,&block)
end
end
end
end
class SubClass < ActiveClass
before_operations :first_validate_something, :do_this_method, :do_that_method
def do_this_method(*args,&block)
p doing:'this', with:args, and:block
end
def do_that_method; end
private
def first_validate_something
p :validating
end
end
SubClass.new.do_this_method(3,4){ |x| p x }
#=> :validating
#=> {:doing=>"this", :with=>[3, 4], :and=>#<Proc:0x007fdb1301fa18@/tmp.rb:31>}
If you want to make the idea by @SteveTurczyn work you must:
- receive the args params in the block of
define_method
, not as arguments to it. - call
before_operations
AFTER your methods have been defined if you want to be able to alias them.
class ActiveClass
def self.before_operations(before_method, *methods)
methods.each do |meth|
raise "No method `#{meth}` defined in #{self}" unless method_defined?(meth)
orig_method = "_original_#{meth}"
alias_method orig_method, meth
define_method(meth) do |*args,&block|
send before_method
send orig_method, *args, &block
end
end
end
end
class SubClass < ActiveClass
def do_this_method(*args,&block)
p doing:'this', with:args, and:block
end
def do_that_method; end
before_operations :first_validate_something, :do_this_method, :do_that_method
private
def first_validate_something
p :validating
end
end
SubClass.new.do_this_method(3,4){ |x| p x }
#=> :validating
#=> {:doing=>"this", :with=>[3, 4], :and=>#<Proc:0x007fdb1301fa18@/tmp.rb:31>}
Scripting language to compiled language or pure compiled language, which is faster?
(Disclaimer: this might not be a complete answer but it was too long for a comment)
Although I have next to no experience with JRuby's implementation in particular, dynamic languages implemented on the JVM have their limitations in terms of the speed you can get compared to what you would get if you coded directly in Java.
From what I understand, this comes from the trade-offs client language implementations (JRuby, Clojure, Jython, etc.) have to make, in order to "simulate" their inner workings on the host platform (in this case the JVM). Maybe in the most common scenarios the performance difference is not that bad since the JVM's HotSpot optimizations kick in, but when it comes down to getting Java-like code performance, you might need to dive into the details of the language implementation in order to bypass some of its limitations.
In the case of the Ruby extensions in C, what you are actually executing is native code (machine language) that was originally coded in C, but which you can call from your Ruby code. When running JRuby code, the language you are programming in is Ruby, which has to be compiled to Java bytecode, which runs on the JVM, which translates that into operations in the native operating system. Something analog to Ruby/C would be in this case JRuby/Java, that would be calling some Java code, that was originally written in Java from your JRuby program.
EDIT
This discussion mentions some of the points I have included above and a more detailed explanation on some other interesting points.
Related Topics
How to Set Default Ruby Version with Rvm
How to Read the Content of an Excel Spreadsheet Using Ruby
How to Use Hash Keys as Methods on a Class
Irb History Not Working with Ruby 2.3.0
Why Do I Get a Bcrypt-Ruby Gem Install Error
Rails Hidden Field Undefined Method 'Merge' Error
Activerecord - Querying Polymorphic Associations
Ruby: How to Convert a String to Boolean
Why Does Array.Slice Behave Differently for (Length, N)
When Would a Ruby Flip-Flop Be Useful
Keep Form Fields Filled After an Error (Ror)