[HN Gopher] Sorbet and 100% cov makes Ruby refactoring possible
___________________________________________________________________
 
Sorbet and 100% cov makes Ruby refactoring possible
 
Author : craigkerstiens
Score  : 72 points
Date   : 2022-04-25 18:45 UTC (4 hours ago)
 
web link (brandur.org)
w3m dump (brandur.org)
 
| d4mi3n wrote:
| This reflects my experience working with Ruby over the years as
| well. I find it a perfectly pleasant language, but as with many
| other scripting languages, things get difficult as a project
| increases in size, complexity, and number of contributors.
| 
| Types are a great way to remove certain classes of issues. It's
| my hope that newer versions of Ruby really push gradual typing
| features I've been hearing about forward into common use. The
| productivity gains of preventing all silly Nil and type errors
| will be enormous.
 
  | valcron1000 wrote:
  | Setting aside all the dynamic stuff that you can't statically
  | check, what does Ruby offer fhat makes it so "pleasant" to use?
  | 
  | I'm trying to understand what a statically typed Ruby could
  | bring to the table.
 
    | andrewzah wrote:
    | There essentially already is a statically typed ruby:
    | Crystal.
    | 
    | https://en.wikipedia.org/wiki/Crystal_(programming_language)
 
      | chrisseaton wrote:
      | Crystal has a look-alike syntax to Ruby. But the semantics
      | (the important bit) aren't even remotely the same.
 
        | andrewzah wrote:
        | What semantics is crystal lacking? Obviously it's not a
        | 1:1 replacement as the ecosystem is different, gem
        | management (shards) is different, etc. Crystal also has
        | union types and concurrency.
        | 
        | For people who want a syntax like ruby +
        | performance/concurrency, but are willing to deal with a
        | different ecosystem and having to do more stuff by hand,
        | Crystal is a nice choice.
 
        | chrisseaton wrote:
        | Local variables, method calls, basics like that are
        | basically entirely different. Take the Ruby specification
        | test suite and try to run it on Crystal, even with adding
        | typing and other minor changes, and see how far you get.
        | 
        | (Ruby also has full concurrency, by the way.)
 
        | sethrin wrote:
        | Your criticism does not seem rigorously considered. Toy
        | programs can often simply be renamed from .rb to .cr and
        | be compiled and run as Crystal code. Crystal is not,
        | however, trying to be a drop-in replacement: among other
        | things, an enforced type system is not a minor change.
        | Compilation is not a minor change to a language either.
        | It's valid to say that you don't like the tradeoffs (and
        | of course ideally you would have a full understanding of
        | what those are), but it's incorrect to suggest that these
        | languages are not extremely similar.
 
        | andrewzah wrote:
        | Crystal has local variables. I didn't mean to imply that
        | ruby & crystal are 1:1 drop-in replacements, but there
        | are a great many similarities. Enough for me as a
        | ruby/rails dev to start on crystal projects with
        | relatively little issue once I learned a bit about the
        | standard library and the ecosystem.
        | 
        | Ruby's way of handling types is abhorrent compared to
        | Crystal, though. Sorbet/RBS are unfortunate systems
        | tacked on after people realized that type systems are
        | actually really good and not really that verbose.
 
    | thr0wawayf00 wrote:
    | The block syntax is a wonderful way to build and chain
    | flexible iterative logic, the object model is fairly
    | straightforward and the standard library strikes a good
    | balance between functionality and brevity. Once you start to
    | pick up on ruby idioms, the language is pretty fun to use
    | IMO.
 
