Using Opal to Generate JavaScript

I just think Javascript is really icky. Icky like HP printer layout language or raw RTF. I don't know a lot about JavaScript, and I don't really want to. But the world is going bonkers over this gross little language and it's time to put an end to the myth that you must use it to do things in the browser.

I said I don't know a lot about it. Here is the sum of my knowledge and some badly scribbled notes here. I choose to embrace my ignorance of JavaScript and just keep it that way. I am a naiive but happy person. 1

So it's kind of built into me to hate Javascript a bit. JavaScript is also at the center of Polyglot programming which I both admire and despise. I can't deny I like the idea of a programming language in the web browser, but does it have to be Javascript? (and nine other languages thrown in?) Unfortunately, yes, it does. And yet it does not.

Thanks to the promising work of some people out there, as Ruby-lovers and Humanists, we all just may be able to not program in JavaScript as much as possible, and still have the good things in life. This idea grasps at the core of what Ruby is all about. Programmer happiness.

I name a couple of projects: WebRuby by Xuejie Xiao and Opal by Forrest Chang, and I'm sure there are others, but these are the two I've investigated.

The unique quality of these projects, is they run Ruby in the web browser. XueJie's work compiles an entire Ruby VM into a JavaScript library, making it accessible from the browser as a kind of resident machine. Forrest's work is a transliteration engine that recodes a very Ruby-like language into JavaScript - Check out his talk at Miami Ruby Conference 2013. He is not only an engaging speaker with a great slide deck, he has a mission he is passionate about. Coding should be fun, and as a community, Ruby people feel strongly about that.

Both of these projects are highly experimental and I've gotten both to work, with a considerable amount of elbow-grease. XueJie was very helpful in getting my copy of his proof of concept running. I had fewer problems with Forrest's project, but there was a lot of debugging that had to happen with my test program. Forrest has a number of people working on his project, so it was naturally able to handle a more challenging task.

The test program I ran under his project was a copy of something I wrote a few years ago when I was unemployed for almost two years straight. Although that situation remedied itself then unremedied itself again, the program still works. The program just takes some dates of employment and displays a report telling you your total experience both employed and unemployed. I've Perl-tested it, meaning it seems to work fine for me.

Here is the original copy that runs in the console with Ruby MRI:

require 'date';

class WorkHistoryAnalysis
   attr_accessor :total, :company, :latest

   def initialize()
      @total_days = 0
      @company = Hash.new
      @latest = Date.new
   end

   def workedFor(who, from, to)
      to_d = Date.new(to[0],to[1],to[2])
      from_d = Date.new(from[0],from[1],from[2])

      latest?(to_d)

      days = (to_d - from_d).to_i

      if (@company.has_key?(who))
         @company[who] += days
      else
         @company[who] = days
      end

      @total_days += days
   end

   def latest?(dat)
      if ((dat <=> @latest) > 0)
         @latest = dat
      else
         return false
      end
      return true
   end

   def employed
      years = days2yrs(@total_days)
      return ["Employed for:"],["   #{years} years [#{@total_days} total days]"]
   end

   def unemployed
      days = (Date.today - @latest).to_i
      years = days2yrs(days)
      return ["Currently unemployed for:"],["   #{years} years [#{days} days]"],["Last worked:"],["   #{@latest.to_s}"]
   end

   def days2yrs(days)
      years = days/365.to_f
      return (years * 100).round.to_f / 100
      ## / just to fix syntax highlighting
   end

   def list_experiences
      str = "Employment breakdown by company:\n"
      co = @company.sort_by { |k,v| v }
      co.each { |a| str += "   #{a[0]} #{days2yrs(a[1])} years [#{a[1]} days]\n" }
      return str      
   end

   def print
      puts employed
      puts unemployed
      puts list_experiences
   end
end


