Skip to content

mwpastore/ruby-piecewise

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Piecewise Rubygem

Piecewise Enumerables, Enumerators, and Lazy Enumerators for Ruby.

Synopsis

Do you ever find yourself doing something like:

even_keys = tuples.map { |k, v| k.upcase if v.even? }.compact

# Or:

even_keys = tuples.select { |_, v| v.even? }.keys.map(&:upcase)

# Or:

even_keys = tuples.each_with_object([]) do |(k, v), memo|
  memo << k.upcase if v.even?
end

Now consider the case when tuples is infinitely large and you want to yield even keys, capitalized, to a downstream consumer.

I've always found this pattern (and its alternatives) a little bit off-putting, especially when you have a big do..end block containing conditional logic and then a .compact tacked on to it. (Maybe I'm picky.) And sometimes nil is a valid value, so you have to introduce a new sentinel value to reject by. You can wrap the logic in an Enumerator.new { |yielder| .. } or move the logic to a method that returns an enumerator, but that can be a lot of boilerplate for what should be a simple filter+map operation.

This simple gem monkeypatches Enumerable, Enumerator, and Enumerator::Lazy to add a #piecewise method that lets you do kind of an inline enumerator. The above example can be rewritten like this:

even_keys = tuples.piecewise { |yielder, (k, v)| yielder << k.upcase if v.even? }

I find this easier to read and grok, and it works the same whether tuples is a simple enumerable or a (lazy) enumerator.

Concept

This gem implements an inversion of control pattern in which the underlying Enumerator::Yielder object of the Enumerator is yielded back to the caller to make use of directly. This makes it easy to perform piecewise filter and/or map operations over an enumerable collection. It is particularly useful when not all of the elements on the input are guaranteed to be present on the output (otherwise a simple #map would do) and when some of the elements may be mutated by the filter (otherwise a simple #select would do). The confluence of these two requirements can lead to inelegant Ruby code.

You can think of #piecewise like Enumerable#each_with_object and Enumerator#with_object with the following key differences:

  • The order of block arguments is reversed
  • The "memo" is always an Enumerator::Yielder (not an arbitrary object)
  • When the receiver is an Enumerator, the result is an Enumerator (not an array)
  • When the receiver is an Enumerator::Lazy, the result is an Enumerator::Lazy (not an array)

Installation

Add this line to your application's Gemfile:

gem 'piecewise'

And then execute:

$ bundle

Or install it yourself like so:

$ gem install piecewise

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

This gem was briefly named chainenum before being renamed to piecewise.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/mwpastore/ruby-piecewise.

License

The gem is available as open source under the terms of the MIT License.

About

Piecewise Enumerables, Enumerators, and Lazy Enumerators for Ruby

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published