Monday, August 1, 2016

The Pros and Cons of Ruby Refinements

In Ruby 2.0.0, refinements were introduced as a new feature. Monkey patching has been used for a long time for modifying code behavior, but it creates side effects in code elsewhere that ends up affected by the modified code.
The purpose of Ruby refinements is to provide a solution by scoping changed behavior to the very specific area of code you choose. This is a huge blessing for the language, but the current implementation of refinements has its flaws. Still, everyone should be using refinements to create a more sane code base and not create surprise side effects in your code base. In this post, I’ll go into detail on how refinements can be used, as well as discuss many of the issues you’ll face with its as-yet-incomplete design.

How to Use Refinements

First, you need to define the behavior you wish to change by defining a module and declaring which class(es) you will be refining.

module ListStrings
  refine String do
    def +(other)
      "#{self}, #{other}"
    end
  end
end

To use the behavior, simply use using ListStrings in one of several areas.

using ListStrings
'4' + '5'
# => "4, 5"
You can use the refinement at the top level of your code, and all code written below that in the current file will be affected. You may also use it within eval statements.

eval "using ListStrings; '4' + '5'"
# => "4, 5"
As of Ruby 2.3, these won’t work in IRB but will still work in your Ruby files.
The use of using is lexically scoped. When used at the top level of your code, its affects are scoped only to the current file from that point on to the end and not in any other code you require from other files.
As of Ruby 2.3, you may use refinements within classes.

module Foo
  refine Fixnum do
    def to_s
      :foo
    end
  end
end

class NumAsString
  def num(input)
    input.to_s
  end
end

class NumAsFoo
  using Foo
  def num(input)
    input.to_s
  end
end

NumAsString.new.num(4)
# => "4" 
NumAsFoo.new.num(4)
# => :foo

This creates a very safe and sane way to scope changed behavior for existing code.

Current Issues with Ruby Refinements

As with most relatively young features, refinements still has some flaws. Let’s unpack where the trouble is.

Refinements can only be used once within a scope

If you’re benchmarking alternative ways and using multiple refinements on the same method name, then consecutive runs will fail to reuse used refinements. While this may not be a likely scenario, it’s good to keep this limitation in mind.

Dynamic dispatch won’t work with refinements yet

Dynamic dispatch happens when you use the proc form of mapping method calls, like [1,2,3].map(&:to_s). Here’s an example of how it won’t work with refinements:

module Moo
  refine Fixnum do
    def to_s
      "moo"
    end
  end
end

class A
  using Moo
  def a
    [1,2,3].map {|x| x.to_s }
  end

  def b
    [1,2,3].map(&:to_s)
  end
end

A.new.a
# => ["moo", "moo", "moo"] 
A.new.b
# => ["1", "2", "3"]

If you had redefined the to_s method as a monkey patch, then both method calls would have returned lists of “moo”.

You cannot use methods for introspection

The Ruby documentation says, “When using indirect method access such as Kernel#sendKernel#method, or Kernel#respond_to?, refinements are not honored for the caller context during method lookup. This behavior may be changed in the future.”

module Baz
  refine String do
    def baz
      :baz
    end
  end
end

class Venue
  using Baz
  def respond?
    "".respond_to? :baz
  end

  def call
    "".baz
  end
end

Venue.new.respond?
# => false 
Venue.new.call
# => :baz

This makes determining whether code has been changed more difficult if you’re not looking at the source code yourself. It might be worthwhile to add something like an instance variable to your refinement if you need to dynamically determine what refinements are in play.

module Bar
  def refinements
    require 'set'
    @refinements = (@refinements || Set.new) << Bar
  end

  refine String do
    def bar
      :bar
    end
  end
end

class A
  include Bar and using Bar
  def a
    "".bar
  end
end

A.new.refinements
# => #<Set: {Bar}> 
A.new.a
# => :bar

You can decide for yourself how important introspection is for you; it would be quite handy when debugging. You may also choose to give more details for your introspection method than the example given here.

You can’t use usingfrom within a method or module

This requires you to very explicitly declare using within the lexical scope where you’ve choosen to modify the codes behavior. This is good for the sanity of all future developers who’ll look at the code and see that there is modified behavior.

You’re unable to experiment with refinements at the top level

As I mentioned earlier, the behavior in IRB changed at 2.3, limiting the ways you can experiment with refinements. To experiment with it in IRB with Ruby 2.3 and later, you may do so with anonymous classes.

module Fiz
  refine Fixnum do
    def +(other)
      "#{self}#{other}"
    end
  end
end

class << Class.new
  using Fiz
  1 + 2
end
# => "12"

Summary

