Saturday, June 15, 2024

Todo MVC Ruby Edition Is the One Todo MVC To Rule Them All!!!

Update 2024-06-29: I am sorry for missing the fact that the Hyperstack Todo MVC, written in Frontend Ruby using the Hyperstack framework, already made the accomplishment of being the one Todo MVC to rule them all (it was not listed on the Todo MVC website, which is why I missed it), albeit the project is out of date and has not had a release in 3 years (2021) as of the time of writing. But, I was still right about Todo MVC Ruby Edition being the One Todo MVC To Rule Them All! And, I suppose this Todo MVC takes its place given that the other one's project is not actively maintained.

Todo MVC is a popular sample that is built in all Web Frontend Frameworks to provide a comprehensive SPA (Single Page Application) example covering all basic Frontend features. As such, it provides Software Engineers with a very good idea of how day-to-day Frontend Development would be with every Frontend Framework, whether in JavaScript, using React, Angular, Ember, or Vue, or in a Compile-to-JavaScript language, using something like Elm, GWT, or Scala.js. I am happy to announce that Todo MVC has just been built using Glimmer DSL for Web with 100% Frontend Ruby code (included in Glimmer DSL for Web version 0.2.7). And, this version's code is easily simpler, more minimalistic, more readable, and more maintainable than all 45+ versions on the Todo MVC website! I checked all of them myself and every one of them fell in one of multiple ways, like by requiring developers to manage code in multiple languages, which adds mental friction and hinders productivity (for example JavaScript and some form of ugly XML/HTML/JSX), having over-engineered overly-complicated code that is not readable at a glance or is mostly imperative instead of being declarative, requiring developers to pollute the code or mix concerns with weird sometimes too-low-level concepts that do not map to any real world domain model concepts (like state hooks and effects, which make code maintainability a lot more complicated than it needs to be and often an absolute hell), or adding a compilation step to do premature optimization when in fact even plain JavaScript performance is good enough for most business apps' simple interaction use cases nowadays. I can confidently say Todo MVC Ruby Edition is the one Todo MVC to rule them all! Thanks to the awesomeness of Ruby in the Browser!!!

You can try Todo MVC Ruby Edition over here (keep in mind it is hosted on a slow inexpensive Backend, but once the page loads up, the Frontend is fast) and find out how fast Ruby code is in the Frontend (hint: fast enough!):

https://sample-glimmer-dsl-web-rails7-app-black-sound-6793.fly.dev/


