• Stars
    star
    150
  • Rank 240,119 (Top 5 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 8 years ago
  • Updated over 7 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

The most complete implementation of an Elixir/F#-like "Pipe" for Ruby in the form of "chainable methods"

Chainable Methods

The Elixir language is great and within its many incredible features is the famous "Pipe Operator". Other popular functional languages like Haskell and F# sport a similar feature to chain method calls in a non-OO language.

It allows you to do constructs such as this:

require Integer
1..100_000
  |> Stream.map(&(&1 * 3))
  |> Stream.filter(&(Integer.is_odd(&1)))
  |> Enum.sum

In a nutshell, this is taking the previous returning value and automatically passing it as the first argument of the following function call, so it's sort of equivalent to do this:

require Integer
Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), &(Integer.is_odd(&1))))

This is how we would usually do it, but with the Pipe Operator it becomes incredibly more enjoyable and readable to work with and shifts our way of thinking into making small functions in linked chains. (By the way, this example comes straight from Elixir's Documentation)

(In F# it's even more important to make proper left-to-right type inference.)

Now, in the Ruby world, we would prefer to do it in a more Object Oriented fashion, with chained methods like this:

object.method_1.method_2(argument).method_3 { |x| do_something(x) }.method_4

This is how we do things with collections (Enumerables in general), and in Rails. For example, Arel coming into mind:

User.first.comments.where(created_at: 2.days.ago..Time.current).limit(5)

This pattern involves the methods returning a chainable Relation object and further methods changing the internal state of that object.

On the other hand, sometimes we would just want to be able to take adhoc returning objects and passing them ahead and isolating on the methods level instead of the objects level. There is a lot of existing discussions so the idea is not to vouch for one option or another.

In case you want to do the "semi-functional" way, we can do it like this:

Installation

Add this line to your application's Gemfile:

gem 'chainable_methods'

And then execute:

$ bundle

Or install it yourself as:

$ gem install chainable_methods

Usage

The easiest way and one of the most important purposes of this implementation in Ruby is to allow quick prototyping and better interface discovery. The gist of it is to think of "given an input data, what transformation steps should I follow to get to a certain output data?"

For example, given a text with links in it, how do I extract the links, parse them, fetch thee content of a link, parse the HTML, and finally get the title?

This is one such example:

include Nokogiri
CM("foo bar http://github.com/akitaonrails/chainable_methods foo bar")
  .URI.extract
  .first
  .URI.parse
  .HTTParty.get
  .HTML.parse
  .css("H1")
  .text
  .unwrap

And that's it!

Now, an important point is that I am NOT saying that this is a good state to leave your code, but it makes it easy to quickly prototype, test and reason about which parts should be encapsulated in a different method or even a different class.

The only other way to quickly prototype the same thing without Chainable Methods would be to use temporary variables which litter your code with dangerous variables that can be misused and make refactorings more difficult, for example:

sample_text = "foo bar http://github.com/akitaonrails/chainable_methods foo bar"
sample_link = URI.extract(sample_text).first
uri = URI.parse(sample_link)
response = HTTParty.get(uri)
doc = Nokogiri::HTML.parse(response)
title = doc.css("H1").text

Chaining feels way more natural. And in a pseudo-Elixir version it would be something like this:

"foo bar http://github.com/akitaonrails/chainable_methods foo bar"
  |> URI.extract
  |> List.first
  |> URI.parse
  |> HTTParty.get
  |> HTML.parse
  |> css("H1")

So we got similar levels of functionality without compromising the dot-notation and the Ruby style.

And we can advance further in other ways to use this chaining methods scheme to better organize functional-style coding:

# create your Module with composable 'functions'
module MyModule
  include ChainableMethods

  def method_a(current_state)
    # transform the state
    do_something(current_state)
  end

  def method_b(current_state, other_argument)
    do_something2(current_state, other_argument)
  end

  def method_c(current_state)
    yield(current_state)
  end
end

And now we can build something like this:

MyModule.
  chain_from(some_text).
  upcase. # this calls a method from the string in 'some_text'
  method_a.
  method_b("something").
  method_c { |current_state| do_something3(current_state) }.
  unwrap

And that's it. Again, this would be the equivalent of doing something more verbose like this:

a = some_text.upcase
b = MyModule.method_a(a)
c = MyModule.method_b(b, "something")
d = MyModule.method_c(c) { |c| do_something3(c) }

So we have this approach to create modules to serve as "namespaces" for collections of isolated and stateless functions, each being a step of some transformation workflow. A module will not hold any internal state and the methods will rely only on what the previous methods return.

Sometimes we have adhoc transformations. We usually have to store intermediate states as temporary variables like this:

text  = "hello http:///www.google.com world"
url   = URI.extract(text).first }
uri   = URI.parse(url)
body  = open(uri).read
title = Nokogiri::HTML(body).css("h1").first.text.strip

Or now, we can just chain them together like this:

include Nokogiri
CM("hello http:///www.google.com world")
  .URI.extract.first
  .URI.parse
  .chain { |uri| open(uri).read }
  .HTML.parse
  .css("h1")
  .first.text.strip
  .unwrap

I think this is way neater :-) And as a bonus it's also easier to refactor and change the order of the steps or add new steps in-between.

If you don't have a neat "Module.method" format to chain, you can use the #chain call to add transformations from anywhere and keep chaining methods from the returning objects as well, in the same mix, and without those ugly dangling variables.

The shortcut CM(state, context) will wrap the initial state and optionally provide a module as a context upon which to call the chained methods. Without you declaring this context, the chained methods will run the initial state object's methods.

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.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/akitaonrails/chainable_methods. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

CHANGELOG

v0.1.0

  • initial version

v0.1.1

  • introduces the ability to wrap any plain ruby object, without the need for a special module to extend the ChainableMethods module first.
  • fixes the priority of methods to call if both state and context has the same method, context always has precedence

v0.1.2

  • introduces a shortcut global method 'CM' to be used like this:
CM(2, ['a', 'b', 'c'])
  .[]
  .upcase
  .unwrap
# => "C"

v0.1.3

  • introduces the #chain method do link blocks of code together, the results are wrapped in the Link object and chained again

v0.1.4

  • makes the ChainableMethods module "includable" and it automatically makes all instance methods of the parent Module as class methods that can be easily chainable without having to declare all of them as def self.method first. So you can do it like this:

v0.2.1

  • use a const_get trick to allow to chain Module or Class names directly in the dot notation. Inspired by this gist. Kudos to @bkerley for the idea and letting me know.

License

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

More Repositories

1

akitando_episode_0118

examples to illustrate my YouTube channel's episode 118 about Databases
JavaScript
231
star
2

computer_languages_genealogy_graphs

Graphviz/Dot files generator for computer languages genealogy
Ruby
187
star
3

webdevbox

The Best Web Development Environment in a (Distro)Box
Lua
166
star
4

rinhabackend-rails-api

Ruby
158
star
5

plex-docker-compose

docker compose to run the containers in my local plex server
116
star
6

manga-downloadr

download mangas from MangaReader.net and compile Kindle optimised PDFs
Ruby
85
star
7

cramp_chat_demo

Bare bone chat demo using @lifo's Cramp
JavaScript
81
star
8

ex_manga_downloadr

Port of the Ruby version of Manga Downloadr to fetch mangas from MangaReader.net
Elixir
70
star
9

plex_home_server_docker

Docker Compose scripts for a Plex Media Server with pull capabilities with Sonarr, Radarr, etc.
70
star
10

dynamic_liquid_templates

Manage your templates in your database instead of view files, and with Liquid templating support.
JavaScript
58
star
11

rinhabackend-lucky-crystal-api

Crystal
48
star
12

ObjC_Rubyfication

Attempt to make Objective-C more like Ruby
C
35
star
13

dotfiles

Chezmoi managed dotfiles
Lua
34
star
14

third_rails

A fresh Rails 3.0 app with generators, Rack::Bug with Orchestra and Thorfile examples.
Ruby
32
star
15

rubyconf2011

Site da RubyConf Brasil 2011
Ruby
30
star
16

ASP_Classic_WebForm

A very old (2001) experiment on implement a ASP.NET Web Forms-like framework in ASP Classic
ASP
27
star
17

slack_cleanup

Elixir based command line tool to delete all files from your Slack account
Elixir
24
star
18

locarails

Configuracao de Capistrano automatica para hospedagens Linux Locaweb
Ruby
24
star
19

lw-pagto-certo

Gem que se comunica com o serviço "Pagamento Certo" da Locaweb
Ruby
22
star
20

fiberpool

Simulate a queue and a pool of job workers, in essence a "Fiber Pool" for Crystal
Crystal
22
star
21

cr_manga_downloadr

Second version of the Ruby version of my MangaReader.net crawler adapted for Crystal
Crystal
15
star
22

ryanb-15min-blog

Ryan Bates' new 15 min blog demo available at the official Ruby on Rails website
Ruby
14
star
23

episode0139

