17-03-2018

A guide to Ruby in ten lines of code

This is a slightly unorthodox introduction guide to the Ruby language. Through this guide we will examine just ten one-line snippets of code, and discover a lot of features from each of them.

This is the post version of a talk I gave for LibreIM.

Syntax

puts "Hello #{gets.strip}!"

This snippet accepts a string from standard input, then cleans the leading and trailing spacing characters and embeds it in a greeting which is shown on standard output. Notice that:

  • puts and gets are the standard functions for displaying messages and reading input, respectively.
  • You can omit parenthesis around parameters for better readability.
  • #{...} in strings interprets small pieces of code and transforms the output into a string.
puts "boooored".upcase unless Time.now.saturday?

This example is so readable you probably don’t need a description. Notice that:

  • Literals are Ruby objects and can receive methods.
  • if, unless, while and until can be used as one-line structures.
  • There is a convention to end methods which return booleans with ? and methods which have non-explicit side effects with !.
real, imag = (1 + 3i).rectangular

This snippet assigns the real part of 1 + 3i to real and the imaginary part to imag. Notice that:

  • Ruby has built-in support for complex numbers, big numbers (as in symbolic, bigger than long long numbers) and symbolic rationals.
  • Assignment allows splatting arrays. The splat operator * allows for related operations. For example,
    x, *xs = [1, 2, 3, 4]
    

    performs some Haskell-ish pattern matching: x receives the first element and xs holds the rest of the array. There is also a similar double-splat ** for hashes.

Object oriented programming

class DeadPlayerError < StandardError; end

Defines a class useful to return errors (exceptions). Notice that:

  • Classes are defined with class (which is syntax sugar for Class.new { ... }).
  • There is simple inheritance with <.
  • You can throw an error with raise DeadPlayerError. Ruby distinguishes errors, from which a program can recover, and other kinds of exceptions, which leave the program in an invalid state and therefore it must stop.
Point = Struct.new :x, :y, :z

Creates a class with getters and setters for the given attributes. Notice that:

  • Member attributes of a class are always private (or protected, as they are inherited). Thus, the dot only sends methods and doesn’t directly access attributes.
  • Methods can be defined in a do ... end block. You should consider defining a class if you want to add them.
  • New objects are created with Point.new.

Iteration and data structures

puts %w[such elegant wow].each_with_index.map { |w,i| "#{i}. #{w}" }

Walks through the array, obtains an array of strings index. element and prints it on stdout. Notice that:

  • %w[] splits a list of words by spaces and returns an array.
  • each_with_index iterates giving each element and its index in the collection.
  • map applies a function on each element of a collection and returns the result.
  • {...} o do ... end denote blocks, structures of code which are passed to methods in order to be ran from them. Blocks can receive parameters between bars |a, b|.
dot = ->(v1, v2) { v1.zip(v2).reduce(0) { |p, (n, m)| p + n * m } }

Computes the dot product. Example usage: dot.([1, 2, 3], [-1, 0, 2]). Notice that:

  • Lambda functions are defined with ->. As opposed to usual functions or methods, Lambdas are objects.
  • zip pairs the elements of two or more arrays.
  • reduce accumulates results of a binary function.
fib = Hash.new { |h, i| h[i] = h[i - 2] + h[i - 1] }.update(0 => 0, 1 => 1)

Creates a Hash which contains, for each index, the corresponding term of the Fibonacci sequence. Notice that:

  • Hash may receive a default initialization (usually nil). This initialization takes place whenever the user attempts to access a value which has not been assigned previously.
  • Hash#update assigns several values at a time.
  • This would be equivalent to a memoized recursive version.
solution.neighborhood.detect { |attempt, fitness| fitness > @current_fitness }

Assuming the .neighborhood returns a collection of possible solutions and their performance (fitness), this finds the first one which improves the current solution. Actual use: https://git.io/vPxQ6. Notice that:

  • detect receives a predicate and returns the first ellement of the collection which verifies it.
  • If you want to get the best solution in the neighborhood instead, you can use max_by.
  • There is a huge list of iteration methods available in any Ruby collection class: Enumerable

I/O

open(DATA.read, "w").write IO.read($0).gsub(/^#' /, "")

This snippet reads itself (as in, the own program running), uncomments lines marked with #' and passes the result as input to the program started by Kernel#open. Notice that:

  • $0 is the name of the current program/script.
  • gsub performs global sustitution
  • Kernel#open opens read/write pipes with other processes when the “filename” looks like "|program_name".

The catch The program is hidden in the data section of the original script:

__END__
|pandoc -t beamer -o slides.pdf --pdf-engine=xelatex