RubyGreenBlue

What are Ruby Symbols

Posted by keith 5 months ago

One of the hardest things to explain about Ruby (especially to newcomers) is what symbols are. Many explanations of symbols that I see or hear often only cover a low-level/technical difference between symbols and strings. That is, that symbols use one memory location for each unique instance whereas strings use separate memory locations for each instance. Also that symbols are kept around in memory for the life of the program while strings get cleaned up when they are no longer used.

While these differences are accurate, they are just implementation details and not really what Ruby symbols are all about and those who quote only these differences by way of explanation of what Ruby symbols are, are missing the point.

What Ruby symbols are really about is beauty and semantics. Ruby symbols give the programmer the distinction between identity and information.

Consider this method call:

validates_presence_of :firstname, :lastname, :on => :create, :message => "can't be blank"

and if we single out

:message => "can't be blank"

:message is a symbol and "can't be blank" is a string. We could have easily written this statement like:

"message" => "can't be blank"

But the difference here is that :message is just a label for something, some concept, that a part of our program needs to work with. Whereas "can't be blank" is actual data or information. Another way to look at it is that we could mess with one of the two and the program would not care but if we mess with the other and the program does care.

If I change it to:

:my_message => "can't be blank"

The program no longer works as it did before. The method does not look for anything labeled :my_message. It is not a concept it knows about. The label :message is there only so the programmer and the method can talk about the same concept.

Now if I change it to:

:message => "can't be blank, sorry about that."

or

:message => "darf nicht leer sein"

The program still works as it did before, it just displays a different message. The "can't be blank, sorry about that." is clearly different to :message. It's not a label or an identity at all, it's information.

Many other languages don't have this distinction between information and identity, or at least not in such a nice, convenient way. As a result, strings are often used to perform the task of labeling.

$car = array ("weight"=>"1000", "unit_of_measure"=>"kg", "year"=>"2004", "price"=>"7000");

If the program knows about the concept of weight and unit_of_measure, using strings for those labels doesn't quite feel right.

car = {:weight => '1000', :unit_of_measure => 'kg', :year => '2004', :price => '7000'}

Now we have a separation between the concepts our program deals with and the associated data. And with syntax highlighting it reads so much nicer.

Take this example again

validates_presence_of :firstname, :lastname, :on => :create, :message => "can't be blank"

It reads so much nicer and is easier to see which bits, if changed, will affect the semantics of the program and which bits won't, but if we use strings for our labels, we lose that distinction.

validates_presence_of "firstname", "lastname", "on" => "create", "message" => "can't be blank"

An easy way to work out if you should use a symbol or a string is this: Does the program care about the thing I am typing, or does the user care about it?

  • if the program cares:

    :symbol
    
  • if the user cares:

    "string"
    

Finding Descendants of a Class

Posted by keith about 1 year ago

Ruby offers a method for finding all the ancestors of a class (ancestors funnily enough) but, as far as I know, no easy way to get at all the descendants of a class.

I thought it had to be possible, this *is* Ruby after all! But in thinking about it a little more, I figured it could well be a tricky thing to do and probably require some hook in the Ruby language for it all to be even possible.

Luckily I found this beauty: ObjectSpace. With ObjectSpace you can eacherate over every object in the Ruby object space. What else?

So it tuns out that finding all the descendants of a particular class is not such a big deal after all. Just eacherate over ObjectSpace and do a little recursion:

class Object

  def self.show_descendants(klass = nil, indent_level = 1)
    unless klass
      klass = self
      puts ' - ' * (indent_level) + klass.to_s
    end
    ObjectSpace.each_object do |obj|
      if obj.class == Class and obj.superclass == klass
        puts ' - ' * (indent_level + 1) + obj.to_s
        show_descendants(obj, indent_level + 1)
      end
    end
  end

  def show_descendants(klass, indent_level = 1)
    klass.show_descendants(nil, indent_level)
  end

end


# you can pass the class as an argument
show_descendants(ScriptError)

# or you can send the method to the class
Numeric.show_descendants

This code could be modified a little to return a list of descendants, each containing their own list of descendants rather than putsing it all to the screen.


Enumerable Constants for Active Record Models

