It never ceases to amaze me how much work programmers create for themselves. Time and again I hop onto DuckDuckGo to search for a particular answer to a problem, or a tool to help me accomplish a task, only to find article after article and readme after readme riddled with overly-complicated, brute force, verbose solutions to what are ostensibly simple problems.
Question: how do I convert JSON to YAML?
Verbose Answer: you install these packages and then you download this script and then you modify these three variables and then you pray to the open source gods and then you sign up for this service and then you connect to that API and then you…
Real Answer: you don’t need to do anything…JSON is valid YAML. 😅
So here’s the deal: I hate work. 😜 I’d much rather go outside and take a walk. So whenever I’m trying to solve a big, thorny programming problem, what I’m not going to do is try a linear A, then B, then C, then D, then E approach to getting it to work. Because I know from experience that thinking in that manner actually creates far more work down the road. And I’m lazy, remember? So I want to get less work done in the long run. Way less work.
According to Wiktionary, “brute force” is a method of computation wherein the computer is let to try all permutations of a problem until one is found that provides a solution, in contrast to the implementation of a more intelligent algorithm. We can also apply that concept to our own “human computation” as we’re programming: just taking wild stabs in the dark to guess a solution to a problem, and upon the first working demonstration, well there you go!Problem solved.
That’s really not the ideal way to go about things at all.
When I’m in my “flow state” as a programmer, what I’m constantly doing is finding ways to eliminate redundancies. This goes far beyond DRY (Don’t Repeat Yourself), which is a generally useful concept but typically only thought of as applying to small code blocks. “Hey, these few lines here are basically the same as these few lines over there. Let’s extract them out to a single function! Cool, cool.”
I find that application of DRY to be far less compelling than one where you can recognize that entire subsystems of your application as a whole can be made entirely redundant if you simply took the time to search for higher-level abstractions.
Sometimes these higher-level abstractions are missed on codebases because multiple people are working in silos. Programmer A works on a feature over here. Programmer B works on a feature over there. At first glance neither feature seems related. But to a well-trained eye looking at the sales pitch for both features, it becomes apparent that most of the code could be conceptually shared between the two features. They’re really not two features at all. They’re one feature, expressed in slightly different ways depending on the context.
And that’s what I mean by eliminating redundancies in code. What if your application with 50 features could actually be built with only 25 features, each slightly more malleable at run-time to provide the illusion of 50 features? What if you could whittle that down even further? What if you could extract some of the features’ lower layers to a shared library? What if you could use someone else’s battle-hardened library instead?
Measuring your application codebase’s health by LoC is never wise. But I believe looking at the number of new lines of code added in each PR is very important. If PR after PR comes up for code review and nearly everything is “new code”—unless this is literally a brand-new application, something has gone terribly wrong. The majority of your PRs should be attempting to refactor, to streamline, to eliminate redundancies. Programmer works on Feature B, realizes it’s not that different from Feature A, so her PR for Feature B actually redoes Feature A so they both share as much in common—a higher-level abstraction.
Remember, the best code is the code nobody writes. “No code” is bug-free, infinitely fast, and incredibly easy to maintain. “No code” doesn’t need to be tested or understood because it doesn’t exist. The next best thing to “no code” is “worthy code”. The more verified impact each single line of code can have within your overall application architecture, the better.
But I Don’t Know What Other Code Has Already Been Written! #
One possible objection to this way of thinking might be that on sufficiently-large codebases, multiple programmers won’t have any idea what other people have already written or how those subsystems work exactly. So someone can be forgiven for pushing up a PR that’s basically just new code they’ve written to get something done.
I don’t subscribe to that philosophy.
Either (a) that programmer needs to spend more time simply reading code and learning how all of the different components and subsystems and configurations and layers of the application function and why they’re there, or (b) the code review process needs to incorporate an “architectural review” step to ensure PRs have taken possible refactoring into account, rather than just offering yet additional “append-only” code blocks.
That Sounds Like a Lot of Work! I Thought You Were Lazy?! #
A cursory examination of the concepts above might lead you to believe all this reading and refactoring and high-level architectural review of codebase concepts is a ton of extra work. Much easier to simply sit down at your code editor with a blank file, write some stuff, write some tests, verify the damn thing works, and call it a day.
Wrong!
That approach only works when you have a simple, greenfield application. As your codebase grows larger and more sophisticated, writing code in that manner eventually leads to “the big ball of mud” architecture, also known as “spaghetti code”. Unfortunately, I can’t begin to count how many tutorials I’ve seen (DEV is sadly riddled with them) which present code which is obviously spaghetti in nature, yet provide a sort of “copy-and-paste these 20 files into your project and you’re done!” appeal to newbies.
Listen, I understand that appeal. You don’t want to have to spend five hours crafting the perfect software architecture for your specific problem domain. You just want to download the “Gatsby-React-Tailwind-Firebase-Stripe-Netlify” starter kit, stuff a wad of JSON here and JSX there, copy-n-paste some random components off of StackOverflow, and boom you have a website.
Unfortunately the quality of that website’s code will be hot garbage. 😬 Might not matter to you now. But down the road, it’s going to bite you in the ass.
By taking the time to carefully, deliberately, intentionally write high-quality code from the start, understanding your problem domain while avoiding redundancies, expressing features in higher-level abstractions, utilizing battle-tested and well-crafted libraries/frameworks based on “first principles”, staying away from “new hotness” tools which make for cool tutorials but are a disaster to maintain over the long term—you begin to reap the benefits of DOEY. Soon, while other people are spending hours, days, even weeks wrestling with brittle codebases which are hard to intuitively understand and nearly impossible to refactor, you are enjoying your outside walk in the sun because you already shipped three new features yesterday.
All of that upfront work paid off. Now you get to be lazy. Stop to smell the roses. Life is good.
As a follow up to my recent podcast all about componentized view architecture, I thought it would be worthwhile to share some real-world code examples from various projects I’ve worked on so you can get a sense of what I’m talking about.
As you’ll soon discover, many of the Ruby view components I write tend to wrap around web components—either ones I’ve written or from third-party libraries. A web component is technically a custom HTML element, paired with some combination of JavaScript and optionally CSS which affects the styling and behavior of the element. For example, instead of writing HTML for a “badge” like this (example from Bootstrap):
You could write it like this (example from Shoelace):
<sl-badgevariant="warning">Warning</sl-badge>
The DX of web components tends to be much higher than CSS frameworks & utility-class-based libraries because the web component can provide an explicit API at both the HTML markup level and within JavaScript. For example, if you wanted to change the above badge’s variant from “warning” to “danger”, it’s as simple as:
Now let’s look at some real-world examples. On the Ruby side, some components use Bridgetown’s native component class, others use ViewComponent within a Rails app. For templates I generally use Serbea, but I’ll also provide some ERB translations. On the frontend side, you’ll see much use of Ruby2JS paired with Lit & Crystallized.
We’ll start out with something simple. At the time of this writing I’m designing a new website for Bridgetown, and I need to add notes here and there on various documentation pages. I decided to use Shoelace’s sl-alert element since that gets me pretty close to how I want the notes to look visually:
The Ruby component code is nice and concise. It accepts a type keyword argument which defaults to :primary, and an optional icon identifier. Otherwise the icon will be determined based on the note type.
As you can see, this sets up the sl-alert markup as well as sl-icon for displaying an icon in the note. The content variable is automatically provided by the component class which is the output of the block passed to the note, and we use Bridgetown’s markdownify helper to render Markdown content to HTML. Using the note component on a page couldn’t be easier:
<!-- Serbea -->{%@Notedo%}
#### Front matter variables are optional
If you want to use [Liquid tags and variables](/docs/variables/)
…etc.
{%end%}
<!-- ERB --><%=renderNote.newdo%>
#### Front matter variables are optional
If you want to use [Liquid tags and variables](/docs/variables/)
…etc.
<%end%>
And passing keyword arguments is just how you might expect:
I’m working on a Rails app where a “ratable” object needs to display a component where people can rate it from 1 to 5 stars. Since Shoelace offers a very nice stars component, we can wrap that in our own component with both Ruby and frontend aspects. The component actually serves two purposes: it can display a read-only average of all the ratings for the object, or it can display the current user’s own rating of the object (if any).
First, here’s how the component gets used within a Rails template (all examples in Serbea):
The value method returns the value which many have been passed to the component, otherwise it returns the average rating (if possible). Also, since currently the system only has one type of ratable object, the ratable_url is hardcoded, but that could easily be made more flexible later on.
Pretty straightforward—but what’s the deal with that rating-stars tag? That is the custom element which has also been written alongside the Ruby component/template. Let’s take a look at that now.
# app/components/rating_stars_element.js.rbimport[signed_in,initiate_sign_up],from: "../javascript/lib/utils.js.rb"classRatingStarsElement<LitElementcustom_element"rating-stars"defconnected_callback()set_timeout100doself.add_event_listener"sl-change"do|event|value=event.target.valuerate(value)endendendasyncdefrate(value)unlesssigned_in?()# grab the context and id out of the url for ratingcontext,context_id=@href[1..].split("/")returninitiate_sign_up(context.delete_suffix("s"),context_id)endresponse=awaitDaniel.post(@href,rating: value)returnToaster.raise("check2-circle","Thanks for your rating!")ifresponse.ok?alert"I'm sorry, there was a problem saving your rating. Please contact our support team."data=awaitresponse.text()console.errorresponse,dataenddefrender="<slot></slot>"end
There’s a lot going on here so I’ll break it down for you. Also, in case you’re still scratching your head wondering how a web component has been written using Ruby (that is, something very much like it), you can thank Ruby2JS. We can even use the latest Ruby 3 syntax! Awesome, isn’t it?
So here’s the rundown:
The top line is an import statement, which should seem familiar if you’ve worked with ES modules. We actually could have added this to Ruby2JS’ autoimports config and thus omitted this line (as we have for other imported modules like Daniel and Toaster).
The RatingStarsElement is a subclass of LitElement from the Lit library. Though in this example we’re not making any particular use of Lit-specific functionality (other than the implicit href property), it’s right there if and when it’s needed.
When the element is connected to the DOM, we wait a beat for Shoelace’s star rating component to initialize, then we add an event handler so we can run code when the user has clicked on the stars.
Inside the rate method, marked as async so we can use an await inside the method, we try to find out if there’s even a user currently signed in. If not, it hands things off to the user sign-up process. Otherwise, it POSTs to the supplied ratable URL with the new rating value, and displays a “toast notification” so the user knows their rating was accepted. If there was an error condition, we handle that too (and this area is certainly ripe for improvement).
Finally, the render method is used by Lit to determine the internal template to use within the web component, and since we don’t need to augment the Ruby template with anything, we just return a basic slot.
(FYI: if you’re wondering what Daniel is, it’s a simple wrapper around fetch I wrote, and it’s called Daniel because there’s a popular Ruby gem for making web requests called Faraday, and there’s a character in the TV show Lost named Daniel Faraday. Daniel. Faraday. Get it? 😋)
Now you may be wondering why I would even use a web component in this context, when it seems like Stimulus could do the job quite nicely. And many people working on a Rails app would probably assume you should use Stimulus for this sort of thing.
If that’s the flavor of ice cream you prefer, go for it! You can still employ patterns very similar to the one above. However, I personally have chosen to migrate away from Stimulus and only write web components. The reason for this is that I want to limit architectural complexity. After writing a wide variety of Stimulus controllers in the past, there were a number of cases where Stimulus just wasn’t cutting it, and I was able to write better and less buggy code by switching to Lit/web components technology. And at that point, if I’m writing both Stimulus controllers and Lit components in the same project, the question becomes: why? Why can’t I just use Lit alone?
So that’s the direction I’ve headed in. I find the conceptual 1:1 mapping between a Ruby component and a web component to be very easy to reason about. Plus, thanks to Crystallized—a small Lit add-on I wrote which provides a solid Stimulus-like actions/targets mechanism for “light DOM” markup—I really don’t miss Stimulus in the least. Let’s take a look at that next.
Here’s a component which provides a play/pause button for an audio clip. It also connects up with a site-wide, persistent audio player not covered herein. (I’ve simplified the example down a little from the shipping component for clarity.)
So how does soundclip-button work and what’s with all those soundclip-button-action and soundclip-button-target attributes? Let’s find out!
classSoundclipElement<LitElementself.properties={playing: {type: Boolean,reflect: true},# additional properties are auto-defined by Ruby2JS}self.targets={button: ".button",title_text: "@title",icon: "@",}custom_element"soundclip-button"definitializeDeclarativeActionsController.newselfTargetsController.newself@playing=false@order=0end# no shadow domdefcreate_render_root()=selfdefplay(event=nil)event&.prevent_default()player=document.query_selector("audio-player")if@playingplayer.stop()# player will then call the stop methodelseresume()player.playselfendend# Plays the next soundclip within the current boxdefplay_next()returnunless@order.present?self.closest(".box").query_selector("soundclip-button[order='#{@order+1}']")&.play()enddefstop()@playing=falseself.icon.class_list.replace"icon-music-pause-button","icon-music-play-button"enddefresume()@playing=trueself.icon.class_list.replace"icon-music-play-button","icon-music-pause-button"endend
I won’t go through every single line of code in detail here, but I want to highlight a few of the special aspects:
Ruby2JS does some clever parsing of LitElement subclasses and tries to turn any instance variables you use into element properties. Pretty cool stuff, but we also declare playing explicitly because we want the current play state of the component to be reflected back out to the playing attribute of the element in the DOM.
self.targets — this uses Crystallized’s targets config to set up methods which can be called to query various elements. So icon: "@" means that you can call self.icon and get the child DOM element marked with soundclip-button-target="icon", also button: ".button" means self.button will get any child element with a button class.
The lines DeclarativeActionsController.new self and TargetsController.new self instantiate the Lit controllers provided by Crystallized. The actions controller handles markup like you saw in the Ruby component template—e.g., a soundclip-button-action="play" attribute means when the link is clicked, the play method of the web component will get called automatically.
As DHH is often fond of saying, conceptual compression is a hallmark of Rails, and it’s a philosophy I very much subscribe to as well. I also like to collapse mental models. The fewer layers of “different stuff” living in parallel universes you have to boot up in your mind in order to accomplish simple tasks, the better.
What I love so much about the patterns above is that once you’ve wrapped your mind around what’s a Ruby component and what’s a web component, the two can operate as one conceptually-speaking across a wide variety of use cases…and by using Ruby2JS, you don’t even need to leave your beloved Ruby syntax behind. I find it fatiguing to have to context-switch constantly between Ruby and JavaScript when working on a singular feature. Now I don’t have to. Amazing! While knowledge of DOM APIs and some JavaScript methods is still required, the mental models are mostly collapsed. In a broad sense, you’re just writing Ruby objects and templates to build up discrete building blocks of user interface, and merely a small amount of effort is required to determine which is the code that executes server-side vs. client-side.
There are no full stack engineers?! Let’s talk about that. Also, just what is a componentized view architecture anyway? What are components? For that matter, what are templates? What are partials? I break it all down and explain why I’m gung-ho about view components. Plus I answer questions regarding Stimulus, nice_partials, and other Rails tooling from listeners like YOU! Enjoy, and keep on Ruby-ing!
Hey everybody, I’m so glad you could tune in for the debut episode of Fullstack Ruby. I’ve been on a few Ruby-themed podcasts over the past 18 months, but this is the first time I’m running a show about Ruby myself!
To kick things off, I’d like to introduce you to Ruby2JS and explain why I think this technology is a game changer.
Ruby2JS isn’t simply about an attempt to write what appears to be Ruby code for your website frontend. It’s really about writing JavaScript—AS IF JavaScript had Ruby’s syntax and was inspired by Ruby’s stdlib, ActiveSupport, and the like. A “RubyScript” if you will.
Three examples I cover on today’s episode:
set_timeout
tap & yield_self
implicit self method calls within a class definition
Visit the Ruby2JS website for live compilation demos, documentation on the various transformations and approaches available, and a whole lot more.
Welcome back to RUBY3.dev! Only…it’s not! Rather, a very warm welcome from Fullstack Ruby. Why the name change?
Well, a couple of reasons—the first of which is that your humble author (that’s me!) is not just a “Ruby developer” but a “web developer” as well. Yes, I’ll admit it: I don’t just write Ruby because I like assembling command line tools or crafting data processors or solving algorithmic puzzles. I like building websites. And I like building tools for building websites. I’m a web developer. It’s in my DNA.
So running a blog that’s generically about Ruby couldn’t hold my attention for too long. Thus I had to simultaneously narrow the focus all while expanding it to the broader web industry.
The second reason is that today, right now, right this very minute, is the absolute best time to be a fullstack Ruby/web developer. And tomorrow will be even better! Never have we had such a robust arsenal of tools at our disposal for building sites and apps that encompass both the backend and frontend in novel and exciting ways. Let us enumerate just what’s so great about the Ruby landscape at this juncture:
Turbo: in many ways a straightforward evolution of Turbolinks, Turbo—as a cornerstone of Hotwire (aka HTML-Over-the-Wire)—brings a new layer of interactivity to the frontend which leverages the backend templates and processes you already know and love. Instead of having to write two apps (a frontend app and a backend API), you just write one app, and Turbo provides the baseplate of “glue code” for composing your frontend out of backend “parts”. Whereas fullstack web development used to be primarily a “page-based” notion, it’s now fully modular. Turbo even works on static sites! Whoa.
StimulusReflex & CableReady/CableCar: StimulusReflex has taken the Rails world by storm as a launching pad for “reactive” programming which leverages WebSockets for fast two-way communications and broadcasts. It utilizes Stimulus (also part of Hotwire) as well as CableReady, a lower-level fullstack toolkit for generating and performing dynamic DOM operations. Of personal interest to me is CableCar, a feature currently in beta which lets you build and execute CableReady operations via any standard request/response. Paired with mrujs, a new swiss-army-knife library by Konnor Rogers, it makes advanced Ruby-based form handling a breeze.
Ruby2JS: what if I told you…you could write Ruby for the frontend, not just the backend? 🤯 That’s the promise of Ruby2JS. It’s not Opal—it doesn’t ship a veritable Ruby runtime to your browser. (Though Opal is very, very cool in its own right and in fact powers Ruby2JS’ pure-Node compiler implementation.) Rather, Ruby2JS allows you to write clean, modern ESM-flavored frontend code via a Ruby syntax and many Ruby idioms (enabled by configurable “filters”). And it now sports a sweet, sweet Lit component filter which I use heavily. To underscore just how real this is, I use Rubocop to lint all my Ruby2JS files. And the output? Looks 99% like concise, hand-written JavaScript with no compromises. Works with Webpack, Snowpack, Vite, and—soon—esbuild. Boom. 💥
Serbea: after literally decades of Ruby’s most popular template language, ERB, remaining entirely unchanged, Serbea is an exciting new take created by yours truly. It combines ERB’s power & flexibility with the expressiveness of handlebar-style languages like Nunjucks or Liquid, and it offers a native directive for rendering view components. I use it on all my projects these days—yes, even in Rails—and can’t imagine ever going back to plain ERB.
Bridgetown: sure, I’m extremely biased. What can I say? As lead maintainer of Bridgetown, I believe it’s the best platform upon which to build public-facing websites. By taking full advantage of the power of Ruby, and combining it with nearly all of the next-gen techniques enumerated above, you can create sites which start out as blogs, landing pages, portfolios, stores, educational resources, etc.—then grow into fullstack applications with authentication, paywalls, payment processing, headless CMS integrations with live previews, and more. We’re still in the alpha days of what I call the DREAMstack (Delightful Ruby Expressing APIs & Markup), but everything listed above is under active development. Come 2022, this dream will officially turn into reality.
So that’s the primary goal of the Fullstack Ruby blog going forward: to talk at length and in depth about all of the above futuristic technologies. And not just here on the blog, but on a new podcast as well entitled—shocker I know—Fullstack Ruby. 😅 Keep an eye out for the first teaser episode in early December.
So if that’s the primary goal, what’s the secondary goal? To help introduce backend-focused Rubyists to some of the exciting new browser developments they may not be familiar with. Advancements in CSS and JavaScript. New APIs. New client/server architectures. Something I’ve discovered in talking with various long-time Ruby developers is that some have thrown the baby out with the bathwater. By rightly eschewing the madness of JS frontend frameworks/tooling run amuck, they’ve also limited their knowledge of what is genuinely cutting-edge and useful on the frontend. For example, it’s fine if you opine “gee, heavy-duty React development seems like a PITA!” But if in the process you also ignore custom elements/shadow DOM, libraries like Lit, CSS variables, animations, and other techniques for building live, reactive frontend components, you’re cutting off your nose to spite your face. Not everything can fit cleanly into a Turbo/CableReady pipeline, or even a Stimulus controller. Sometimes, you just need to embrace “vanilla” JS & CSS. It’s OK. You can do it—and maintain your sanity! 😌
Finally, our third goal here at Fullstack Ruby is to introduce JavaScript developers to Ruby. We can shout all day from the rooftops how much we love Ruby and think it’s expressive and delightful—plus MINASWAN and all that—but if a JS dev who’s written some APIs in Node Express and assembled some pages with Next.js has no idea what we’re talking about or why—or how it’s relevant to their career—the #Ruby #WebDev community won’t grow. It’s as simple as that. So let’s take a moment out of our day to respectfully showcase to our fellow JS devs what is so appealing about Ruby, about the ecosystem, and about the community. Not in a spirit of competition, but in a spirit of collaboration. We’re ultimately all in the same boat: building great websites and applications. A polyglot web is a stronger web, a better web.
As a core member of the Bridgetown project, I realize I’m biased. I think every Rubyist who works on or even near the web should take a look—especially anyone who has current or past experience using Jekyll. But today’s post isn’t about Bridgetown per se but about how the next big release, v0.21 “Broughton Beach” (currently in beta and due out in late May), provides an intriguing new environment for teaching and learning Ruby and trying out new tools in the Ruby ecosystem.
One of the new features in Broughton Beach which is germane to this discussion is the ability to write web pages in pure Ruby. Previously, you could write a webpage in a template language such as Liquid, ERB, Haml, etc., similar to other Ruby frameworks like Rails.
Wait, I hear you say. Isn’t ERB just Ruby inside the<% %>delimiters?
Sure, it is. But you usually don’t see people writing an entire Ruby script in an ERB file. It’s mainly intended for first authoring the raw text of the template and then sprinkling bits of Ruby into it.
What’s changed in v0.21 is you can now add a page, or a layout, or a data file, using nothing more than .rb. Basically you can write any Ruby code you want, and the value returned at the end of the file becomes the content of the page. So you can build up web page markup using string concatenation, fancy DSLs, transformations of incoming data, the whole nine yards. And you can add methods and inner classes and anything else you need to accomplish your objective.
Feel free to fork the repo and take it for a spin! The only top-level files needed are the typical Gemfile/Gemfile.lock pair, and the bridgetown.config.yml file loaded by Bridgetown. Everything else goes in src. Let’s see what we have inside:
In _data/site_metadata.rb, we return a simple hash of key/value pairs we can access in any page via site.metadata. Only title is defined here but you can add any site-wide values you like.
In _layouts/default.rb, we define a simple HTML wrapper that can be used for any page on the site. First we obtain the logo SVG (aka ) we’ll use for our site-wide header. Next, we define a small stylesheet we’ll inject into the HTML head using a style tag. Then, we return the HTML itself using heredoc string interpolation to add in a few variables.
index.md is where things really get interesting. First we define a block of front matter using Ruby rather than YAML. (Note, we use the ###ruby … ### formatting in order for the front matter to get extracted and parsed separately at an earlier time than the rest of the page.) It references our default layout and sets the page title. Then in the body of the page, we create a few helper methods and values and begin constructing the page content using our home-grown DSL. Finally we return the @output of the page produced by our helper methods.
Is this the right way to build a Bridgetown site? 🤔 Well I certainly wouldn’t recommend shipping it to production! 😅 The point isn’t if you should use any of these techniques to build a website—rather that you can if you want to. (Just keep in mind that meme about scientists getting so preoccupied…)
Because you can, this becomes a compelling way to teach or to learn Ruby in the guise of building a website. Try out new techniques, new syntax, new parts of the standard library, new gems…the sky’s the limit! In the past, I might write one-off Ruby scripts and execute them on the command line, or maybe fiddle around in IRB. But now, with Bridgetown 0.21, I can actually maintain an experimental website full of pages which house various tips & tricks of Ruby programming I’ve picked up. Git init a repo, deploy it in mere minutes on Render, and we’re all set!
Want to get really fancy? Add the method_source gem to your project, and then inside a Ruby page you can grab a string representation of a proc or a method in the page and use that to output the source code to the webpage itself. Mind blown! 🤯
Another thing you can do (even if your pages use traditional ERB or another template language) is use the src/_data folder to drop .rb files that could load in data from filesystems or APIs (or generate data directly) and do all kinds of interesting things to it before returning either an array or a hash which is then accessible via site.your_data_file_here (tack on .rows if an array).
My goal in creating Bridgetown was always to consider the “Ruby-ness” of the tool a feature, not a bug. (By contrast it’s progenitor, Jekyll, strangely doesn’t overtly spell out that it’s a Ruby tool built by Rubyists for Rubyists).
I’m very excited to see what crazy, experimental projects people will build using this new version of Bridgetown. Feel free to hop on over to our Discord chat room and let us know!
As you sit down to write a new class in Ruby, you’re very likely going to be calling out to other objects (which in turn call out to other objects). Sometimes this is referred to as an object graph.
The outside objects created or required by a particular class in order for it to function broadly are called dependencies. There are various schools of thought around how best to define those dependencies. Let’s learn about the one I prefer to use the majority of the time. It takes advantage of three techniques Ruby provides for us: variable-like method calls, lazy instantiation, and memoization.
First of all, what do I mean by “variable-like method calls”? I mean that this:
thing.do_something(123)
could refer either to thing (a locally-scoped variable) or thing (a method of the current object). What’s groovy about this is when I instantiate thing, I can chose how to instantiate it. I could either set it up like this:
The beauty of the second example is it makes thing available from more than one method—all while using the same initialization values. The problem with this example however is if I access thing more than once, it will create a new object instance.
Oh no! The thing of the second line will be a different object than the thing of the first line! Yikes! Thankfully, we have a technique to fix that: “memoization via instance variable”.
Memoization is a technique used to cache the result of a potentially-expensive operation. In our particular case, we’re less concerned with performance-improving caching as we are with saving a unique value for reuse. We want the thing which gets used repeatedly to always refer to the same object. So let’s rewrite our thing method this way:
defthing@thing||=Thing.new(:abc)end
This code uses Ruby’s conditional assignment operator to either (a) return the value of the @thing instance variable, or (b) assign it and then return it. Now it’s assured we’ll never receive more than a single object instance of the Thing class. Let’s put it all together:
defsome_methodthing.do_something(123)# first call instantiates @thingthing.finalize!# second call uses the same @thingenddefthing@thing||=Thing.new(:abc)end
Let’s take a look at what we might do if we weren’t using the above technique and we needed thing available across multiple methods. We might use an approach like this:
classThingWranglerattr_reader:thing# create a read-only accessor methoddefinitialize@thing=Thing.new(:abc)# create @thing when this object is createdenddefsome_methodthing.do_something(123)thing.finalize!endend
Arguably this is an anti-pattern. Because if some_method never actually gets called, thing was instantiated for nothing—wasting memory and CPU resources. In addition, it makes swapping out the Thing class challenging in tests or subclasses because the Thing constant is hard-coded into the initialize method.
Some might recommend that you reach for the DI (Dependency Injection) pattern instead:
classThingWranglerattr_reader:thingdefinitialize(thing:)@thing=thingenddefsome_methodthing.do_something(123)# first call instantiates @thingthing.finalize!# second call uses the same @thingendend
Then you’d simply need to pass an initialized object to the new method of ThingWrangler from a higher-level:
Honestly, I really don’t like DI. It often makes for cumbersome APIs which are harder to comprehend as well as exposes implementation details to higher levels in situations where it might not even make sense. Do I really need to know that ThingWrangler doesn’t work without a Thing to rely on? Probably not. Contrast that with our friend the “lazily-instantiated memoized dependency” solution:
classThingWranglerdefinitialize(value)@important_value=value# we store useful data for future useenddefsome_methodthing.do_something(123)# first call instantiates @thingthing.finalize!# second call uses the same @thingenddefthing@thing||=Thing.new(@important_value)# aha! time to use saved dataendend# This level doesn't need to know about the Thing class!# It also doesn't cause any premature instantiation of @thing:wrangler=ThingWrangler.new(:abc)# NOW we call a method which in turn instantiates @thing:wrangler.some_method
What’s great about this pattern is it affords you many opportunities for customization. For example, you can write a subclass which swaps Thing out entirely! Dig this:
classHugeThingWrangler<ThingWranglerdefthing@thing||=HugeThing.new(@important_value)endendwrangler=HugeThingWrangler.new(:abc)wrangler.some_method# uses HugeThing under the hood
Or when testing ThingWrangler where you want Thing to be a mock object under your control, you could simply stub the thing method so it returns your mock instead of the usual Thing instance.
Or if you wanted to get real wild, here’s a bit of metaprogramming to add custom functionality around the original method:
ThingWrangler.class_evaldoalias_method:__original_thing,:thingdefthingputs"ThingWrangler#thing has been called!"obj=__original_thingputs"Now returning the thing object!"objendend
Now every time ThingWrangler accesses thing internally, your custom code will get run. (Careful out there!)
A memoized method shouldn’t be reliant on changing data, because its job is to return a single instance of Thing that gets cached and won’t ever change. So if you had code that looks like this:
You can’t memoize that instantiation, because you need a new Thing instance every time. However, what you could do instead is memoize the class itself! 🤯
This still provides many of the benefits of the techniques we’ve described in terms of allowing subclasses to alter functionality, mock objects in tests, etc. Depending on the needs of your API, you might even want to create a configuration DSL to allow that Thing constant to be officially customizable by consumers of your API. (And to reiterate, still no DI techniques required!)
One other caveat is if the original memoization method is overly complicated or reliant on internal implementation details, you could get into trouble with future subclasses.
classParentClassdefdependency@dependency||=DependentClass.new(lots,of,input,values)endendclassChildClass<ParentClassdefdependency# Hmm, what if the parent class changes internally and I don't?!@dependency||=AnotherDependentClass.new(what,should,go,here)endend
In fact, expensive custom logic typically isn’t compatible with the memoization technique as-is. Instead, a good pattern (if possible) to use for your dependency is simply to be given a reference to the calling object itself:
That way, it’s up to the dependency to glean any relevant data from the calling object in order to perform its work when required. This technique is used frequently across the Bridgetown project which I maintain.
The Lazily-Instantiated Memoization technique is a powerful one and, when used appropriately and in a consistent fashion, it will help your objects become more modular and more easily customized and tested. Consider it whenever you need to manage dependencies within your Ruby code.
I’ve had a doozy of a time writing this article. See here’s the thing: I’ve been a Ruby programmer for a long time (and a PHP programmer before that). My other main language exposure just before becoming a Rubyist was Objective-C. That did require putting type names before variable or method signatures, but also Objective-C featured a surprising amount of duck typing and dynamism as well (for better or worst…Swift tried to lock things down quite a bit more).
But then there’s JavaScript / TypeScript.
My relationship with JavaScript is…complicated, at best. I actually write quite a lot of JavaScript these days. Even more to the point, a lot of the JavaScript I write is in the form of TypeScript. I don’t hate JavaScript. The modern ESM environment is quite nice in certain ways. Certainly an improvement over jQuery spaghetti code and callback hell.
But TypeScript is simply a bridge too far for me. I use it because a project I’m on requires it, but I don’t enjoy it. At times I hate it so much I want to throw my computer across the room. However, I can’t deny its appeal in one respect: those Intellisense popups and autocompletes in VSCode are very nice, as well as the occasional boneheaded mistake it warns me about.
What does any of this have to do with Ruby? I’m getting there. Bear with me just a wee bit longer, I implore you!
One interesting trend I’ve started to see as of late (at least on Twitter) is taking what’s cool about TypeScript type checking, Intellisense, and all the rest…but applying it in such as way that you’re not actually writing TypeScript, you’re writing JavaScript. What you do is use JSDoc code comments to add type hints to your file (but not as your actual code). Then use a special mode of TypeScript type checking which will parse the JSDoc comments and interpret them as if you’d written all your type hints inline as actual code. Here’s a fascinating article all about it.
If this is starting to sound just a wee bit familiar to you, O Rubyist, it should—because that’s exactly what it’s like using YARD + Solargraph with Ruby.
Right now, I’m in the middle of an extensive overhaul of the Bridgetown project to add YARD documentation comments to all classes and methods. With the Solargraph gem + VSCode plugin installed, I get extensive type descriptions and code completion with a minimal amount of effort. If I were to type:
resource.collection.site.config
It knows that:
resource is a Bridgetown::Resource::Base
collection is a Bridgetown::Collection
site is a Bridgetown::Site
config is a Bridgetown::Configuration
And if I were to pass some arguments into a method, it would know what those arguments should be. And if I were to assign the return value of that method to a new variable, it would know what type (class) that variable is.
Livin’ the dream, right? But the one missing component of all this is strict type checking. Now the Solargraph gem actually comes with a type checking feature. But I’ve never used it, because I feel like if I were to go to the trouble of adding type checking to my Ruby workflow, I’d want something which sits a little closer to the language itself and is a recognized standard.
Sord was originally developed to generate Sorbet type signature files from YARD comments. Sorbet is a type checking system developed by Stripe, and it does not use anything specific to Ruby 3 but is instead a custom DSL for defining types. However, Sord has recently been upgraded to support generation of RBS files (Ruby Signature). This means that instead of having to write all your Ruby 3 type signature files by hand (which are standalone—Ruby 3 doesn’t support inline typing in Ruby code itself), you can write YARD comments—just like with Solargraph—and autogenerate the signature files.
Once you have those in place, you use a tool called Steep, which is the official type checker “blessed” by the Ruby core team. Steep evaluates your code against your signature files and provides a printout of all the errors and warnings (similar to any other type checker, TypeScript and beyond).
So here’s my grand unifying theory of Ruby 3 type checking:
You write YARD comments in your code.
You install Solargraph for the slick editor features.
You install Sord to generate .rbs files.
You install Steep to type check your Ruby code.
Nice theory, and extremely similar in overall concept to all the folks writing JavaScript yet using JSDoc to add “TypeScript” functionality in their code editors and test suites.
Unfortunately the reality is…not quite there yet. It kinda sorta works—with several asterisks. Hence the reason it took me so long to even write an article about Ruby 3 typing…and I’m not even sharing examples of how to do it but instead my thought process around why you’d want to do it and what the benefits are relative to all the hassles and headaches.
In my opinion, a type checking system for Ruby is useless unless it’s gradual. I want everything “unchecked” by default, and “opt-in” specific classes or even methods as we go along. While YARD + Solargraph alone gives you this experience, adding Sord + Steep into the mix does not. There doesn’t currently seem to be a way to say only generate type signatures for this file or that and only check this part of the class or that. At least I wasn’t able to find it.
In addition, all this setup is confusing as hell to beginners. There’s no way I can take someone’s fresh MacBook Air and install Ruby + VSCode + Solargraph + Sord + Steep (perhaps also Rubocop for linting) and get everything working perfectly with a minimum of headache and fuss. I myself have seen Solargraph and/or Rubocop support in VSCode break several times for unclear reasons, and it’s been a PITA to fix.
So here’s my crazy and wacky proposal: This should all be one tool. 🤯 I want to sit down at a computer, install Ruby + AwesomeRubyTypingTool, and it all just works. That’s the real dream here. I mean, TypeScript is TypeScript. It’s not a bunch of random JS libraries you have to manually cobble together into some kind of coherent system. TypeScript—for all its gotchas and flaws—is a known quantity. You might even say it just works—at least in VSCode. (No surprise there: both VSCode and TypeScript are Microsoft-sponsored projects.)
I have no idea what it would take for the Ruby core team and the other folks out there building these various tools to get together and hash this all out. But I really hope this story gets a hell of a lot better over the coming months. Because if not…I might just kiss Ruby 3 typing goodbye.
But not Solargraph. You’d have to pry that out of my cold dead hands. 😆