Refinements are still in their infancy in Ruby, and improvements are very likely to be made to them. However, current development on refinements seems to be on hold, as Ruby development focuses on optimizing Ruby 3 to be three times faster than Ruby 2. As a result, refinement issues raised on the bug tracker aren’t receiving much attention.
Regardless of all the issues that exist with refinements, they are still a must for developers. With them, you can help ensure a code base that clearly shows alternative behavior while ensuring safe cohesion with larger code bases. You can worry less about name collision, side effects, and hard-to-track bugs when you contain your changes with refinements.

Refinements become most important any time you’re making modifications to existing methods on core classes in Ruby or any other widely used code base. I have tried monkey patching in the past to write a gem in the past that would have a String object behave like an Array, but when I included it in a Rails project, it all came crashing down. Many applications may use methods like respond_to? to type check and perform different actions based on type. So of course making strings respond as arrays will break things globally. Now with refinements, you don’t have to worry about this — the changes are specifically only effective within the file, eval string, or class you used the refinement in.
Don’t rely on the lack of introspection in refinements as a feature. This will likely change in the future. If you use a refinement to have a method respond to a specific method call, and you type-check against that knowing it won’t show up, you’d have created a “bug waiting to happen.”
I for one really look forward to future development in Ruby around refinements to get past the current issues and move on to a more mature code use for all our benefit. And the biggest thing that will help refinements move further along in the development process is for more people to use it. The more people who use it, the more attention it will get, which will lead to more focus in developing it within the language.
Written by: Daniel P. Clark

If you feel useful for you and for everyone, please support and follow us.
Suggest for you:

The Complete Ruby on Rails Developer Course

Learn Ruby on Rails from Scratch

Ruby Scripting for Software Testers

Python, Ruby, Shell - Scripting for Beginner

How to use PHP solarium in a Laravel project

SOLR Instance

You have a running SOLR instance somewhere, if you don't have a running SOLR instance then please follow this guide to install one on a DigitalOcean droplet - Install SOLR on Ubuntu.

We will integrate the code of the first blogpost from Multi-Select facets with SOLR in a new Laravel project.

Lets start with a fresh Laravel project

laravel new solarium
If you are starting fresh with Laravel and you have an OSX machine, I would suggest you start with my Starting with Laravel Valet on OSX tutorial to setup a quick local development environment. You can test your new Laravel project in the browser at the url: http://solarium.dev/.

Add a solarium configuration


The Client of solarium library uses a injection of a configuration array. Make a new file named solarium.php in the config folder and add the following content to make this configuration array.
<?php

return [
    'endpoint' => [
        'localhost' => [
            'host' => 
 env('SOLR_HOST', '127.0.0.1'),
            'port' => 
 env('SOLR_PORT', '8983'),
            'path' =>
 env('SOLR_PATH', '/solr/'),
            'core' =>
 env('SOLR_CORE', 'collection1')
        ]
    ]
];
This configuration assumes that the SOLR is installed on the local machine, uses the default port of 8983, the default solr install path and with a default collection named collection1.
You can override every configuration setting within the .env file in your projectfolder. If you installed SOLR with the tutorial you can add the IP of the DigitalOcean droplet by adding this to .env file.
SOLR_HOST=99.9.9.999
Where 99.9.9.999 is the IP of your droplet. If you want to know more about environment configuration please look at the guide on the Laravel site for the official environment configuration.

Install the PHP Solarium library

This library is the bread and butter for SOLR usage within PHP. This can be quickly installed by composer using the following command.
composer require solarium/solarium
At this time of writing it will install the version of 3.6.

Lets boot up the Solarium Client

Within Laravel service providers are the central place of all the applications bootstrapping. Bootstrapping is nothing more than registering things, including registering service container bindings, event listeners, middleware, and even routes. Service providers are the central place to configure your application. So lets make a service provider for registering the Solarium Client with our created solarium configuration file.
php artisan make:provider SolariumServiceProvider
This will create the file SolariumServiceProvider.php within your app/Providers folder. Open this file in your favourite editor and add the following contents:
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Solarium\Client;

class SolariumServiceProvider extends ServiceProvider
{
    protected $defer = true;

    /**
     * Register any application services.
     *
     * @return  void
     */
    public function register()
    {
        $this->app->bind(Client::class, function ($app) {
            return new Client($app['config']['solarium']);
        });
    }

    public function provides()
    {
        return [Client::class];
    }
}
Now open up the file app/config/app.php and add the new SolariumServiceProvider to the other application providers.
return [
    'providers' => [
        // List off others providers...
        App\Providers\SolariumServiceProvider::class,
    ]
];
Now every time you instantiate the Solarium\Client within your application it will inject the environment configuration. Notice the $defer setting, this setting makes sure that its not loaded at every request but only when you use the Client within your application. This will make you application faster if you don't always need the Client. If your application needs it with every request remove that line from the service provider.