Posted by keith about 1 year ago

A problem that had often bothered me in the past is this:

You're modelling a concept in a database like, say, property listings and a property has an attribute of "type". The possible values of "type" are not arbitrary. They are some fixed set of values like "rental", "for sale" and "auction", and the software needs to do stuff depending on the value of "type".

So this is like a :belongs_to where a property belongs to a suburb say. But there is an important difference. With a belongs_to :suburb the values for suburb are arbitrary. That is to say that the software doesn't care about the values, only that there is some value (which the user cares about, of course). But the program does care about the set of values available for the "type" attribute.

Setting up a has_many/belongs_to relationship always seemed overkill but it gave me a nice list of available attributes and I could enforce the relationship. The other problem was that the data in the table would mirror some set of constants in the code. If you changed one, you had to make sure you changed the other or things got out of whack. Not to mention problems of populating a new database and making sure all such "non-arbitrary" value set tables had the correct values (including correct ids to match the constants in code) in them.

Making the attribute "type" a string and stuffing values "rental", "for sale" and "auction" in it felt particularly bad.

Solution

I felt the correct way to do it was to use integer values in the field and represent these values as constants in the code (which is where the values have meaning).

But writing validation for these and coming up with a list of human readable descriptions (for drop-downs) was tedious. It felt like the values belonged in the database... but wait, no, the values have meaning in the code...

So I've written a Rails plugin to take away the tediousness of it all and make it a lot more DRY. With it I get

  • an easy way to define, in a nice DSL looking way, what the constants are
  • validation code generated dynamically
  • a nice way of getting the list of values and descriptions (useful for UI stuff)

Examples

Model definition:

class Property < ActiveRecord::Base
  enumerable_constant :type, :constants => [:rental, :for_sale, :auction]
end

Some view code:

<p><label for="property_type">Property Type</label><br/>
< %= collection_select(:property, :type, Property::Type.constants, :id, :title, {:prompt => 'Select a Property Type'})  % ><br />

Note the use of Property::Type.constants in place of where you would normally use a collection object like @suburbs

Some code that tests the value

property = Property.find :first
if property.type == Property::Type::FOR_SALE
  # do some stuff
end

Other things you can access

>> Property::Type.constants.first.name
=> "RENTAL"
>> Property::Type.constants.first.title
=> "Rental"
>> Property::Type.constants.first.value
=> 1

Download the Enumerable Constants project


Rails Workshop - Sydney

Posted by keith about 1 year ago

Last weekend I attended a Rails Workshop in Sydney. The workshop was put together by wwworkshops and presented by Geoffrey Grosenbach of Top Funky Corp.

The workshop was great and I got a lot out of it. Geoffrey gave us a lot of material to wade thru and learn from including code (lots of code), screencasts and cheatsheets.

Geoff is a great presenter. Although Geoff knows a ridiculous amount of stuff, he always comes across as being very approachable willing to learn from those he's presenting to. I like his style.

It was also great to meet up with some talented people in the Sydney Rails community.


Convertible to csv format options

Posted by keith about 1 year ago

The latest version of convertible to csv can be told how to format particular fields when it returns data for the to_csv method. The default behaviour is to use the to_s method for each field. But sometimes you might want to display the data a little differently.

Use :format_options to specify a format method to be called when outputting a field. This is useful, for example, if you have a Time field and you want to format it differently to how Time.to_s displays a time.

Here's an example:

class Customer < ActiveRecord::Base
  acts_as_convertible_to_csv :header => true, 
                             :fields => %w(id firstname lastname email_address),
                             :format_options => {:lastname => :format_lastname, :firstname => :format_firstname}
  def format_firstname
    firstname.upcase
  end

  def format_lastname
    lastname.reverse
  end
end

As Customer.find(:all).to_csv is invoked, each time the fields "lastname" and "firstname" are output, instead of calling to_s on the fields, calls are made to the format_firstname and format_lastname methods respectively and your csv output will contain upcased firstname and reversed lastname.


Markdown on Rails

Posted by keith about 1 year ago

I have renamed the Markdown Template Handler project to Markdown on Rails. "Markdown Template Handler" was kinda dull and "Markdown on Rails" has a much better ring to it.

If you install the new renamed version you will need to change your config/environemt.rb file:

from this

require 'markdown_template'
ActionView::Base.register_template_handler('md', MarkdownTemplate)
ActionView::Base.register_template_handler('text', MarkdownTemplate)
MarkdownTemplate::map_headings_down_by 0

to

require 'markdown_on_rails'
ActionView::Base.register_template_handler('md', MarkdownOnRails)
ActionView::Base.register_template_handler('text', MarkdownOnRails)
MarkdownOnRails::map_headings_down_by 0

simple as that.


Convertible to Csv

Posted by keith about 1 year ago

Convertible to csv now has a project page.

I've also added some unit tests.

Convertible to csv allows you to specify that an ActiveRecord model should respond to a to_csv message. You can also specify if a header row of field names should be used and which fields (and their order) should be used in the csv output.


Markdown template handler for rails

Posted by keith about 1 year ago
Vote for this article on digg.com: Digg It!

Writing is all about content. Style and presentation is a separate concern. When I am writing, be it for the web or some documentation or whatever, I like to write pure text. Style and presentation comes later.

Markdown allows me to do that very effectively. I get to concentrate purely on the content of what I am writing and the semantics of the content (this is a heading, this is a list, this should be emphasised...)

For me, this desire to write plain text extends to writing content for web sites. Of course, html has it place but if I am writing a content page for a client's web site, I would much rather just write the content and not have the html markup of that content get in my way.

I solved this by writing a rails markdown template handler so I could write web site content in my rails apps in markdown.

These two images show a rails view (this site's about page) as html and markdown. In the markdown version, the content is free of the clutter of html tags.

The code is released under the MIT licence and can be found at the markdown_template_handler project home


acts_as_convertible_to_csv

Posted by keith over 2 years ago
Vote for this article on digg.com: Digg It!

update: convertible to csv now has a project page

A little ruby magic in the form of meta-programming allows you to define your models as being able to output data in csv format. The definition is a DSL style acts_as_convertible_to_csv with :header and :fields as valid specifications.

Here's how you would use it.

In your model declaration:

class Customer < ActiveRecord::Base
  acts_as_convertible_to_csv :header => true, 
                             :fields => %w(id firstname lastname email_address)
end

The first row will contain fields names if you specify :header => true. You can specify which fields and the order in which they appear by using :fields. If you omit :fields, the field list will default to those returned by the human_readable method.

Getting the csv data from a collection of records:

Customer.find(:all).to_csv

You can pass a block:

Customer.find(:all).to_csv do |line|
  # write the line to a file or something
end

You can also convert individual records to csv:

Customer.find(:first).to_csv

The code

class ActiveRecord::Base

  def self.acts_as_convertible_to_csv(*args)

    @@use_header_fields = false

    args.each do |param|
      if param.is_a? Hash
        if param.include? :header and param[:header]
          @@use_header_fields = true
        end

        if param.include? :fields and param[:fields] and param[:fields].is_a? Array
          class_eval <<-CEEND
            def self.csv_header
              %w(#{param[:fields].join(' ')})
            end
          CEEND
        else
          class_eval <<-CEEND
            def self.csv_header
              field_names = Array.new
              self.content_columns.each do |column|
                field_names << column.name
              end
              field_names
            end
          CEEND
        end
      end
    end

    class_eval <<-CEEND
      def self.use_header_fields
        @@use_header_fields
      end

      def to_csv
        line = ""
        self.class.csv_header.each do |field_name|
          line = line + "," + self.send(field_name).to_s
        end
        # remove first ', '
        line.gsub! /^,/, ''
      end
    CEEND
  end

end

# Allow a collection class to repond to to_csv
class Array

  def to_csv(&block)
    data = Array.new
    # check to see if the objects in the array are capable of to_csv    
    if self[0] and self[0].class.method_defined?(:to_csv)
      if self[0].class.use_header_fields
        data << self[0].class.csv_header.join(',')
        yield self[0].class.csv_header.join(',') if block_given?
      end
      each do |object|
        data << object.to_csv
        yield object.to_csv if block_given?
      end
    end
    data
  end

end