W = WorkHistoryAnalysis.new
W.workedFor("NWlink",[1996,2,1],[1997,7,1])
W.workedFor("Orrtax",[1997,8,1],[2000,1,1])
W.workedFor("Hostpro",[2000,2,1],[2001,11,1])
W.workedFor("College",[2002,4,1],[2005,12,16])
W.workedFor("Microsoft",[2006,1,1],[2006,5,1])
W.workedFor("Marchex",[2006,6,1],[2008,4,1])
W.workedFor("Microsoft",[2008,10,1],[2009,9,26])
W.workedFor("Microsoft",[2011,9,1],[2012,9,1])
W.print

Below is the slightly modified copy of the same script that ran correctly under Opal in my web browser:

require 'opal'
require 'jquery'
require 'opal-jquery'
require 'date'


class WorkHistoryAnalysis
   attr_accessor :total, :company, :latest

   def initialize()
      @total_days = 0
      @company = Hash.new
      @last = Date.new(0,0,0)
   end

   def workedFor(who, from, to)
      to_d = Date.new(to[0],to[1],to[2])
      from_d = Date.new(from[0],from[1],from[2])

      latest?(to_d)
      days = (to_d - from_d).to_i

      if (@company.has_key?(who))
         @company[who] += days
      else
         @company[who] = days
      end

      @total_days += days
   end

   def latest?(dat)
      if (dat > @last) 
        @last = dat 
      else 
        return false
      end
      return true
   end

   def employed
      years = days2yrs(@total_days)
      ret =  "Employed for:\n"
      ret += "   #{years} years [#{@total_days} total days]\n"
      ret += "\n"
   end

   def unemployed
      days = Date.today - @last
      years = days2yrs(days)
      ret = "Currently unemployed for:\n"
      ret += "   #{years} years [#{days} days]\n"
      ret += "\n"
      ret += "Last worked:\n"
      ret += "   #{@last.to_s}\n"
      ret += "\n"
   end

   def days2yrs(days)
      pre_years = (days/365 * 100).to_i
      years = pre_years / 100
      return years.to_s
   end

   def list_experiences
      str = "Employment breakdown by company:\n"
      co = @company.sort_by { |k,v| v }
      co.each { |a| str += "   #{a[0]} #{days2yrs(a[1])} years [#{a[1]} days]\n" }
      return str
   end

   def print
      return employed + unemployed + list_experiences
   end
end


Document.ready? do
   body = Element['body']

   W = WorkHistoryAnalysis.new

   W.workedFor("NWlink",[1996,2,1],[1997,7,1])
   W.workedFor("Orrtax",[1997,8,1],[2000,1,1])
   W.workedFor("Hostpro",[2000,2,1],[2001,11,1])
   W.workedFor("College",[2002,4,1],[2005,12,16])
   W.workedFor("Microsoft",[2006,1,1],[2006,5,1])
   W.workedFor("Marchex",[2006,6,1],[2008,4,1])
   W.workedFor("Microsoft",[2008,10,1],[2009,9,26])
   W.workedFor("Microsoft",[2011,9,1],[2012,9,1])

   body.append("<pre>#{W.print}<b>")
end

There are a number of minor changes to this script with regard to math and comparison functions. Opal seemed not to like the spaceship operator <=> very much, so I had to simplify it. The major changes are at the top and bottom of the script and to the naiively-written print function. The top of the script contains require for things that must be in-order, with opal being the first module listed.

The bottom section is wrapped by functions offered by the opal-jquery library, which essentially makes JQuery accessible to your Ruby script. Note that all JQuery selectors are funneled through Element objects in Opal.

The goal of this experiment was to get this code to run in my web browser using Opal. First thing, get on IRC into the #opal channel. They can help. It was here I learned that the RVM installation option for Opal was grossly out of date. I discovered myself it had some permissions issues on installation, so I do not recommend installing it. RVM is in a state of flux right now, so this will get fixed at some point. But at the moment, just use the one at Opal's main website: http://opalrb.org/.

Basically, I followed instructions there on how to build a static app that outputs to the JavaScript dev console of Chrome, then when I got that running satisfactorily, I made some modifications to the project to make it run as an Opal JQuery app in the browser proper.

Hopefully, I can give a little auxiliary guidance in this post on how to get to that second stage directly. The full source for this experiement is on GitHub.