| pqdbr wrote:
| It's a shame the article ends with "So as usual, consider not
| writing Ruby, but if you do, ...". That's worthless advice and
| many people, myself included, have Ruby as their most beloved
| programming language.
 
  | afandian wrote:
  | I've not written Ruby since about 2006 except with a stint in a
  | Rails codebase which did nothing to give me any faith. How do
  | you go about e.g. extracting a group of fields and renaming
  | them? Or reorder function arguments? Is there a static tool
  | that can do this, or do you have to do some kind of text search
  | and hope you got them all?
 
    | dymk wrote:
    | There's basically no tool for doing that, aside from relying
    | on tests to tell you a callsite is now incorrect. "find and
    | replace" is... okay, up to a point, but obviously that has
    | its limitations (especially once you start metaprogramming).
    | 
    | That being said, I still love Ruby. I have all sorts of
    | little tools written in it that would have been a pain to do
    | in another language.
 
      | afandian wrote:
      | Coming from a Clojure background which is ... different.
      | But I understand the enthusiasm around similar language
      | features.
      | 
      | But to borrow words from another comment I find Kotlin
      | "ruthlessly productive". Apart from the lambdas, functions,
      | data classes and immutably, it's the ability to quickly and
      | correctly refactor that makes me feel like I can work at
      | speed and try stuff out.
 
  | bestinterest wrote:
  | There is a gap between languages at the moment imo. Ruby is
  | just ruthlessly productive.
  | 
  | Compare
  | https://github.com/benhoyt/countwords/blob/master/simple.rb
  | with
  | https://github.com/benhoyt/countwords/blob/master/simple.go
  | 
  | Hoping new age compiled languages like Crystal & Nim fill the
  | gap of performance, types & productivity. But compile speeds
  | need to be factored in.
 
    | andrewzah wrote:
    | Comparing a trivial <500 LoC program between languages
    | doesn't tell you anything that's useful other than the
    | terseness of the syntax. You might as well chain unix
    | utilities together at that point.
    | 
    | Maintaining a 5k+ LoC java/c#/go/rust/crystal codebase is
    | orders of magnitude simpler than standard ruby. Sorbet/RBS
    | bridge that gap now, but are a pita to use compared to
    | natively implementing a type system. I know with rust/go I
    | get a simple binary at the end to copy over. Rust,
    | unfortunately, has slow compilation times still compared to
    | go, but I really can't stand error handling in go compared to
    | rust.
    | 
    | That said, "ruthlessly productive" is an apt description. I
    | just don't want to have to maintain a large rails codebase
    | again without sorbet/rbs. I'm hoping phoenix/elixir or
    | something in rust catches on.
 
    | lawl wrote:
    | > Hoping new age compiled languages like Crystal & Nim fill
    | the gap of performance, types & productivity.
    | 
    | Yes, go is verbose at times. But the go language server for
    | example lets you not write a lot of the boiler plate you see
    | there.
    | 
    | E.g. instead of typing out a for loop, i'd just (start)
    | typing `foo.range!`, or for sorting `foo.sort!`.
    | 
    | I'm not going to argue that writing it in go even with those
    | would be more terse, just that I think it _looks_ worse than
    | it is.
 
    | gedy wrote:
    | > Ruby is just ruthlessly productive.
    | 
    | Not disagreeing, but the nuance is productive for writing new
    | code/features. It really feels counter productive once you
    | have a large codebase/team and you need to refactor existing
    | apps.
    | 
    | Spent 5 years at a Rails shop and it's crazy how much
    | engineering effort was spent on keeping this app going.
    | Adding typing seems like a nice step to help here.
 
      | klardotsh wrote:
      | Yeah, I disagree with "ruthlessly productive" as a blanket
      | statement because of this. I tend to find that a huge chunk
      | of the time I save at write-time in Ruby
      | (Rails)/Python/etc., I end up repaying at either runtime
      | (nil exceptions) or read/explain-to-other-dev/refactor
      | times, sometimes in multiplicative form (chasing down why
      | something became nil, or a string, or an elephant, but only
      | if the ORM did X, Y, and Z to the DB response, etc. gets
      | ridiculous quickly)
 
    | pphysch wrote:
    | Is Ruby "ruthlessly productive" for building a fast, highly
    | concurrent program that can be deployed to X platforms with
    | minimal fuss? No.
    | 
    | Is Golang "ruthlessly productive" for interfacing with
    | complex relational databases? No, not even with generics.
    | 
    | Tradeoffs, every language has 'em.
 
    | recuter wrote:
    | Well, the go one would compile to an easily deployable binary
    | and actually be able to, you know, count a whole lot of words
    | quickly. But the ruby one is much more terse and quicker to
    | write.
    | 
    | I say we go back to awk and get the best of both worlds:
    | '{num+=NF} END{print num+0}'
 
      | lambdaba wrote:
      | Ironically half that code (END...) is valid Ruby!
 
    | latenightcoding wrote:
    | The Perl solution would be significantly smaller tho.
 
    | rco8786 wrote:
    | > Ruby is just ruthlessly productive.
    | 
    | This is the best description of Ruby I've ever read.
 
      | princevegeta89 wrote:
      | +1. After working on Elixir for a long time I missed the
      | simple ruby way to do things and found myself running in
      | circles to troubleshoot some basic 3rd party libraries.
      | Going back to Ruby for my next side project.
 
  | rattray wrote:
  | Just because you disagree with it doesn't mean it's worthless
  | advice.
  | 
  | I always have a fond flutter whenever I see Ruby (which I used
  | to write, and really enjoyed) but absolutely wouldn't start a
  | new project with it, for practical reasons. It's slow, it can
  | turn into a big ball of mud as applications scale (there's no
  | named imports for crying out loud, everything is just in a
  | global namespace with side-effects everwyhere), etc.
  | 
  | Certainly there are still people/projects where, upon
  | consideration, Ruby is still the best choice (eg; small Rails
  | shop has a standard CRUD app to build quickly that will not
  | likely ever scale to be huge). But you should still _consider_
  | not using Ruby.
 
  | ecshafer wrote:
  | Absolutely agree. Ruby is a great and beautiful language, and
  | Rails is imo the best web framework for getting things done.
 
    | RTFM_PLEASE wrote:
    | Rails is frankly a double-edged sword, though. While it
    | helped propel Ruby, and is a great framework, it also seems
    | to have permanently marked the language with a notion that
    | its simply a vehicle for Rails.
    | 
    | Ruby is probably one of the more coherently designed general
    | purpose programming languages (in my opinion, much more so
    | than Python, a language that dominates the industry) but
    | doesn't seem to get much use in that domain, which is a
    | shame.
 
      | chris24680 wrote:
      | As a rails developer I really believe if Ruby had at least
      | one more 'Killer App' it would lead to a much healthier
      | overall community.
 