Example files described in the Episode 139, about Containers
JavaScript
14
star
24

elo_demo

small demonstration of how using Elo rating makes a lot of difference in rankings
Ruby
13
star
25

ex_messenger_exercise

Original from Drew Kerrigan
Elixir
12
star
26

gamegenie

simple exercise to decode Game Genie codes into the proper addresses and values
Crystal
11
star
27

rinhabackend-python-fastapi

Python
11
star
28

mygist

Ruby Gem to interface with Gist + command line to use it interactively
Ruby
11
star
29

vagrant-railsbox

Rails development environment for Vagrant + Berkshelf
Ruby
10
star
30

i3-dotfiles

Arch + i3 + nvim
Vim Script
9
star
31

static_site_demo

Example Rails 4.2 app implementing a fast static site that rivals pure static generators
Ruby
9
star
32

rolling_with_rails_tutorial

Support material for the Rolling with Rails Tutorial
Ruby
8
star
33

image_uploader_demo

Simple Demonstration of how to add S3 Direct Upload with Carrierwave Backgrounder
Ruby
8
star
34

answers_demo

answers.37signals.com clone
Ruby
8
star
35

code42coin-exercise

Leartning from: https://blog.zeppelin.solutions/how-to-create-token-and-initial-coin-offering-contracts-using-truffle-openzeppelin-1b7a5dae99b6
JavaScript
7
star
36

rinhabackend-nim-jester-api

Nim
7
star
37

cr_chainable_methods

Attempt at a (limited) Pipe Operator for Crystal
Crystal
7
star
38

typeractive-corne-zmk-config

7
star
39

ASP-Classic-XML-NoSQL

This is an example of how I made AJAX and NoSQL before the terms were even created
ASP
6
star
40

ex_pusher_lite

Implementation of a Phoenix Channel based core for a Pusher replacement
Elixir
5
star
41

cr_subtitle_shift

Port of my Shift-Subtitle Ruby script to Crystal
Crystal
5
star
42

udev-usb-controller

Scripts to enable hotplugging usb joysticks into a running Virsh VM
Shell
4
star
43

Shift-Subtitle

One example of solution for the Rubylearning Challenge #1
Ruby
4
star
44

depot

Sample application from "Agile Web Development with Rails."
Ruby
4
star
45

rust_ruby_exercise_1

Simple Rust exercise to parse a file and be consumed through Ruby/FFI
Rust
4
star
46

jruby_calculator_demo

JRuby Swing Calculator demonstration
Java
4
star
47

RailsConf-2010---Restfulie

Basic demonstration for the RailsConf 2010 talk about RESTful and Restfulie
Ruby
4
star
48

reveal.js-for-developers

Customization over vanilla Reveal.js to make it suitable for Software Developers
JavaScript
4
star
49

elixir-koans-results

Elixir
4
star
50

CoffeeScript-Demonstration

Small demonstration of coffeescript with jQuery UI in a Rails app
Ruby
4
star
51

TinyClone

Rails 3 version of the original Sinatra-based TinyClone
Ruby
4
star
52

wiki_exercicio

Pequeno exercício de Ruby on Rails - não é uma aplicação completa
Ruby
3
star
53

pusher_lite_demo

Basic Rails app to showcase the difference between using Pusher and a Phoenix-based replacement
Ruby
3
star
54

learn_to_program_pt_br

the original source for http://aprendaaprogramar.rubyonrails.pro.br/
Ruby
3
star
55

twitter_dumper

Ruby
3
star
56

screencast_demo

Projeto de demonstração para o Screencast do Git
Ruby
3
star
57

demo_responds_to_parent

Exercise With Responds To Parent
JavaScript
2
star
58

gem_exemplo

Gem de exemplo.
2
star
59

ObjC_XmlBuilder

Experimental clone of Ruby's Builder::XmlMarkup
Objective-C
2
star
60

locosxrails_i18n_demo

Demonstration I18n App for Locos x Rails conference
Ruby
2
star
61

restfulie-full-examples

Full examples on how to use restfulie
Ruby
2
star
62

enki-translator

Importers for enki from other blog engines
2
star
63

ObjC_OnigurumaDemo

Small app to demonstrate integration with ObjC_Rubyfication, particularly using Oniguruma
C
2
star
64

cr_slack_cleanup

Crystal version for my Elixir-based Slack clean up tool
Crystal
1
star
65

pxblog_phoenix_exercise

Elixir
1
star
66

fisl_10_demo

Demonstração da minha palestra no FISL 10 de 2009
Ruby
1
star