One of the most incredible things about the Frontend Ruby version of Todo MVC is that it is the only version that allows writing all code in Ruby instead of being forced to split mental resources between HTML, CSS, and JavaScript (while still having the option to manage CSS in CSS/SCSS files if preferred). That has the huge advantage of making the code a lot more readable and programmable in the minimalistic Ruby syntax than every other Todo MVC version as there is no need to muck with ugly XML/HTML/JSX syntax anymore (no more redundant repetitions of every element's name to open and close). Instead, Glimmer DSL for Web provides a Ruby HTML DSL for the HTML part, a Ruby CSS DSL for the CSS part (although it is also possible to embed basic CSS into components, but I chose to use the CSS DSL to demonstrate it even if CSS programmability is not needed for this sample), and just plain Ruby code for the JavaScript part. And, the code flows very nicely from structure to logic to styling in one language (no need to add `<% %>` or `{ }` anymore) with excellent Object Oriented Design and separation of concerns, thanks to the MVP Architectural Pattern (Model-View-Presenter is a variation on MVC). That pattern includes clean Presenter and Model layers that offload presentation and business domain concerns from the View layer, making the View layer very slim and minimalistic, meaning Glimmer Web Components can get as small as 1/10 the size of React Components. Colocating CSS styling with each component's HTML structure code enables higher productivity and better style maintainability with a similar approach to that of React Styled Components, albeit in much more readable Ruby code. Also, the Fronend-Ruby-to-JavaScript-transpiler that is used by Glimmer DSL for Web supports Ruby 3.1 features (like the Shorthand Hash Syntax), which is totally awesome! This provides the added advantage of being able to reuse Backend Ruby logic in the Frontend as is if needed (which is not possible in any JavaScript framework).  

I generated most of the Ruby HTML DSL and CSS DSL code automatically from the HTML/CSS of other Todo MVC versions by using the HTML to Glimmer Converter and CSS to Glimmer Converter. This facilitates rewriting any existing SPA (Single Page Application) in Ruby very quickly without having to do much manual work. But, of course, after the code has been converted into Ruby code, it becomes programmable (e.g. with `if/else` statements and `.each` iterators) and can leverage event listeners (e.g. `onclick`) and advanced data-binding features like Bidirectional/Unidirectional/Content Data-Binding, which are super-cool, unique, and expressive in Ruby given its support for operator overload of `<=>` (for Bidirectional Data-Binding) and `<=` (for Unidirectional Data-Binding). 

Check out the Todo MVC Ruby Edition code below, starting with the View layer, moving into the Presenter layer, and ending with the Model layer; and then read the conclusion of the blog post afterwards.

Update: the code has been improved and simplified significantly in newer versions of Glimmer DSL for Web. Read the follow-up blog post "Todo MVC Ruby Edition w/ Component Style Blocks, Inline-Style Data-Binding, and Class-Inclusion Data-Binding" for more details. Click these links to see the updated Todo MVC entry file and models/presenters/views.

Todo MVC Code

# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc.rb
require 'glimmer-dsl-web'
require_relative 'todo_mvc/presenters/todo_presenter'
require_relative 'todo_mvc/views/new_todo_form'
require_relative 'todo_mvc/views/todo_list'
require_relative 'todo_mvc/views/todo_filters'
require_relative 'todo_mvc/views/todo_mvc_footer'
class TodoMvc
include Glimmer::Web::Component
before_render do
@presenter = TodoPresenter.new
end
after_render do
@presenter.setup_filter_routes
end
markup {
div(class: 'todomvc') {
section(class: 'todoapp') {
new_todo_form(presenter: @presenter)
todo_list(presenter: @presenter)
todo_filters(presenter: @presenter)
style {
todo_mvc_styles
}
}
todo_mvc_footer
on_remove do
@presenter.unsetup_filter_routes
end
}
}
def todo_mvc_styles
rule('body, button, html') {
margin '0'
padding '0'
}
rule('button') {
_webkit_font_smoothing 'antialiased'
_webkit_appearance 'none'
appearance 'none'
background 'none'
border '0'
color 'inherit'
font_family 'inherit'
font_size '100%'
font_weight 'inherit'
vertical_align 'baseline'
}
rule('.todoapp') {
background '#fff'
margin '130px 0 40px 0'
position 'relative'
box_shadow '0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1)'
}
media('screen and (-webkit-min-device-pixel-ratio:0)') {
rule('body') {
font "14px 'Helvetica Neue', Helvetica, Arial, sans-serif"
line_height '1.4em'
background '#f5f5f5'
color '#111111'
min_width '230px'
max_width '550px'
margin '0 auto'
_webkit_font_smoothing 'antialiased'
font_weight '300'
}
}
end
end
Document.ready? do
TodoMvc.render
end

# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/new_todo_form.rb
require_relative 'new_todo_input'
class NewTodoForm
include Glimmer::Web::Component
option :presenter
markup {
header(class: 'header') {
h1('todos')
new_todo_input(presenter: presenter)
style {
rule('.header h1') {
color '#b83f45'
font_size '80px'
font_weight '200'
position 'absolute'
text_align 'center'
_webkit_text_rendering 'optimizeLegibility'
_moz_text_rendering 'optimizeLegibility'
text_rendering 'optimizeLegibility'
top '-140px'
width '100%'
}
}
}
}
end

# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/new_todo_input.rb
require_relative 'todo_input'
class NewTodoInput < TodoInput
option :presenter
markup {
input(class: todo_input_class, placeholder: "What needs to be done?", autofocus: "") {
# Data-bind `input` `value` property bidirectionally to `presenter.new_todo` `task` attribute
# meaning make any changes to the new todo task automatically update the input value
# and any changes to the input value by the user automatically update the new todo task value
value <=> [presenter.new_todo, :task]
onkeyup do |event|
presenter.create_todo if event.key == 'Enter' || event.keyCode == "\r"
end
style {
todo_input_styles
}
}
}
def todo_input_class
'new-todo'
end
def todo_input_styles
super
rule(".#{todo_input_class}") {
padding '16px 16px 16px 60px'
height '65px'
border 'none'
background 'rgba(0, 0, 0, 0.003)'
box_shadow 'inset 0 -2px 1px rgba(0,0,0,0.03)'
}
rule(".#{todo_input_class}::placeholder") {
font_style 'italic'
font_weight '400'
color 'rgba(0, 0, 0, 0.4)'
}
end
end

# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_input.rb
# Superclass for NewTodoInput and EditTodoInput with common styles
class TodoInput
include Glimmer::Web::Component
def todo_input_class
'todo-input'
end
def todo_input_styles
rule(".#{todo_input_class}") {
position 'relative'
margin '0'
width '100%'
font_size '24px'
font_family 'inherit'
font_weight 'inherit'
line_height '1.4em'
color 'inherit'
padding '6px'
border '1px solid #999'
box_shadow 'inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2)'
box_sizing 'border-box'
_webkit_font_smoothing 'antialiased'
}
rule(".#{todo_input_class}::selection") {
background 'red'
}
end
end

# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_list.rb
require_relative 'todo_list_item'
class TodoList
include Glimmer::Web::Component
option :presenter
markup {
main(class: 'main') {
# Data-bind main element style property unidirectionally to `Todo.all` value (`Array` of `Todo` objects)
# meaning make any changes to `Todo.all` automatically update the style of the main element
# on_read option is a converter that is invoked upon reading data from `Todo.all` to update the main style
# with the converted value (e.g. 'display: none;')
style <= [ Todo, :all,
on_read: ->(todos) { todos.empty? ? 'display: none;' : '' }
]
div(class: 'toggle-all-container') {
input(class: 'toggle-all', type: 'checkbox')
label('Mark all as complete', class: 'toggle-all-label', for: 'toggle-all') {
onclick do |event|
presenter.toggle_all_completed
end
}
}
ul(class: 'todo-list') {
# Content data-binding for ul automatically re-renders its content block every time `presenter` `todos` change.
content(presenter, :todos) {
presenter.todos.each do |todo|
todo_list_item(presenter:, todo:)
end
}
}
style {
todo_list_styles
}
}
}
def todo_list_styles
rule('.main') {
border_top '1px solid #e6e6e6'
position 'relative'
z_index '2'
}
rule('.toggle-all') {
border 'none'
bottom '100%'
height '1px'
opacity '0'
position 'absolute'
right '100%'
width '1px'
}
rule('.toggle-all+label') {
align_items 'center'
display 'flex'
font_size '0'
height '65px'
justify_content 'center'
left '0'
position 'absolute'
top '-65px'
width '45px'
}
rule('.toggle-all+label:before') {
color '#949494'
content '"❯"'
display 'inline-block'
font_size '22px'
padding '10px 27px'
_webkit_transform 'rotate(90deg)'
transform 'rotate(90deg)'
}
rule('.toggle-all:focus+label, .toggle:focus+label, :focus') {
box_shadow '0 0 2px 2px #cf7d7d'
outline '0'
}
rule('.todo-list') {
list_style 'none'
margin '0'
padding '0'
}
end
end

# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_list_item.rb
require_relative 'edit_todo_input'
class TodoListItem
include Glimmer::Web::Component
option :presenter
option :todo
markup {
li {
# It is possible to configure multiple data-bindings as done with class_name below
# Data-bind li class unidirectionally to `todo.completed` using `on_read` converter
# to convert `todo.completed` value and produce final value to update li class with
# using li_class_name method defined below.
class_name <= [ todo, :completed,
on_read: -> (completed) { li_class_name(todo) }
]
# Data-bind li class unidirectionally to `todo.editing` using `on_read` converter
# to convert `todo.editing` value and produce final value to update li class with
# using li_class_name method defined below.
class_name <= [ todo, :editing,
on_read: -> (editing) { li_class_name(todo) }
]
div(class: 'view') {
input(class: 'toggle', type: 'checkbox') {
# Data-bind `input` `checked` property bidirectionally to `todo` `completed` attribute
# meaning make any changes to the `todo` `completed` attribute value automatically update the `input` `checked` property
# and any changes to the `input` `checked` property by the user automatically update the `todo` `completed` attribute value.
# `after_write` hook is invoked after writing a new value to the model attribute (`todo` `completed` attribute)
checked <=> [ todo, :completed,
after_write: -> (_) { presenter.refresh_todos_with_filter if presenter.filter != :all }
]
}
label {
# Data-bind `label` inner HTML unidirectionally to `todo.task` (`String` value),
# meaning make changes to `todo` `task` attribute automatically update `label` inner HTML.
inner_html <= [todo, :task]
ondblclick do |event|
todo.start_editing
end
}
button(class: 'destroy') {
onclick do |event|
presenter.destroy(todo)
end
}
}
edit_todo_input(presenter:, todo:)
if todo == presenter.todos.first
style {
todo_list_item_styles
}
end
}
}
def li_class_name(todo)
classes = []
classes << 'completed' if todo.completed?
classes << 'editing' if todo.editing?
classes.join(' ')
end
def todo_list_item_styles
rule('.todo-list li.completed label') {
color '#949494'
text_decoration 'line-through'
}
rule('.todo-list li') {
border_bottom '1px solid #ededed'
font_size '24px'
position 'relative'
}
rule('.todo-list li .toggle') {
_webkit_appearance 'none'
appearance 'none'
border 'none'
bottom '0'
height 'auto'
margin 'auto 0'
opacity '0'
position 'absolute'
text_align 'center'
top '0'
width '40px'
}
rule('.todo-list li label') {
color '#484848'
display 'block'
font_weight '400'
line_height '1.2'
min_height '40px'
padding '15px 15px 15px 60px'
transition 'color .4s'
word_break 'break-all'
}
rule('.todo-list li .toggle+label') {
background_image 'url(data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E)'
background_position '0'
background_repeat 'no-repeat'
}
rule('.todo-list li.completed label') {
color '#949494'
text_decoration 'line-through'
}
rule('.todo-list li .toggle:checked+label') {
background_image 'url(data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E)'
}
rule('.todo-list li.editing') {
border_bottom 'none'
padding '0'
}
rule('.todo-list li.editing input[type=checkbox], .todo-list li.editing label') {
opacity '0'
}
rule('.todo-list li .destroy') {
bottom '0'
color '#949494'
display 'none'
font_size '30px'
height '40px'
margin 'auto 0'
position 'absolute'
right '10px'
top '0'
transition 'color .2s ease-out'
width '40px'
}
rule('.todo-list li:focus .destroy, .todo-list li:hover .destroy') {
display 'block'
}
rule('.todo-list li .destroy:focus, .todo-list li .destroy:hover') {
color '#c18585'
}
rule('.todo-list li .destroy:after') {
content '"×"'
display 'block'
height '100%'
line_height '1.1'
}
media ('screen and (-webkit-min-device-pixel-ratio: 0)') {
rule('.todo-list li .toggle, .toggle-all') {
background 'none'
}
rule('.todo-list li .toggle') {
height '40px'
}
}
end
end

# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/edit_todo_input.rb
require_relative 'todo_input'
class EditTodoInput < TodoInput
option :presenter
option :todo
markup {
input(class: todo_input_class) { |edit_input|
# Data-bind `input` `style` property unidirectionally to `todo` `editing` attribute,
# using `on_read` converter to convert `todo.editing` value and produce final value to update `input` `style` with,
# and using `after_read` hook to have `input` grab keyboard focus when editing todo.
style <= [ todo, :editing,
on_read: ->(editing) { editing ? '' : 'display: none;' },
after_read: ->(_) { edit_input.focus if todo.editing? }
]
# Data-bind `input` `value` property bidirectionally to `todo` `task` attribute
# meaning make any changes to the `todo` `task` attribute value automatically update the `input` `value` property
# and any changes to the `input` `value` property by the user automatically update the `todo` `task` attribute value.
value <=> [todo, :task]
onkeyup do |event|
if event.key == 'Enter' || event.keyCode == "\r"
todo.save_editing
presenter.destroy(todo) if todo.task.strip.empty?
elsif event.key == 'Escape' || event.keyCode == 27
todo.cancel_editing
end
end
onblur do |event|
todo.save_editing
end
style {
todo_input_styles
}
}
}
def todo_input_class
'edit-todo'
end
def todo_input_styles
super
rule("*:has(> .#{todo_input_class})") {
position 'relative'
}
rule(".#{todo_input_class}") {
position 'absolute'
display 'block'
width 'calc(100% - 43px)'
padding '12px 16px'
margin '0 0 0 43px'
top '0'
}
end
end


# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/presenters/todo_presenter.rb
require 'glimmer/data_binding/observer'
require_relative '../models/todo'
class TodoPresenter
FILTER_ROUTE_REGEXP = /\#\/([^\/]*)$/
attr_accessor :todos, :can_clear_completed, :active_todo_count
attr_reader :new_todo, :filter
def initialize
@todos = Todo.all.clone
@new_todo = Todo.new(task: '')
@filter = :all
refresh_todo_stats
end
def refresh_todo_stats
refresh_can_clear_completed
refresh_active_todo_count
end
def create_todo
todo = new_todo.clone
Todo.all << todo # indirectly adds todo to todo list in the View through data-binding
new_todo.task = '' # indirectly clears new todo form in the View through data-binding
observe_todo_completion_to_update_todo_stats(todo)
refresh_todos_with_filter
refresh_todo_stats
end
def refresh_todos_with_filter
self.todos = Todo.send(filter).clone
end
def filter=(filter)
return if filter == @filter
@filter = filter
refresh_todos_with_filter
end
def destroy(todo)
delete(todo)
refresh_todos_with_filter
refresh_todo_stats
end
def clear_completed
Todo.completed.each { |todo| delete(todo) }
refresh_todos_with_filter
refresh_todo_stats
end
def toggle_all_completed
target_completed_value = Todo.active.any?
todos_to_update = target_completed_value ? Todo.active : Todo.completed
todos_to_update.each { |todo| todo.completed = target_completed_value }
end
def setup_filter_routes
@filter_router_function = -> (event) { apply_route_filter }
$$.addEventListener('popstate', &@filter_router_function)
apply_route_filter
end
def apply_route_filter
route_filter_match = $$.document.location.href.to_s.match(FILTER_ROUTE_REGEXP)
return if route_filter_match.nil?
route_filter = route_filter_match[1]
route_filter = 'all' if route_filter == ''
self.filter = route_filter
end
def unsetup_filter_routes
$$.removeEventListener('popstate', &@filter_router_function)
@filter_router_function = nil
end
private
def delete(todo)
Todo.all.delete(todo)
observer_registration = observers_for_todo_stats.delete(todo.object_id)
observer_registration&.deregister
end
def observers_for_todo_stats
@observers_for_todo_stats = {}
end
def observe_todo_completion_to_update_todo_stats(todo)
# save todo observer to deregister when destroying todo
observers_for_todo_stats[todo.object_id] = todo_stat_observer.observe(todo, :completed)
end
def todo_stat_observer
@todo_stat_observer ||= Glimmer::DataBinding::Observer.proc { refresh_todo_stats }
end
def refresh_can_clear_completed
self.can_clear_completed = Todo.completed.any?
end
def refresh_active_todo_count
self.active_todo_count = Todo.active.count
end
end

# Source: https://github.com/AndyObtiva/glimmer-dsl-web/blob/master/lib/glimmer-dsl-web/samples/regular/todo_mvc/models/todo.rb
Todo = Struct.new(:task, :completed, :editing, keyword_init: true) do
class << self
attr_writer :all
def all
@all ||= []
end
def active
all.select(&:active?)
end
def completed
all.select(&:completed?)
end
end
FILTERS = [:all, :active, :completed]
alias completed? completed
alias editing? editing
def active
!completed
end
alias active? active
def start_editing
return if editing?
@original_task = task
self.editing = true
end
def cancel_editing
return unless editing?
self.task = @original_task
self.editing = false
end
def save_editing
return unless editing?
self.editing = false
end
end
 

This version of Todo MVC is a very good start, but like most pieces of software, it does have room for improvement. For example, in the future, Glimmer DSL for Web could support a Frontend Routing API natively, it can avoid embedding style elements in body by having Glimmer automatically shove them in head, it can provide simpler syntax for data-binding CSS class inclusion and inline style properties, and more View responsibilities could be pushed down to the Presenter and Model layers by taking more advantage of the Observer Pattern. CSS Rules are verbosely created with the `rule` keyword, but they could have been created with just `r` or `_` instead if Software Engineers would rather avoid the repetition of the `rule` keyword. Additionally, if no programmability is needed (there is no logic in the style code that depends on component variables), then the CSS could be embedded as pure CSS too within the components. There are many other ideas for improvement, so if I update Todo MVC in the future, I will report on that in future blog posts.

Ruby in the Browser enables handling all kinds of interactions that are not possible in Hotwire without JavaScript, albeit with a simpler mental model. After all, it does not require that developers write ugly verbose HTML code (nobody should in 2024 just like nobody writes XML anymore) nor worry too much about element IDs while breaking MVC by making controllers push data to Views (which causes bad coupling that degrades maintainability). Instead, developers could just build smart Views that pull their own data automatically with Data-Binding observers just as per the proper MVC Pattern and they could work with variables/attributes in Ruby the normal way instead of relying on element IDs, simplifying maintainability immensely. A lot of developers avoid Hotwire and default to JavaScript Frameworks like React because Hotwire cannot do everything. They don't have to be stuck with inferior JavaScript technologies like React anymore if they avoid Hotwire. They can write their Frontend code in Ruby much more effectively (200%-10,000% more) in 2024! Meaning, 12 months of work in React take about 3-6 months in Ruby. That's a massive difference that will make and break companies depending on whether they stick with outdated inferior JavaScript technologies or upgrade to awesome superior Ruby technologies. This is the same exponential jump in productivity that happened in the mid-2000 with Rails, but now happening in the Web Frontend.

I think this is easily the future of productive Frontend Development in Rails for smart Ruby Software Engineers as it's not just a 10% or 20% improvement in productivity/maintainability over JavaScript options like React, yet an exponential improvement, meaning 200%, 400%, or even 10,000% better depending on the Frontend app being built. And, it removes the need for Rails shops to hire extra JavaScript developers. Backend Ruby Software Engineers can now do Frontend work in Ruby, thus enabling businesses to cut their hiring budgets in half and scale better (on top of cutting development cost and time-to-delivery in half by writing half the code of JavaScript in Ruby)!!! 

Happy Glimmering!

1 comment:

Paul Howson said...

Very impressive. Ruby has depth and subtlety that is showing itself in exercises like this.