The best kittens, technology, and video games blog in the world.

Friday, June 01, 2007

Active Record for RLisp

What Do I Have To Do To Get A Drink Around Here by seasideshe from flickr (CC-BY)
Every language needs a web framework. RLisp is in lucky position as it can reuse major parts of Ruby on Rails. So far Brad Ediger created RLisp plugin for Ruby on Rails, but that's just part of the story. Today I'm going to talk about a different part - the Object-Relational Mapper library Active Record.

Real databases are complex, ugly, and take hundreds of megabytes or more, so as an example I used a toy one by Mike Wilson, which describes music albums.

The original Ruby version looks like this, and we want to recreate it in possibly the nicest RLisp we can.

require 'active_record'

ActiveRecord::Base.logger = Logger.new(STDERR)
ActiveRecord::Base.colorize_logging = false

ActiveRecord::Base.establish_connection(
:adapter => "sqlite3",
:dbfile => ":memory:"
)

ActiveRecord::Schema.define do
create_table :albums do |table|
table.column :title, :string
table.column :performer, :string
end

create_table :tracks do |table|
table.column :album_id, :integer
table.column :track_number, :integer
table.column :title, :string
end
end

class Album < album =" Album.create(:title"> 'Black and Blue', :performer => 'The Rolling Stones')
album.tracks.create(:track_number => 1, :title => 'Hot Stuff')
album.tracks.create(:track_number => 2, :title => 'Hand Of Fate')
album.tracks.create(:track_number => 3, :title => 'Cherry Oh Baby ')
album.tracks.create(:track_number => 4, :title => 'Memory Motel ')
album.tracks.create(:track_number => 5, :title => 'Hey Negrita')
album.tracks.create(:track_number => 6, :title => 'Fool To Cry')
album.tracks.create(:track_number => 7, :title => 'Crazy Mama')
album.tracks.create(:track_number => 8, :title => 'Melody (Inspiration By Billy Preston)')

album = Album.create(:title => 'Sticky Fingers', :performer => 'The Rolling Stones')
album.tracks.create(:track_number => 1, :title => 'Brown Sugar')
album.tracks.create(:track_number => 2, :title => 'Sway')
album.tracks.create(:track_number => 3, :title => 'Wild Horses')
album.tracks.create(:track_number => 4, :title => 'Can\'t You Hear Me Knocking')
album.tracks.create(:track_number => 5, :title => 'You Gotta Move')
album.tracks.create(:track_number => 6, :title => 'Bitch')
album.tracks.create(:track_number => 7, :title => 'I Got The Blues')
album.tracks.create(:track_number => 8, :title => 'Sister Morphine')
album.tracks.create(:track_number => 9, :title => 'Dead Flowers')
album.tracks.create(:track_number => 10, :title => 'Moonlight Mile')

puts Album.find(1).tracks.length
puts Album.find(2).tracks.length

puts Album.find_by_title('Sticky Fingers').title
puts Track.find_by_title('Fool To Cry').album_id

It's a very straightforward database schema with just two tables. There's some duplication in code, especially all the table.column, and album.tracks.create fragments, but overall it's a pretty nice code. Its direct translation to RLisp however leaves a lot to be desired.
(ruby-require "active_record")

[ActiveRecord::Base logger= [Logger new STDERR]]
[ActiveRecord::Base colorize_logging= false]

[ActiveRecord::Base establish_connection (hash adapter: "sqlite3" dbfile: ":memory:")]

[ActiveRecord::Schema define &(fn()
[self create_table 'albums &(fn (table)
[table column 'title 'string]
[table column 'performer 'string]
)]

[self create_table 'tracks &(fn (table)
[table column 'album_id 'integer]
[table column 'track_number 'integer]
[table column 'title 'string]
)]
)]

