Ruby ist eine gute Programmiersprache und mein täglich Brot auf der Arbeit. Ich bin sehr zufrieden mit Rubys Features, dem Ökosystem und den Bibliotheken für Web-Entwicklung, jedoch vermisse ich aufgrund meiner früheren Experimente mit Python ein bisher noch nicht implementiertes Feature.

Die Problematik

Die folgenden Code-Schnipsel demonstrieren ein tiefergreifendes Problem:

[][0]
{}[:foobar]
[][0]
{}['foobar']

Ein weniger erfahrener Entwickler würde erwarten dass beide Beispiele eine Exception für den fehlerhaften Array- und Dictionary-/Hash-Zugriff werfen. Dies ist bei der Python-Variante der Fall, jedoch gibt die Ruby-Variante einfach nil in beiden Fällen zurück.

Während man argumentieren kann dass dieses Verhalten nützlich sein kann, halte ich es für eine mögliche Fehlerquelle des berüchtigten undefined method for nil:NilClass-Fehlers. Die offizielle Empfehlung ist die #fetch-Methode für Exceptions bei fehlerhaftem Zugriff auf Arrays und Hashes zu erhalten. Ich könnte natürlich dieser Empfehlung nachgehen und bei jeder Stelle im Code prüfen welche Verhaltensweise besser ist, aber was wenn ich mir stattdessen ein Feature à la JavaScripts use strict wünsche?

Die Lösung

Es stellt sich heraus, dass es erstaunlich einfach ist Ruby dank seiner ausgezeichneten Metaprogramming-Fähigkeiten dieses Verhalten beizubringen. Die übliche Herangehensweise ist das "öffnen" von Klassen und selektives Verändern ihrer Methoden, jedoch ist dieser Ansatz problematisch da er global angewendet wird und mit Code der sich auf das alte Verhalten lässt (oder anderem Code welcher die gleichen Klassen manipuliert) kollidieren kann.

Aus diesem Grund werde ich stattdessen das "Refinements"-Feature ausprobieren welches in Ruby 2.1.0 eingeführt wurde. Es erlaubt einem das Verhalten bestehender Klassen zu verändern und die Manipulation in jedem beliebigen Scope zu aktivieren. Der andere fehlende Baustein für diese technische Demo ist alias_method, ein Weg Klassenmethoden Aliase zu vergeben. Der gewünschte Effekt kann einfach erzielt werden indem man einen Alias von :fetch auf :[] erzeugt:

autoload :ItsATrap, 'its_a_trap'

module ItsATrap
  refine Array do
    alias_method :[], :fetch
  end

  refine Hash do
    alias_method :[], :fetch
  end
end

An dieser Stelle fehlt nur noch das Modul als Ruby Gem zu paketieren. Die offizielle Anleitung empfiehlt folgende Dateistruktur:

.
├── its_a_trap.gemspec
└── lib
    └── its_a_trap.rb

Hier noch die fehlende Gemspec-Datei:

Gem::Specification.new do |s|
  s.name        = 'its_a_trap'
  s.version     = '0.0.0'
  s.date        = '2018-02-15'
  s.summary     = 'It\'s a trap!'
  s.description = 'Strict mode for your Ruby files'
  s.author      = 'Vasilij Schneidermann'
  s.email       = 'v.schneidermann@gmail.com'
  s.files       = ['lib/its_a_trap.rb']
  s.homepage    = 'https://github.com/wasamasa/its_a_trap'
  s.license     = 'MIT'
end

gem build its_a_trap.gemspec; gem install its_a_trap-0.0.0.gem installiert das so erzeugte Gem lokal, auf diese Weise kann es in eigenem Code wie folgt verwendet werden:

require 'its_a_trap'
using ItsATrap

[][0]
{}[:foobar]

Monkeypatching in Ruby - Geschafft!

Und das war es auch schon! Durch das Befolgen dieser Schritte erhält man eine Bibliothek die für Debugging-Zwecke selektiv aktiviert und in Release-Code wieder deaktiviert werden kann.