[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) (HTM) web link (brandur.org) (TXT) 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)