| mpweiher wrote:
| The idea that refactoring requires static types is ahistoric and
| simply incorrect.
| 
| The first automatic refactoring tool was the Refactoring Browser,
| in Smalltalk.
| 
| https://refactory.com/refactoring-browser/
| 
| https://www.researchgate.net/publication/220346807_A_Refacto...
 
  | recuter wrote:
  | I don't understand why this is the bottom comment instead of
  | the top one.
 
  | a1445c8b wrote:
  | The PDF[1] that the second link points to gives a very good
  | background on why a refactoring tool should be part of any
  | software engineer's toolkit.
  | 
  | Thanks for sharing!
  | 
  | [1] https://www.researchgate.net/profile/Don-
  | Roberts-4/publicati...
 
| alberth wrote:
| NIM
| 
| I'm still surprised more startups don't use NIM. It has all of
| the productivity benefits of Python but performance of C.
 
  | brigandish wrote:
  | Did you come to Nim from a Ruby background? I've been using
  | Crystal and am very happy with it but would be interested to
  | know what the jump to Nim might be like.
 
  | [deleted]
 
| DerArzt wrote:
| I get what the author is saying, but static analysis aside I
| would do all that I can to avoid a 178 file update. I get that
| sorbet is allowing them to do this with higher confidence but
| even in a language that's compiled (thus performing a similar
| role to Sorbet) that much change is asking for trouble.
| 
| If making a single update touches that many source files, then
| they may want to take a look at their code organization and
| architecture as that is a whole heck of a lot!
 
  | shoo wrote:
  | the most productive i have ever been working as a software
  | engineer was in a smaller company where roughly all of the
  | company's product and library code was in a monorepo, and we
  | had a reasonable culture of writing automated tests (although
  | not at 100% branch coverage). sometimes you realise the
  | abstraction in a core library used by all the product code is
  | wrong or limiting, and if your test suite gives you confidence
  | that you're going to catch any potential regressions, you can
  | rework the library with a breaking change & fix everything that
  | transitively depends on that library in a single atomic commit.
 
    | DerArzt wrote:
    | The rub for me here is that making one change in a core
    | library ideally wouldn't touch 100+ other source files. This
    | feels like a separation of concerns issue in which a module
    | is being over used.
    | 
    | Perhaps I'm just in the wrong headspace, as I have never had
    | to work in a monorepo fashion. That being said just because
    | your code is in a monorepo, does that directly mean that you
    | aren't versioning your libraries?
    | 
    | To my understanding most package management tooling in most
    | echo systems allow for versioning. With that said, wouldn't
    | you slowly roll out the version to the downstream projects
    | Monorepo or not?
 
      | swatcoder wrote:
      | Refactoring doesn't just mean optimizing the inside of an
      | implementation.
      | 
      | With a monorepo and good test coverage, I can improve the
      | signature of a function on some library, and use a full-
      | featured IDE to confidently update the usage myself without
      | making it a 10-ticket task that spans 12 teams and 18
      | meetings.
 
      | striking wrote:
      | > This feels like a separation of concerns issue in which a
      | module is being over used.
      | 
      | It's not always modules. Libraries and patterns and
      | frameworks and so on, they will come and they will go.
      | Sometimes you want to just change something across the
      | whole codebase, and having complete type and test coverage
      | over a codebase ensure that you can do so _fearlessly_.
      | 
      | > wouldn't you slowly roll out the version to the
      | downstream projects
      | 
      | No need. The upside of doing so is that you generally
      | prevent breakage, but at the cost of having to support
      | multiple older versions of everything. In a monorepo, you
      | can change your codebase from one language to another in a
      | single commit, and everything should work just fine.
      | (Speaking from experience.)
 
        | shoo wrote:
        | > having complete type and test coverage over a codebase
        | ensure that you can do so fearlessly.
        | 
        | here's a bit of hard-won wisdom. if you have a company
        | codebase in a monorepo where the different components
        | (products, libraries) in the monorepo have a fairly
        | uniform level of quality standards, and there is a
        | uniformly high level of test automation & tooling to
        | detect regressions and enable confidence when making
        | sweeping changes, it is incredibly productive.
        | 
        | however, if you have a company codebase in a monorepo
        | where some regions of the code have wildly different
        | quality standards, and test automation may be patchy or
        | missing from some components, the lack of flexibility and
        | coupling caused by how a monorepo resolves internal
        | dependencies can produce a miserable experience. low-
        | quality code without a good automated test suite or other
        | tools to detect regressions needs to be able to pin
        | versions of libraries, or some other mechanism to
        | decouple from the rate of change of the high-quality
        | components.
        | 
        | E.g. a single developer may be the sole person allocated
        | to a project to attempt to build a prototype to try to
        | win a new customer, so they may be bashing out a lot of
        | lower quality code -- often for a good business reason --
        | without much peer review or test automation. If that
        | lower quality codebase is in the monorepo and depends on
        | core libraries in the same monorepo, then you get a
        | situation where core library developers expect to be able
        | to make breaking changes to core libraries, and if the
        | test suite is green, merge -- but the low-quality
        | prototype codebase doesnt have any tests and just gets
        | increasingly broken (the consultingware prototype code
        | test suite is always vacuously green). Or conversely, the
        | developer trying to get their consulting project over the
        | line might end up telling the core library developers
        | that they're not allowed to make breaking changes as it
        | keeps pulling the rug out from underneath business-
        | critical prototype project delivery, and then you're in a
        | situation where it is no longer possible to refactor the
        | core libraries.
 
      | shoo wrote:
      | > just because your code is in a monorepo, does that
      | directly mean that you aren't versioning your libraries?
      | 
      | If you are living the monorepo trunk-based development
      | dream, you version all of your product and library code
      | with the commit of the enclosing monorepo. The product code
      | contained in monorepo commit X depends on the versions of
      | the libraries also contained in that same monorepo commit
      | X. Maybe another way to say it is that library dependencies
      | are resolved to whatever is checked out in version control,
      | without going through an abstraction layer of versioned
      | package releases pushed to some package repository.
      | 
      | > To my understanding most package management tooling in
      | most echo systems allow for versioning
      | 
      | correct. but that doesn't mean adopting a decentralised
      | package-based versioning strategy is the most productive
      | way for a team to operate!
      | 
      | > With that said, wouldn't you slowly roll out the version
      | to the downstream projects Monorepo or not?
      | 
      | Perhaps! I can think of some arguments why you might prefer
      | a gradual rollout: to reduce effort and split the work into
      | smaller pieces that can be delivered independently, to
      | reduce risk of the change breaking one of N products it
      | touches, forcing the entire change to be rolled back.
      | 
      | But on the other hand, you don't have to -- you can choose
      | to do the refactor atomically, which is not a choice you
      | have if the product and library code is scattered across N
      | different source control systems that depend on each other
      | through versioned package releases.
      | 
      | If you are working in a monorepo & all your internal
      | library dependencies are fulfilled by the coupled library
      | version in the monorepo commit checkout, not decoupled
      | through versioned package releases, then you would need to
      | use different techniques to allow flexibility of some
      | products to depend on version V1 of a library at the same
      | time as other products depend on version V2. The most
      | obvious one is creating a copy of the entire V1 library,
      | giving it a new name, making the V2 change, checking it in
      | to the monorepo as a sibling library, then rewriting some
      | of the products to depend on V2. See also
      | https://trunkbaseddevelopment.com/branch-by-abstraction/
 
    | swatcoder wrote:
    | Yup.
    | 
    | A project that can absorb refactors safely has a lot more
    | headroom than one where every change has to be an incremental
    | one near the leaves.
 
___________________________________________________________________
(page generated 2022-04-25 23:00 UTC)