Testing the integration

How can we test our Laravel application with SOLR that everything is setup correctly for querying SOLR? Well, SOLR has a built in ping feature so you can easily monitor your SOLR instance if its up or down. So lets create a SolariumController with the Ping feature.
php artisan make:controller SolariumController
This will add the Controller to the file app/Http/Controllers/SolariumController.php. Open this file in your favourite editor and add the following content.
<?php

namespace App\Http\Controllers;

class SolariumController extends Controller
{
    protected $client;

    public function __construct(\Solarium\Client $client)
    {
        $this->client = $client;
    }

    public function ping()
    {
        // create a ping query
        $ping = $this->client->createPing();

        // execute the ping query
        try {
            $this->client->ping($ping);
            return response()->json('OK');
        } catch (\Solarium\Exception $e) {
            return response()->json('ERROR', 500);
        }
    }
}
Now lets create a ping route within the routes file. You can find the routes file at the default location app\Http\routes.php.
Route::get('/ping', 'SolariumController@ping');
Now if you go the the URL of http://solarium.dev/ping it should return a string with the text 'OK'. If it returns the string 'ERROR' please check the IP, path and the name of the collection of the SOLR instance.

Now what is happening here?

In the constructor we use the IoC binding for the solarium client. Our created SolariumServiceProvider uses its binding and injects our environment configuration to the $client variable of the controller.

You can now use $this->client in the whole controller for tinkering with the Solarium library.

Using the Multi-Select facets from our first blog

Now that we have a connection to our running SOLR instance. We can now test our Multi-Select facets with SOLR code by updating the Controller with the following content.
<?php

namespace App\Http\Controllers;

class SolariumController extends Controller
{
    protected $client;

    public function __construct(\Solarium\Client $client)
    {
        $this->client = $client;
    }

    public function ping()
    {
        // create a ping query
        $ping = $this->client->createPing();

        // execute the ping query
        try {
            $this->client->ping($ping);
            return response()->json('OK');
        } catch (\Solarium\Exception $e) {
            return response()->json('ERROR', 500);
        }
    }

    public function search()
    {
        $query = $client->createSelect();
        $query->addFilterQuery(array('key'=>'provence', 'query'=>'provence:Groningen', 'tag'=>'include'));
        $query->addFilterQuery(array('key'=>'degree', 'query'=>'degree:MBO', 'tag'=>'exclude'));
        $facets = $query->getFacetSet();
        $facets->createFacetField(array('field'=>'degree', 'exclude'=>'exclude'));
        $resulset = $client->select($query);

        // display the total number of documents found by solr
        echo 'NumFound: ' . $resultset->getNumFound();

        // show documents using the resultset iterator
        foreach ($resultset as $document) {

            echo '<hr/><table>';

            // the documents are also iterable, to get all fields
            foreach ($document as $field => $value) {
                // this converts multivalue fields to a comma-separated string
                if (is_array($value)) {
                    $value = implode(', ', $value);
                }

                echo '<tr><th>' . $field . '</th><td>' . $value . '</td></tr>';
            }

            echo '</table>';
        }
    }
}
Now add the search router to routes file. You can find the routes file at the default location app\Http\routes.php.
Route::get('/search', 'SolariumController@search');
You are probably getting an error, this is because I already have a running instance with the fields and it is just for showing how easy it is to add the code from my first blog for using it into a Laravel project.

If you want to test SOLR search function with a clean install just removed these rows from the search function.
// Remove these rows
$query->addFilterQuery(array('key'=>'provence', 'query'=>'provence:Groningen', 'tag'=>'include'));
$query->addFilterQuery(array('key'=>'degree', 'query'=>'degree:MBO', 'tag'=>'exclude'));
$facets = $query->getFacetSet();
$facets->createFacetField(array('field'=>'degree', 'exclude'=>'exclude'));
Now if you go the the URL of http://solarium.dev/search, It should return numFound: 0 in your browser.

In my next blog items I will try to explain how to create a custom schema.xml file to add your own fields to the SOLR instance and for searching in special fields like Spatial Search for finding results near a given location.
Written by: Peter Steenbergen

For more information , please support and follow us.
Suggest for you:

Learn PHP 7 This Way to Rise Above & Beyond Competion!

PHP MySQL Database Connections

The Complete PHP with MySQL Developer Course (New)

Learning PHP 7: From the Basics to Application Development

The Complete PHP 7 Guide for Web Developers