First, we will assume you have installed Ruby on your Linux system and created a project directory. We will also assume you have a small console app to test, something that is entirely self-contained for I/O and doesn't need to access any files or take any user input to run.

NOTE: Before any stuff happens, you will need to install node.js and npm (the JavaScript package manager) from your Linux distro's package installation tool. These are needed by the cluster-muck of JavaScript frameworks to get the good things in life (and this project) running.

So, once you have the system dependencies installed, the fist (real) step you need to take from your project directory, is to make a subdirectory called app, and place your script in there. (Use my script if you are not wanting to debug for the next few hours.) Rename it to be application.rb. You will also need to download a copy of JQuery (just a single file) and place it into the app directory as well, renaming it to jquery.js. We deal with fixed names here because we are clueless and it makes setup easier by conforming to the instructions.

Next, you will want to create an HTML file in the toplevel project directory for this experiment called simple.html with the following contents:

<DOCTYPE html>
<html>
  <head>
  </head>
  <body>
    <script src="build.js"></script>
  </body>
</html>

It references a single Javascript called build.js which we will compile with Opal momentarily. The deal here is the Ruby you wrote will be cross-compiled into this file, which in turn runs the JavaScript representation of your Ruby code in the web browser. Pretty awesome stuff when you think about it.

Now onto the next step. Create a file in the toplevel called Gemfile and make sure it has the follwing contents:

source 'https://rubygems.org'

gem "opal", ">= 0.4.3"
gem "opal-sprockets"
gem "opal-jquery"

The source entry is somehwat optional, depending on how your Ruby installation is configured. I needed to insert that line for it to work. The Gemfile is standard equipment on all Ruby projects. It makes it possible to install dependent gems for the project, and we will be doing that in a minute.

Next, create a file called Rakefile in your toplevel directory with the following contents:

# Rakefile

require 'opal'
require 'opal-sprockets'
require 'opal-jquery'

desc "Build our app to build.js"
task :build do
  env = Opal::Environment.new
  env.append_path "app"

  File.open("build.js", "w+") do |out|
    out << env["application"].to_s
  end
end

The Rakefile is the Ruby version of a makefile, normally used under Ruby on Rails to do various tasks, but in our case it is being used to cross-compile the Ruby script into JavaScript.

OK, now you have all the startup files in place.

Now for the first point of action. We need to obtain all the gems listed in the Gemfile. That's easy. Not as root, but as yourself, just run > bundle install. The gems listed on your screen should get downloaded and installed. Bundle is a kind of execution-doer for Ruby projects. 2

After the two or three gems get installed, we will run > rake build from the toplevel to do our first build. If everything goes well, nothing will show in your console after you run this command. But you will find a new file there called build.js. If you get a dump of errors, scroll back to the beginning of the error output for a clue on why the build didn't work. Otherwise, you are ready to see the result.

To do this, use this clever method of starting a quick web server in your project directory:

> ruby -run -e httpd . -p 3500

(credit to Justin Campbell)

This will run a quickie web server on your local machine at port 3500. You can get to it by opening a browser to http://localhost:3500 and it will show a listing of files in that directory for viewing. Just click on simple.html and voila!

I included this projects' Opal Ruby script in this web page. If you see text in the box below, Ruby ran in your web browser, and produced results from the script. It's not just been pasted in to show you what it looks like. No JavaScript had to be written to make this happen:

CLICK ME

Footnotes:


  1. I always objected to javaScript because I grew up online using the {COMMO} terminal on BBS's. It was written by Fred Brucker (one of my personal programming heroes) and had a large following among people who used screen readers, mostly visually impaired folk. Javascript always seemed like an alienating force to me, becuase is never creates tangible HTML. I don't know for sure, but I'm pretty certain it doesn't do a lot to enhance the user experience of sight-impaired users. Either way I think it's just icky.  

  2. You can make most Ruby setup stuff run properly with framworks and tools by running bundle exec <gizmo> instead of running the gizmo's task directly. But that particular command won't apply here. Just a good thing to know.