attr_accessor strongly typed Ruby on Rails

ekynox picture ekynox · Nov 3, 2011 · Viewed 8.5k times · Source

Just wondering if anyone can shed some light on the basics of getter setters in Ruby on Rails with a view on strongly typed. I am very new to ruby on rails and predominately have a good understanding of .NET.

For example, let's consider we have a .net class called Person

class Person
{
 public string Firstname{get;set;}
 public string Lastname{get;set;}
 public Address HomeAddress{get;set;}
}

class Address
{
 public string AddressLine1{get;set;}
 public string City{get;set;}
 public string Country{get;set;}
}

In Ruby, I would write this as

class Person
 attr_accessor :FirstName
 attr_accessor :LastName
 attr_accessor :HomeAddress
end

class Address
 attr_accessor :AddressLine1
 attr_accessor :City
 attr_accessor :Country
end

Looking at the Ruby version of the Person class how do I specify the types for the accessor methods FirstName, LastName and HomeAddress? If I were to consume this class I could feed any type into HomeAddress but I want this accessor method to accept only the TYPE Address.

Any suggestions?

Answer

Jakub Arnold picture Jakub Arnold · Nov 3, 2011

TL;DR: No it's not possible ... and long answer, yes it is possible, read the metaprogramming section :)

Ruby is a dynamic language, that's why you won't get compile time type warnings/errors as you get in languages like C#.

Same as you can't specify a type for a variable, you can't specify a type for attr_accessor.

This might sound stupid to you coming from .NET, but in the Ruby community, people kind of expect you to write tests. If you do so, these types of problems will basically vanish. In Ruby on Rails, you should test your models. If you do so, you won't really have any trouble with accidentaly assigning something somewhere wrong.

If you're talking about ActiveRecord in Ruby on Rails specifically, assigning a String into an attribute which is defined as an Integer in the database will result in exception being thrown.

By the way, according to convention, you shouldn't use CamelCase for attributes, so the correct class definition should be

class Person
 attr_accessor :first_name
 attr_accessor :last_name
 attr_accessor :home_address
end

class Address
 attr_accessor :address_line1
 attr_accessor :city
 attr_accessor :country
end

One reason for this is that if you Capitalize the first letter, Ruby will define a constant instead of a variable.

number = 1   # regular variable
Pi = 3.14159 # constant ... changing will result in a warning, not an error

Metaprogramming hacks

By the way, Ruby also has insanely huge metaprogramming capabilities. You could write your own attr_accessor with a type check, that could be used something like

typesafe_accessor :price, Integer

with definition something like

class Foo

  # 'static', or better said 'class' method ...
  def self.typesafe_accessor(name, type)

    # here we dynamically define accessor methods
    define_method(name) do
      # unfortunately you have to add the @ here, so string interpolation comes to help
      instance_variable_get("@#{name}")
    end

    define_method("#{name}=") do |value|
      # simply check a type and raise an exception if it's not what we want
      # since this type of Ruby block is a closure, we don't have to store the 
      # 'type' variable, it will 'remember' it's value 
      if value.is_a? type
        instance_variable_set("@#{name}", value)
      else
        raise ArgumentError.new("Invalid Type")
      end
    end
  end

  # Yes we're actually calling a method here, because class definitions
  # aren't different from a 'running' code. The only difference is that
  # the code inside a class definition is executed in the context of the class object,
  # which means if we were to call 'self' here, it would return Foo
  typesafe_accessor :foo, Integer

end

f = Foo.new
f.foo = 1
f.foo = "bar" # KaboOoOoOoM an exception thrown here!

or at least something along these lines :) This code works! Ruby allows you to define methods on the fly, which is how attr_accessor works.

Also blocks are almost always closures, which means I can do the if value.is_a? type without passing it as a parameter.

It's too complicated to explain here when this is true and when it's not. In short, there are different types of blocks

  • Proc, which is created by Proc.new
  • lambda, which is created by the keyword lambda

one of the differences is that calling return in a lambda will only return from the lambda itself, but when you do the same thing from a Proc, the whole method around the block will return, which is used when iterating, e.g.

def find(array, something)
  array.each do |item| 
    # return will return from the whole 'find()' function
    # we're also comparing 'item' to 'something', because the block passed
    # to the each method is also a closure
    return item if item == something
  end
  return nil # not necessary, but makes it more readable for explanation purposes
end    

If you're into this kind of stuff, I recommend you check out PragProg Ruby Metaprogramming screencast.