davideisinger.com

My personal website
Log | Files | Refs | README

macwright-com-o4dndf.txt (16371B)


      1 Tom MacWright
      2 
      3 [email protected]
      4 
      5 Tom MacWright
      6 
      7   • [1]Writing⇠
      8   • [2]Reading
      9   • [3]Photos
     10   • [4]Projects
     11   • [5]Drawings
     12   • [6]Micro
     13   • [7]About
     14 
     15 A year of Rails
     16 
     17 Railroad
     18 
     19 I spent most of 2020 working with [8]Ruby on Rails. I moved a project from [9]
     20 Next.js + [10]Rust to… Rails, baby! Back to the future. My earlier post on [11]
     21 Second-guessing the modern web was inspired by this experience, that for the
     22 product we were building, a ‘modern’ stack was not working as well as a
     23 traditional one.
     24 
     25 We didn’t do competitive analysis against Laravel, Django, or Phoenix. They’re
     26 similar, not radically better or worse. There are multiple acceptable solutions
     27 to a problem, and this was more a matter of choosing the right kind of solution
     28 than pursuing some kind of perfect choice and burning hours and motivation
     29 doing the window-shopping.
     30 
     31 What helped Rails win was that the team had a little more experience in Ruby
     32 (with the exception of myself), and we found plenty of resources for developing
     33 and deploying the stack. Rails fit perfectly into the ideology of [12]Choosing
     34 boring technology. Another part of the product would be the hard, innovative
     35 part, so it made no sense to grapple with bleeding-edge web frameworks.
     36 
     37 This was a really fun experience. There’s a lot to love about Rails. Other
     38 communities could learn a bit from the Ruby & Rails culture and wisdom. I won’t
     39 implement everything in Rails, but it’ll be part of the toolbox.
     40 
     41 Before this, I hadn’t touched the stuff. And I bet a lot of people are like
     42 that - they came of age in the world of React and Go, and haven’t tried
     43 anything even remotely similar to Rails. For their benefit, and to debrief from
     44 2020, here are some notes on the experience. Plus, [13]Rails-like projects in
     45 JavaScript are ramping up quickly, and it’s fun to know the origins.
     46 
     47 The good
     48 
     49 Debugging Rails apps is amazing
     50 
     51 A while ago, I [14]wrote on Twitter
     52 
     53     the real reason why javascript developers don’t use breakpoints and use
     54     console.log is that breakpoints don’t work
     55 
     56 After years of working in JavaScript, I’m used to bad debugging experiences.
     57 The Chrome debugger’s [15]automatic pause on caught exceptions is amazing,
     58 sometimes. But throwing a debugger statement in some React code is dodgy as
     59 hell. Sometimes it works, mostly it doesn’t. You have to deal with code that
     60 might not have the right [16]sourcemap to translate from bundled & minified
     61 code to original source. Subtle abstractions like React hooks and advanced
     62 transpiler stuff like [17]Regenerator mean that your code’s stacktrace probably
     63 looks nothing like what you expect, with lots of internal garbage. Sure, you
     64 can learn better techniques for diagnosing and debugging errors, but it’s not
     65 just you - the debugging story in JavaScript is pretty bad. This applies even
     66 to Node.js, where one of the debugging stories is to connect Chrome’s debugger
     67 to a Node.js instance: a finicky solution that doesn’t consistently work.
     68 
     69 In Rails, there is [18]byebug. You write byebug in your source code, and you
     70 get an interactive REPL right there. It works in views, controllers, database
     71 migrations, everywhere. It almost always works. Variables are named what you
     72 expect. The whole system is paused at that moment, and you can actually
     73 interact with it, using all of the Rails utilities and your installed gems.
     74 
     75 If a page crashes unexpectedly, you get a similar REPL experience, in your
     76 browser, automatically. With an automatically cleaned-up stacktrace that
     77 excludes Rails’s own frames. Like the byebug interface, this REPL actually
     78 works and is consistently helpful in finding root causes. Rarely will you need
     79 to use puts to print something to the console because this debugging system is
     80 so good.
     81 
     82 The magic mostly works
     83 
     84 Our Rails app didn’t have any require statements. You mention a module’s name,
     85 and it’s automatically included, using [19]Zeitwerk, a tool that comes standard
     86 with Rails.
     87 
     88 This kind of system was terrifying to me before. What if you accidentally
     89 import something just by mentioning it? What if two things have the same name
     90 and you import the wrong one? How do you really know what’s happening? Sure,
     91 you’re happy now, with all of that annoying importing and exporting taken care
     92 of, but the sky might fall.
     93 
     94 Or maybe it just… doesn’t. Maybe impure, vaguely risky techniques are just a
     95 net positive over time, and making everything fully explicit isn’t really
     96 necessary? Now when I’m using other systems, I wonder - what if I could just
     97 mention one of my React components and it would just… be there? Sure, the
     98 system would have to complain if there were two components with the same name,
     99 and it would have to make assumptions about directory structure, but overall,
    100 wouldn’t this be nice?
    101 
    102 This applies to a lot of other parts of the system too. Rails is famous for
    103 doing pluralization - you name a model Post and you automatically get an
    104 interface called posts. But what, you ask, of words with uneven pluralization
    105 rules? Rails actually [20]does the right thing, almost always. And when it
    106 fails, you can override it. It actually just saves time, reliably.
    107 
    108 Testing works
    109 
    110 I’ve tried to test front-end applications. I’ve set up [21]nightwatch, [22]jest
    111 , [23]enzyme, [24]cypress, and probably 5-10 other frameworks. Front-end
    112 testing is universally terrible. Projects like Cypress are throwing untold
    113 hours into making it less terrible, taking on massive amounts of complexity to
    114 abstract away from fickle browser behavior and complex interactions.
    115 
    116 But it still sucks. Frontend testing has no good attributes: it’s unreliable,
    117 hard to automate, hard to debug when it fails, and often doesn’t even assert
    118 for important behaviors, so it doesn’t actually identify regressions. Running
    119 frontend tests in CI is resource-heavy, requiring you to set up headless X
    120 windows environments on servers or use specialized CI services that produce
    121 screencasts of test runs.
    122 
    123 Testing fully-server-rendered applications, on the other hand, is amazing. A
    124 vanilla testing setup with Rails & [25]RSpec can give you fast, stable,
    125 concise, and actually-useful test coverage. You can actually assert for
    126 behavior and navigate through an application like a user would. These tests are
    127 solving a simpler problem - making requests and parsing responses, without the
    128 need for a full browser or headless browser, without multiple kinds of state to
    129 track.
    130 
    131 Not only do the tests work better, the testing culture is a completely
    132 different universe. There are entire books written about how to write RSpec
    133 tests that catch bugs, allow software evolution, and aren’t filled with
    134 boilerplate.
    135 
    136 Gems are so powerful
    137 
    138 Powerful and dangerous.
    139 
    140 I’m used to modules as they work in other systems - Python, Node, Elm, and so
    141 on. They provide objects, functions, and variables that you can import and
    142 combine into your code explicitly. Usually they sit on some specific level of
    143 abstraction - it’s a utility for connecting to servers or a React component you
    144 can use.
    145 
    146 Gems can do so much more. You install something like [26]Devise into your
    147 system and it adds views, routes, methods, utilities, you name it. It’s not
    148 like “loading some functions”, it’s more like composing a whole different app
    149 into your app, implicitly.
    150 
    151 This is obviously terrifying. It means that you can’t look at your directories
    152 of views and your file of routes.rb and know what exists at a glance. There are
    153 other layers, lurking in the ephemeral space of third-party code. They interact
    154 in serious but uncertain ways.
    155 
    156 But it’s also pretty incredible - the idea that something like [27]passport,
    157 Node’s middleware, could instead be a full-fledged authentication system. It
    158 means that you have to write a lot less code, and it also means that the people
    159 who use that code have a lot more code in common. That gems can work on a
    160 higher level of abstraction, making it possible to cobble together software
    161 faster, to write less ‘glue code.’
    162 
    163 There’s so much good writing about Rails
    164 
    165 Even if you don’t write Ruby, you should pay attention to [28]Sandi Metz. She’s
    166 incredibly wise and has so many incredible ideas to share.
    167 
    168 And then there’s [29]arkency, [30]ThoughtBot, and so many other thoughtful
    169 writers with years of experience in Rails. Sometimes it’s a little shocking to
    170 google for some obscure problem and see a decade of discussion about it.
    171 
    172 The best practices are also formalized into tools like [31]Code Climate and 
    173 [32]reek. I’ve never seen so many actually-useful suggestions come out of
    174 automated systems as I did in the world of Ruby and Rails.
    175 
    176 Ruby
    177 
    178 Ruby is a pretty pleasant language to work in. Sure, it has a lot of syntax and
    179 a sprawling standard library, but you don’t have to use all of that if you
    180 don’t want to. It took me a while to adjust to the object-oriented way of doing
    181 things - in particular, the idea that you can’t just have a free-range function
    182 floating out there, unassociated with a class or module, like you can in
    183 JavaScript. And you can’t just create an arbitrary one-off object - you either
    184 need to define a class to create an object, or use a Hash to store data.
    185 
    186 But Ruby’s standard library isn’t that huge. I’ve seen JavaScript’s ‘standard
    187 library’ grow a lot too, and frankly it’s nice to have methods like [33]
    188 String.prototype.padStart instead of having every little thing in userspace.
    189 The only part that felt actively weird was [34]activesupport - a gem that
    190 extends Ruby’s core objects, but is part of Rails. It felt weird to have string
    191 methods that would only work if your environment was Rails.
    192 
    193 The [35]Dash app for documentation rocketed from my pile of unused tools to an
    194 absolute must-have. In the world of Ruby and Rails, with most gems having
    195 pretty good, semi-standard documentation, you can search for, and get answers,
    196 super fast. The Ruby language documentation and the Rails documentation is
    197 absolutely great. The JavaScript equivalent - [36]MDN - pales in comparison.
    198 
    199 The bad
    200 
    201 The asset pipeline
    202 
    203 Remember SASS and the YUI Compressor? These are, unfortunately, defaults in the
    204 [37]asset pipeline. There’s [38]Webpacker too, which has a parallel approach to
    205 CSS and images as the asset pipeline. It has [39]opinionated integrations with
    206 stuff like React. Ah, and I should mention that Rails’s [40]JavaScript
    207 utilities are written in… CoffeeScript.
    208 
    209 I get it - it’s hard to keep up with the latest trends in frontend. But this is
    210 one area where Rails’s strong backwards compatibility feels iffy. I wish that
    211 Rails was more opinionated about the frontend, and that it had better opinions.
    212 
    213 Best practice churn
    214 
    215     In Smalltalk, everything happens somewhere else. - [41]Adele Goldberg
    216 
    217 Ruby, as today’s Smalltalk, has the same issue. The community venerates small -
    218 that methods should be short, files should be small, complexity should be
    219 controlled. This begs the question of where it all goes - certainly not in
    220 controllers, which should be skinny, and not in views, which should have very
    221 little logic at all, and maybe [42]not in models either. Maybe in [43]Service
    222 Objects, or policies, or decorators?
    223 
    224 I found myself falling victim to this. I’d try to win CodeClimate’s approval by
    225 moving code around, perfecting the art of making everything small or at most
    226 medium-sized, extracting concerns until most files looked okay. This was time
    227 well-spent on learning, but I have to admit that it doesn’t actually matter for
    228 an early-stage startup’s product.
    229 
    230 In stark contrast to the folks who say that Rails is for prototypes, there’s a
    231 lot of attention paid to long-lived engineering efforts - adopting patterns
    232 that let many team work on the same ‘monolith’, identifying [44]shotgun surgery
    233 - a term I first heard from Sandi Metz.
    234 
    235 ActiveRecord is great, except when it isn’t
    236 
    237 One of the hardest bugs we encountered happened with ActiveRecord. We were
    238 creating a set of changes to apply to a model, using their in-memory instances
    239 to do some stuff, and then finally applying them. This broke because one of the
    240 ActiveRecord methods automatically ‘committed’ those changes, quietly.
    241 
    242 ActiveRecord is kind of like this - a lot of the times it’s pleasantly
    243 implicit, letting you just assign a value and automatically saving that to the
    244 database. But then it’ll do something implicitly that you don’t want to happen,
    245 and figuring out why this happened and how to stop it from happening is a real
    246 challenge.
    247 
    248 Most of the time, to be clear - it’s a really great system. It provides lots of
    249 ways to generate efficient-enough queries, knowing full well that SQL
    250 performance is often the bottleneck of web applications. Most of the time it’s
    251 really nice that it automatically casts and deserializes query results. But
    252 when it goes bad, the diagnosis and the cure can be pretty ugly.
    253 
    254 The other issue with ActiveRecord is that it has efficient methods and
    255 inefficient methods right next to each other, because it automatically turns
    256 your ‘query builder’ into an array when you call array-like methods. So, for
    257 example:
    258 
    259 Dogs.all.max_by(&:height)
    260 
    261 Is wildly inefficient. It might fetch and deserialized a million records just
    262 to sort them and give you the first. On the other hand,
    263 
    264 Dogs.order(height: :desc).first
    265 
    266 Is fast - it sorts in the database and fetches a single record. Rails is both
    267 offering smart and easy ways to write optimized code, but also making it really
    268 easy to write inefficient code.
    269 
    270 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    271 
    272 A Rails-like framework is a really good thing to have in your toolbox, and
    273 there’s a lot to learn from the Ruby community. My hope is that we see these
    274 sorts of abstractions in new languages and frameworks, and see more of the Ruby
    275 community’s culture filter into the programming world.
    276 
    277 February 18, 2021  [45]Tom MacWright ([46]@tmcw, [47]@[email protected])
    278 
    279 
    280 References:
    281 
    282 [1] https://macwright.com/
    283 [2] https://macwright.com/reading/
    284 [3] https://macwright.com/photos/
    285 [4] https://macwright.com/projects/
    286 [5] https://macwright.com/drawings/
    287 [6] https://macwright.com/micro/
    288 [7] https://macwright.com/about/
    289 [8] https://rubyonrails.org/
    290 [9] https://nextjs.org/
    291 [10] https://www.rust-lang.org/
    292 [11] https://macwright.com/2020/05/10/spa-fatigue
    293 [12] http://boringtechnology.club/
    294 [13] https://macwright.com/2020/10/28/if-not-spas
    295 [14] https://twitter.com/tmcw/status/1321133460501585922
    296 [15] https://developers.google.com/web/updates/2015/05/automatically-pause-on-any-exception
    297 [16] https://www.html5rocks.com/en/tutorials/developertools/sourcemaps/
    298 [17] https://github.com/facebook/regenerator
    299 [18] https://github.com/deivid-rodriguez/byebug
    300 [19] https://github.com/fxn/zeitwerk
    301 [20] https://weblog.rubyonrails.org/2005/8/25/10-reasons-rails-does-pluralization/
    302 [21] https://nightwatchjs.org/
    303 [22] https://jestjs.io/
    304 [23] https://enzymejs.github.io/enzyme/
    305 [24] https://www.cypress.io/
    306 [25] https://rspec.info/
    307 [26] https://github.com/heartcombo/devise
    308 [27] http://www.passportjs.org/
    309 [28] https://sandimetz.com/
    310 [29] https://blog.arkency.com/
    311 [30] https://thoughtbot.com/blog/
    312 [31] https://codeclimate.com/
    313 [32] https://github.com/troessner/reek
    314 [33] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
    315 [34] https://web.archive.org/web/https://rubygems.org/gems/activesupport/versions/6.1.1
    316 [35] https://kapeli.com/dash
    317 [36] https://developer.mozilla.org/en-US/
    318 [37] https://guides.rubyonrails.org/asset_pipeline.html
    319 [38] https://edgeguides.rubyonrails.org/webpacker.html
    320 [39] https://github.com/rails/webpacker#integrations
    321 [40] https://github.com/rails/rails/tree/main/actionview/app/assets/javascripts
    322 [41] https://en.wikipedia.org/wiki/Adele_Goldberg_%28computer_scientist%29
    323 [42] https://thoughtbot.com/blog/skinny-controllers-skinny-models
    324 [43] https://codeclimate.com/blog/7-ways-to-decompose-fat-activerecord-models/
    325 [44] https://en.wikipedia.org/wiki/Shotgun_surgery
    326 [45] https://macwright.com/about/
    327 [46] https://twitter.com/intent/follow?screen_name=tmcw&user_id=1458271
    328 [47] https://mastodon.social/@tmcw