Stupid Things I Wrote in Ruby Last Week
Last Saturday, I mentored the Ladies Learning Code Introduction to Ruby.
Problem was, one week ago, I didn’t really know Ruby—at least not at the level where I could claim I’m an authority capable of teaching it to others. My experience with Ruby to that point had been so insignificant, I could enumerate just about everything I’ve ever written in Ruby:
- A Jekyll extension
- Simple code generators for my compiler class project
- A weird C/GraphViz state machine generator for a DNA… thing? I guess?
- And configuration files in at least two Ruby DSLs: Vagrantfile, Homebrew Formulae
Since Ruby is a dynamic, imperative, object-oriented programming language, I got by just fine through piecing together its syntax from pre-existing examples. I thought of Ruby as “that weird Smalltalk/Perl language with Lisp symbols” and it all kind of worked out in the end. After all, that combination is hella rad! But now, it’s about time for me to semi-seriously learn the language.
However, I’ve learned enough programming languages that going through in depth tutorials over the basic syntax of the language and its features is pretty dang boring. I did study such a tutorial, but it was to ensure I knew enough of the language to teach the material in the slides for the Ladies Learning Code workshop.
What can I say? I’m impatient! And I’ve always heard of the ease of metaprogramming and monkey-patching in Ruby. Now’s my time to live it! So I decided to dive into it head-first. After completing all of the exercises in the aforementioned absolute beginners slides, I hit Google and found resources on what makes Ruby different from the other programming languages I’m familiar with.
So I somehow found Jonan Scheffler’s articles on Weird Ruby, as well as this list of metaprogramming idioms, and went to town. Here’s a semi-chronological retrospective of the egregious things I wrote throughout my experience learning Ruby. Let’s start!
I don’t even know
I must prefix this by saying:
- This is not the way I’d ever solve this problem in a serious domain.
- I literally don’t even understand what compelled me to write this.
I could have just installed the proc-wait3
Ruby gem, but I decided to
take it in a bit of a different direction. Pouring over
ruby-doc.org, I found Kernel#syscall
and I just could
not resist, you guys! I just had to use it to call arbitrary system
calls on my machine! I just had to! So that’s exactly what this program
does. I don’t even know.
You may notice this invocation to xcrun
at the top. It’s meant to deal
with a some weird Apple things. At some OS X/Xcode update, Apple
decided the path to C things should differ from the traditional Unix
prefix of /usr/{lib,include}
to some branching thing… There probably
is good rationale behind displacing all of this standard C developer
stuff to some different directory, but I never really noticed when it
happened. I guess they want to have several different platform versions
you can link against on one machine, but their solution is leaves a bit
to be desired. Regardless! You can get the effective path to the current
platform using xcrun --show-sdk-path
, which is exactly what I did:
irb(main):001:0> `xcrun --show-sdk-path`.chomp
=> "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.10.sdk"
Why did I do this, you may ask? Well of course, to inspect the
contents of the C header called sys/syscall.h
. It’s a big ol’ list of
C defines that looks a little bit like this:
#define SYS_syscall 0
#define SYS_exit 1
#define SYS_fork 2
#define SYS_read 3
#define SYS_write 4
#define SYS_open 5
#define SYS_close 6
#define SYS_wait4 7
/* 8 old creat */
#define SYS_link 9
#define SYS_unlink 10
/* ... */
When your code needs to prod the kernel to do things, such as read from a file, send a signal to another process, or map more pages of memory (for memory allocation), it invokes an interrupt instruction to tag in the operating system kernel to do the nasty work. What specific operating system function actually gets used is determined by an integer, and this file lists all of those integers for my particular install of OS X.
And what did I do with this file? I grep’d it, and found the syscall for
sigsuspend(2)
. Yes. My file extracts the system call
constant from the C header file. And then it invokes it using
Kernel#syscall
. Because I could, and that is literally the only reason
why.
A fake database wrapper class
I then moved on to implementing control flow using exceptions—probably a bad idea, but kind of neat.
There are a few notable things here. There’s this semi-private
UpdateError
exception class that’s raised by _execute
to indicate
that the “statement” failed. This is caught by transaction
that
returns true or false, depending on whether a statement failed in the
transaction. The bang method transaction!
is the same, except it
raises a TransactionFailed
exception—and here I start to show my
fondness of Ruby naming conventions. I have an boolean attribute
@in_transaction
that can be accessed using the method
in_transaction?
. Allowing !
and ?
at the end of method names is
really nice, and is one of the reasons I like Ruby. As an aside, it’s
also my reason for my artificial preference of Scheme over Common Lisp.
Seriously though, Common Lisp. Are you listening? Why do you use p
to
indicate predicates when you could just use a ?
?
Er… that brings me to the next thing I programmed:
attr_boolean
And so begins my dangerous fascination with metaprogramming and monkey patching! I acknowledge that these two features of Ruby can be horribly, horribly abused… and I decided to take full advantage of it! Note to future collaborators: I promise not to use these techniques in production code. Much.
attr_boolean
is similar to attr_reader
except it adds a boolean
reader with a question mark at the end. That is, if you have an instance
variable @is_okay
, it creates a method called is_okay?
, and this
simply returns @is_okay
, which should presumably be true
or false
The dumb part here is I straight-up don’t know how to access instance variables by name. My Googlefu failed me here so I decided to take it in a bit of a different direction…
I used eval
. Never use eval
, ya’ll. It will probably go horribly
wrong. That said, it didn’t stop me from trying to do a little input
validation. I wanted a single Ruby identifier, since this is the only
that would make syntactic sense in my template code in the eval
. But
how would I know that I have a Ruby identifier? I could write a cheap
regular expression, but I knew there had to be a better way.
This led me to find out whether Ruby has standard libraries to parse itself. And it does! I used Ripper to lex the input and assert that it was a valid identifier. And hopefully this is enough to prevent arbitrary code execution!
glozell.rb
And here’s a demonstration of attr_boolean
in action:
There isn’t really anything technically notable about it. I just wanted to remind the world of GloZell Green’s Cereal Challenge.
That is all.
rubydoc_org
gem
At this point, most of my googling landed me on ruby-doc.org. I had
not learned how to use ri
at this point, and frankly, it’s broken on
my system due to… reasons. I also just wanted to program more Ruby, so
here we are.
The problem with my Google searches for Ruby documentation is that it
would return documentation for all different versions of Ruby. I only
wanted what was most relevant to my current version of Ruby—2.2.2. So
I decided to monkey-patch (of course) a method that would give me
documentation for any object I’m using. This comes from my habit of
using help(obj)
in Python.
I decided to turn this one into a full-blown gem, just to get my feet wet in writing gems and using bundler.
Unicode Literal
This came about because I realized I could. It allows me to write use
Unicode U+HHHH
notation for code points, and return a UTF-8 string:
Well, sort of. You see, you still need to write a valid hexadecimal
literal after it in 0xHHHH
notation. Because of this, I also decided
to throw in support for anything .to_s
-able, which provides nifty
notation for when the first hex digit is an alphabetic character.
irb(main):001:0> U+0x1F4A9
=> "💩"
irb(main):002:0> [U+:CA0].*(2).insert(1, '_').join
=> "ಠ_ಠ"
irb(main):003:0> U+'1F5FF'
=> "🗿"
Unfortunately, you can’t concatenate two literals in a row:
irb(main):004:0> U+0x1F51C + U+0x1F52A
TypeError: no implicit conversion of Module into String
from (pry):1:in `+'
Of course, you could always just use a Unicode escape in a string literal…
irb(main):005:0> "\u{0122D7}"
=> "𒋗"
…but that would be boring.
Numeric#to_utf8
I kind of glossed over my use of Array#pack
in the above example to
turn any integer to the UTF-8 string encoding its code point. We may
very well monkey-patch Integer
or even Numeric
with this method.
Here, I dump it all into its own module, and provide a method to
explicitly install it. This might be preferable than to simply go
ahead and install the monkey-patch on require
.
Blackjack
This is the largest one. The end project of the Ladies Learning Code workshop was writing a blackjack game. I, of course, would not just write it like a normal human being. I’d apply all sorts of unnecessary abstraction to the task an order of magnitude more time writing it than a sane person. But I did! What results is quite a hefty piece of code, but I have cool things like my use of Unicode playing card code points! I mean… someone’s gotta use ‘em, right?
(Feel free to skim over this one though…)
mean_girls
I’d like to end with a stupid thing someone else wrote.
Enter the mean_girls
gem. It’s only job is to do this:
irb(main):001:0> cool_people = {:regina_george => true, :cady_heron => true, :karen_smith => true }
=> {:regina_george=>true, :cady_heron=>true, :karen_smith=>true}
irb(main):002:0> cool_people.fetch :gretchen_wieners
KeyError: Stop trying to make fetch happen, :gretchen_wieners!
Though I’d personally change its implementation to this:
class Hash
def fetch(key, *args, &block)
raise KeyError.new "Stop try to make fetch happen! It's not going to happen!"
end
end
I’d recommend installing my improved version of this gem, and replacing
the default ruby
on your path with the following shell script:
#!/bin/sh
/usr/bin/env ruby-original -r mean_girls $@
Install it with the following shell script:
RUBY=`which ruby`
ORIGINAL_RUBY=`dirname $RUBY`/ruby-original
mv $RUBY $ORIGINAL_RUBY
cat > $RUBY << EOF
#!/bin/sh
/usr/bin/env ruby-original -r mean_girls $@
EOF
chmod +x $RUBY
And overall, enjoy breaking practically every Ruby codebase ever.