(let Album [Class new ActiveRecord::Base])
(class Album
[self has_many 'tracks]
)

(let Track [Class new ActiveRecord::Base])
(class Track
[self belongs_to 'album]
)

(let album [Album create (hash title: "Black and Blue" performer: "The Rolling Stones")])

[[album tracks] create (hash track_number: 1 title: "Hot Stuff")]
[[album tracks] create (hash track_number: 2 title: "Hand Of Fate")]
[[album tracks] create (hash track_number: 3 title: "Cherry Oh Baby ")]
[[album tracks] create (hash track_number: 4 title: "Memory Motel ")]
[[album tracks] create (hash track_number: 5 title: "Hey Negrita")]
[[album tracks] create (hash track_number: 6 title: "Fool To Cry")]
[[album tracks] create (hash track_number: 7 title: "Crazy Mama")]
[[album tracks] create (hash track_number: 8 title: "Melody (Inspiration By Billy Preston)")]

(let album [Album create (hash title: "Sticky Fingers" performer: "The Rolling Stones")])
[[album tracks] create (hash track_number: 1 title: "Brown Sugar")]
[[album tracks] create (hash track_number: 2 title: "Sway")]
[[album tracks] create (hash track_number: 3 title: "Wild Horses")]
[[album tracks] create (hash track_number: 4 title: "Can't You Hear Me Knocking")]
[[album tracks] create (hash track_number: 5 title: "You Gotta Move")]
[[album tracks] create (hash track_number: 6 title: "Bitch")]
[[album tracks] create (hash track_number: 7 title: "I Got The Blues")]
[[album tracks] create (hash track_number: 8 title: "Sister Morphine")]
[[album tracks] create (hash track_number: 9 title: "Dead Flowers")]
[[album tracks] create (hash track_number: 10 title: "Moonlight Mile")]

(print [[[Album find 1] tracks] length])
(print [[[Album find 2] tracks] length])

(print [[Album find_by_title "Sticky Fingers"] title])
(print [[Track find_by_title "Fool To Cry"] album_id])

We can make it a lot better. One feature which Ruby DSLs often take advantage of is Ruby syntax for keyword arguments - obj.foo(x, y, {:a => b, :c => d}) can be abbreviated to much better-looking obj.foo x, y, :a => b, :c => d. RLisp doesn't support such abbreviations directly, but it can do something similar with macros. Macro (cmd ...) defined below will support similar syntax - (cmd obj foo x y a: b c: d) expanding to [obj foo x y (hash a: b c: d)]. We can also provide (cmds obj (command one) (command two) ...) macro for common pattern of issuing multiple commands to the same objects, something like Smalltalk's ;.

(defmacro sendq (obj meth . args)
`(send ,obj ',meth ,@args))

(defmacro cmd options
(let hash_index -1)
[options each_with_index &(fn (x i)
(cond
(and [hash_index == -1] [x is_a? Symbol] [[x to_s] =~ /:\Z/]) (set! hash_index i)
(and [hash_index == -1] [x == '=>]) (set! hash_index [i - 1]))
)]
(if (== hash_index -1)
(sendq options)
(do
(let normal_args [options get [0 ... hash_index]])
(let hash_args [options get [hash_index .. -1]])
`(sendq ,@normal_args (hash ,@hash_args))))
)

(defmacro cmds (recv . args)
(let tmp (gensym))
(let args-expanded [args map &(fn (a)
`(cmd ,recv ,@a)
)])
`(do ,@args-expanded)
)

These macros were pretty generic. Now let's build a simple wrapper for Active Record.
(ruby-require "active_record")

(defmacro active-record-establish-connection options
`(cmd ActiveRecord::Base establish_connection ,@options))

(defmacro active-record-schema-define defs
(let expanded-defs [defs map & (fn (d) (match d
('create-table . args) `(schema-define-create-table ,@args)
(raise "Unrecognized statement in schema definition: #{d}")
))])
`[ActiveRecord::Schema define &(fn() ,@expanded-defs)]
)

(defmacro schema-define-create-table (table-name . entries)
(let tmp (gensym))
(let expanded-entries [entries map &(fn (entry)
(let name [entry get 0])
(let type [entry get 1])
`[,tmp column ',name ',type])])
`[self create_table ',table-name &(fn (,tmp) ,@expanded-entries)]
)

(defmacro define-active-record-class (class-name . defs)
(let expanded-defs [defs map &(fn (d) (match d
(has_many . args) `[self has_many ,@args]
(has_one . args) `[self has_one ,@args]
(belongs_to . args) `[self belongs_to ,@args]
(has_and_belongs_to_many . args) `[self has_and_belongs_to_many ,@args]
_ d
))])
`(do
(let ,class-name [Class new ActiveRecord::Base])
(class ,class-name
,@expanded-defs
)
)
)

Finally Active Record can be used in a nice manner. I think it looks even nicer than Ruby version now.
(require "active_record.rl")

[ActiveRecord::Base logger= [Logger new STDERR]]
[ActiveRecord::Base colorize_logging= false]

(active-record-establish-connection adapter: "sqlite3" dbfile: ":memory:")

(active-record-schema-define
(create-table albums
(title string)
(performer string))
(create-table tracks
(album_id integer)
(track_number integer)
(title string))
)

(define-active-record-class Album
(has_many 'tracks)
)

(define-active-record-class Track
(belongs_to 'album)
)

(let album (cmd Album create title: "Black and Blue" performer: "The Rolling Stones"))

(cmds [album tracks]
(create track_number: 1 title: "Hot Stuff")
(create track_number: 2 title: "Hand Of Fate")
(create track_number: 3 title: "Cherry Oh Baby ")
(create track_number: 4 title: "Memory Motel ")
(create track_number: 5 title: "Hey Negrita")
(create track_number: 6 title: "Fool To Cry")
(create track_number: 7 title: "Crazy Mama")
(create track_number: 8 title: "Melody (Inspiration By Billy Preston)"))

(let album (cmd Album create title: "Sticky Fingers" performer: "The Rolling Stones"))
(cmds [album tracks]
(create track_number: 1 title: "Brown Sugar")
(create track_number: 2 title: "Sway")
(create track_number: 3 title: "Wild Horses")
(create track_number: 4 title: "Can't You Hear Me Knocking")
(create track_number: 5 title: "You Gotta Move")
(create track_number: 6 title: "Bitch")
(create track_number: 7 title: "I Got The Blues")
(create track_number: 8 title: "Sister Morphine")
(create track_number: 9 title: "Dead Flowers")
(create track_number: 10 title: "Moonlight Mile"))

(print [[[Album find 1] tracks] length])
(print [[[Album find 2] tracks] length])

(print [[Album find_by_title "Sticky Fingers"] title])
(print [[Track find_by_title "Fool To Cry"] album_id])

One more thing - as you can see from code examples (which hopefully weren't excessively mutilated by Blogger), the RLisp syntax highlighter got a lot smarter, and quasiquotes should be much easier to read.

No comments: