As someone who’s spent a lot of time working with JavaScript’s rather desolate built-in functionality, something I found quite nice in Ruby (or, more accurately, Rails) is ActiveSupport’s Numeric extensions. The 3.hours.ago
syntax ActiveSupport enables is more English-like than, for instance, Moment.js
1
moment().subtract(3, 'hours')
Unfortunately, this approach involves monkey patching the standard library, which I’ve never been a fan of as it opens you up to unexpected behaviour. Some people are also doing this in JavaScript, but never felt comfortable with the idea of tacking such things onto the built-in types.
After writing several Pull Requests adjusting the delay in a setTimeout
, I sought better way. Sure, I could just define a const
elsewhere with a nice descriptive name like REFRESH_INTERVAL_MILLISECONDS
, but that’s overly verbose and creates a disconnect between the use and the definition which makes it less desirable.
I’m always curious about new and different ways of using the programming languages at my disposal, so I started tinkering!
Approach #1: Messing with Tagged Template Literals
The first thing that came to mind was an ES6 feature I’d been fiddling with at the time: tagged template literals — basically, modern JavaScript’s answer to printf
.
When a template literal (the technical term for those new ES6-style string literals `surrounded by backticks`) is tagged with a function, the function is called with an array of string values and each interpolation as an additional argument, both derived from the template literal. From here, you can manipulate or react to each piece of the string, and return whatever you want! If you‘d like to know more about template literals, Max Stoiber has written a fantastic introduction.
Screenshot of a code example: "span`${10} seconds`.in.milliseconds"
Your number and time unit are included within the template literal. The tag function will then return an object whose properties contain your number in each time unit Moment.js supports.
Admittedly, this is not a very reasonable way to use template literal tags, but it worked.
Approach #2: Calling functions in context with Babel
Dissatisfied with both the overly verbose syntax and weird use of template literal tags, I went looking for more things you could do with ES6 and modern compilers.
It turned out that Babel, which we were already using to build Buildkite, includes some experimental language features. It was here I found an experimental shorthand function bind syntax.
Though not the intended use, it allows you to pass a value into a function from before the function name. Within the function, it’s available as the this
variable! Using the bind shorthand, I could then prepend a normal Number
object, have it passed into my functions, and avoid monkey patching entirely! 🙌🏼
Screenshot of a code example: “2::seconds.in(milliseconds)”
An expression in the format 2::seconds.in(milliseconds)
gets turned into seconds.in.call(2, milliseconds)
at compile time, making it nice and readable, but providing the browser with standard JavaScript code.
This was a fantastic hack, and I was sort of amazed that I could bend JavaScript to my will like this!
A few days later, when I was showing my little hack off to Glen Maddern, he pointed out it could go one step further further…
Approach #3: The birth of Metrick
The function bind shorthand can either call
your function if arguments are provided, or it can just bind
. These are both provided by the Function
prototype. In this case, I’m using call
on the in
function of whatever unit I’m converting from.
It turns out that Babel’s function bind syntax doesn’t actually care what the object you’re binding is. Because of this, you can provide your own bind
function, and include whatever functionality you want.
I added a bind
function which returns milliseconds, and suddenly…
Screenshot of a code example: “15::seconds”
…the syntax was even shorter, and even cleaner!
Metrick: literally one weird trick
It was a combination of approaches 2 and 3 that I ultimately published in my npm package, Metrick. I created a duration
module which supported times from milliseconds to years, support for data
, temperature
and length
units later on.
Metrick has classes for each type of unit. Metrick will only convert units between instances of the same class: you can’t convert an hour
into celsius
, as celsius
is an instance of Temperature
rather than Duration
.
To keep things consistent, each unit class extends a base Unit
class. This base class provides the actual functionality; it’s responsible for providing the in
and bind
functions, as well as performing the actual unit conversion.
The advantage of this structure is that it makes Metrick very flexible!
You can use the built-in units, or you can specify your own! You can even go so far as to create new types of unit if you need to operate on something niche.
Languages aren’t static
People are still trying to build on JavaScript’s strengths, to improve on its weaknesses, and to bring to it new ideas, and that’s really exciting.
Taking my frustration at magic numbers and applying that to discovering something neat has taught me that programming languages (in particular JavaScript) are more flexible than I’d thought.
I think it’s really worth exploring outside the norm, and I had a lot of fun making Metrick! Our tools and languages are often more flexible than we think.