Skip to content

Instantly share code, notes, and snippets.

@rstacruz
Last active December 5, 2021 18:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rstacruz/c13436caf315300336c840fcfe3dbcef to your computer and use it in GitHub Desktop.
Save rstacruz/c13436caf315300336c840fcfe3dbcef to your computer and use it in GitHub Desktop.

Opinion: Ruby's pessimistic operator

Today I realised that Ruby's >= is more useful than ~>.

Rubygems’s pessimistic operator ~>, also known as “twiddle-wakka”, allows developers to specify flexible gem version ranges.

This... Is equivalent to...
gem 'i18n', '~> 2.3.5' gem 'i18n', ‘>= 2.3.5’, '< 2.4.0'
gem 'i18n', '~> 2.3' gem 'i18n', ‘>= 2.3.0’, ‘< 3’
gem 'i18n', '~> 2' gem 'i18n', '>= 2.0.0'

While many gems prescribe the use of ~>, it may be a good choice to opt for >= instead to avoid some of the pitfalls with Ruby’s pessimistic operator.

Problem 1: It restricts updates

When matching full versions (eg, ~> 4.7.1), it may be restricting updates to future versions unintentionally.

gem 'pry', '~> 4.7.1'  # same as '>= 4.7.1', '< 4.8'

When matching full versions (eg ~> 4.7.1), it will restrict matching to the 4.7 series and avoid upgrading to 4.8. This makes little sense in Semver: newer major releases (eg, 4.7, 4.8, 4.9…) should be considered backward-compatible with older releases in the same version (4.1).

Problem 2: Updating patch versions

When trying to update patch versions (eg, 5.2.0 to 5.2.3), using the pessimistic operator can lead to some confusion.

gem 'sprockets', '~> 5.2' # same as '>= 5.2.0', '< 6'

When a gem is matched pessimistically to a minor version (eg ~> 5.2), and a new update is available (eg, 5.2.3), it may be tempting to bump the version.

- gem 'sprockets', '~> 5.2'
+ gem 'sprockets', '~> 5.2.3' # (!) same as '>= 5.2.3', '< 5.3'

Doing this would have an unintendend side effect: it will restrict upgrading to version 5.3, which wasn’t intended in the previous revision. The correct fix is to change it to use the >= operator:

- gem 'sprockets', '~> 5.2'
+ gem 'sprockets', '>= 5.2.2', '< 6.0'

Problem 3: Pre-1.0 versions

When matching versions before 1.0, it may eagerly match incompatible versions unintentionally.

gem 'geocoder', '~> 0.7' # (!) same as '>= 0.7.0', '< 1'

Specifying pre-1.0 versions as ~> 0.7 is common, but semver discourages this. Every minor version before 1.0 is considered to be a breaking version. That is, upgrading from 0.7 to 0.8 should be considered in the same regard as 1.0 to 2.0. Semver spec has this:

“Major version zero (0.y.z) is for initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable.”

Semantic Versioning 2.0.0, spec item 4

A more reasonable approach might be to specify an upper bound:

gem 'geocoder', '>= 0.7.0', '< 0.8'
# or
gem 'geocoder', '~> 0.7.0'

Prior art: the caret operator

Node.js also supports the ~ operator for semver ranges. However, it’s use is generally discouraged, as a newer ^ (caret) operator was introduced to fix the faults of the ~ operator. Unlike the Ruby pessismistic operator, the caret will always pick compatible versions based on Semver’s specification.

^1.2.3 := >=1.2.3, <2.0.0
^1.2   := >=1.2.0, <2.0.0
^1     := >=1.0.0, <2.0.0
^0.2.3 := >=0.2.3, <0.3.0
^0.2   := >=0.2.0, <0.3.0
^0.0.3 := >=0.0.3, <0.0.4
^0.0   := >=0.0.0, <0.1.0
^0     := >=0.0.0, <1.0.0

There were a few discussions in the Node.js community that might be worth pursuing:

Rust’s Cargo also adopts and encourages the use of the ^ specifier.

The caret operator exists in other languages and package managers:

  • PHP's Composer (docs)
  • Rust's Cargo (docs)
  • Node.js's npm
  • Elixir's mix

Recommendations

Ruby doesn’t have a ^ operator, but it can be emulated using a combination of >=and < specifiers.

Instead of… Consider instead...
gem 'xxx', '~> 2.4'
gem 'xxx', '~> 2.4.5'
gem 'xxx', '>= 2.4.0'
gem 'xxx', '>= 2.4.0', '< 3'

Interesting tidbits

@simnalamburt
Copy link

@rstacruz Elixir's mix does not support caret operator. So I just opened a PR for this: elixir-lang/elixir#11448

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment