Austin.RB

Ruby to Elixir

2015-08-03

Luke Imhoff

luke_imhoff@rapid7.com Kronic.Deth@gmail.com
@limhoff-r7 @KronicDeth
@KronicDeth

Outline

  1. Overview
  2. Installation
  3. Interactive
  4. Types
  5. Control Flow
  6. Pattern Matching
  7. Project
  8. Code
  9. Testing
  10. Metaprogramming
  11. Concurrency
  12. Resources
  13. Upcoming Dates

Overview

Ruby Elixir
Paradigms Imperative
Concurrent
Functional
Object-Oriented
Typing Dynamic
Duck
Strong
Mutability Mutable Immutable
Concurrency CPU-bound OS Processes VM Processes
IO-bound Threads, Fibers VM Processes
Metaprogramming Runtime, Class Methods Compilation, Macros

Installation

OSX

Ruby Elixir
Homebrew brew install ruby brew install elixir
Version Manager rvm install VERSION kiex install VERSION

Windows

Ruby Elixir
Installer rubyinstaller.exe elixir-websetup.exe
Chocolatey cinst ruby cinst elixir

Linux

Use your package manager

Interactive

Starting Interactive

Ruby Elixir
irb iex

Breaking the current command

Ruby Elixir
CTRL+C #iex:break on line by itself
Ruby Elixir
exit CTRL+C CTRL+C

Types

Numeric Types

Ruby Elixir
Name Example Name Example
Integer 9, 0b1, 0o7, 0xF Integer 9, 0b1, 0o7, 0xF
Float 1.2, 3e+0 float 1.2, 3e+0

Constant Types

Ruby Elixir
Name Example Name Example
Symbol :symbol, :"symbol", :'symbol' Atom :atom, :"atom", :'atom'
Class/Module name MyClass, MyNamespace::MyModule Alias MyModule, MyNamespace.MyModule, :erlang_module
Constant MY_CONSTANT Module Attribute @my_attribute

Boolean Types

Ruby Elixir
false false, :false
nil nil, :nil
true true, :true

Strings

Ruby Elixir
Format "string" "string"
Interpolation "Hello #{:world}" "Hello #{:world}"
Encoding UTF-8 UTF-8
Unicode Capitalization "José Valim".upcase # "JOSé VALIM" String.upcase "José Valim" # "JOSÉ VALIM"
Unicode Graphemes Rendering "\u0065\u0301" # "é" "\x{0065}\x{0301}" # "é"
Unicode Graphemes Length "\u0065\u0301".length # 2 String.length "\x{0065}\x{0301}" # 1

Regular Expressions

Ruby Elixir
Literals
  • /[A-Z]+/
  • %r{[A-Z]+}
  • ~r{[A-Z]+}
  • ~r[(.*)]
  • ~r<[A-Z]+>
  • ~r"[A-Z]+"
  • ~r/[A-Z]+/
  • ~r([A-Z]+)
  • ~r|[A-Z]+|
  • ~r'[A-Z]+'
Compile Regexp.new "string" Regexp.compile! "string"
Replace

'`spec` is a task for `rake`'.gsub(
  /`(.*?)`/,
  '<code>\1</code>'
) # "<code>test</code> is a task for <code>rake</code>"
                                  

Regex.replace(
  ~r/`(.*?)`/,
  "`test` is a task for `mix`",
  "<code>\\1</code>"
) # <code>test</code> is a task for <code>mix</code>"
                                    

Anonymous Functions

Ruby Elixir
Declaration
  • add = ->(a,b){ a + b }
  • add = lambda { |a,b| a + b }
  • add = proc { |a,b| a + b }
  • add = Proc.new { |a, b| a + b}
  • add = fn a, b -> a + b end
  • add = fn (a, b) -> a + b end
  • add = &(&1 + &2)
Calling
  • add.call(1,2)
  • add.call 1, 2
  • add.(1,2)
  • add[1,2]
  • add.(1,2)

Collections

Ruby Elixir
Name Example Name Example
Array [1,2,3] Tuple [1,2,3]
Hash
  • {a: 1, b: 2}
  • {:a => 1, :b => 2}
Map
  • %{a: 1, b: 2}
  • %{:a => 1, :b => 2}
Set Set.new [1,2,3] HashSet Enum.into [1,2,3], HashSet.new
Linked List
  • [1,2,3]
  • [1 | [2 | [3 | []]]]
Keyword List
  • [a: 1, a: 2, b: 3]
  • [{:a, 1}, {:a, 2}, {:b, 3}]

Control Flow

Boolean Control Flow

Ruby Elixir

if false
  'This will never be seen'
else
  'This will'
end
                                    

if false do
  "This will never be seen"
else
  "This will"
end
                                    

unless true
  'This will never be seen'
else
  'This will'
end
                                    

unless true do
  "This will never be seen"
else
  "This will"
end
                                    

if one
  'one is true'
elsif two
  'two is true'
else
  'neither one nor two is true'
end
                                    

cond do
  one -> "one is true"
  two -> "two is true"
  true -> "neight one nor two is true"
end
                                    

Rescuing Exceptions

Ruby Elixir

begin
  raise 'some error'
rescue RuntimeError => runtime_error
  puts runtime_error
rescue ArgumentError
  puts 'argument error occurred'
rescue => exception
  puts exception
rescue
  puts 'some exception'
else
  puts 'no exception'
ensure
  puts 'always runs'
end
                                  

try do
  raise "some error"
rescue
  x in [RuntimeError] ->
    IO.puts x.message
  ArgumentError ->
    IO.puts "argument error occurred"
  error ->
    IO.puts error
  _ ->
    IO.puts "some error"
else
  IO.puts "no error"
after
  puts "always run"
end
                                    
begin try do
rescue Klass => instance variable in [Alias] ->
rescue Klass Alias ->
rescue => exception error ->
rescue _ ->
else else
ensure after
end end

Catching Throws And Exits

Ruby Elixir

answer = nil
catch (:done) do
  answer = 42
  throw :done
end
                                    

try do
  name = "Alice"
  throw("Hello", name)
  exit "I am exiting"
catch
  {greeting, name} ->
    IO.puts "#{greeting} to you, #{name}"
  :exit, _ -> "not really"
after
  IO.puts "Nothing thrown"
end
                                    

Pattern Matching

Ruby Assignment to Elixir Matching

Step Ruby Elixir
1 foo = 1 foo is 1 foo is 1
2 1 = foo SyntaxError foo is 1
3 2 = foo SyntaxError ** (MatchError) no match of right hand side value: 1

Destructuring to Match

Ruby Elixir
Expression Variable Value(s) Expression Variable Value(s)
a, b = [1, 2] a = 1
b = 2
{a, b} = {1, 2} a = 1
b = 2
_, b = [1, 2] b = 2 {_, b} = {1, 2} b = 2
a, b* = [1, 2, 3] a = 1
b = [2, 3]
[a | b] = [1, 2, 3] a = 1
b = [2, 3]

ArgumentError to Match

Ruby Elixir
Expression Variable Value(s) Expression Variable Value(s)

a, b = [nil, 2]
unless a == 1
  raise ArgumentError,
        "a should be 1"
end
                                    
ArgumentError: a should be 1

{1, b} = {0, 2}
                                    
** (MatchError) no match of right hand side value: {nil, 2}

opening, closing = [:td, :th]
unless opening == closing
  raise ArgumentError,
        "opening and closing tag don't match"
end
                                    
ArgumentError: opening and closing tag don't match

{tag, tag} = {:td, :th}
                                    
* (MatchError) no match of right hand side value: {:td, :th}

Function Clauses

Ruby Elixir

def cat_greet(who)
  case who
  when :owner
    'Purr!'
  when :dog
    'Hiss!'
  else
    '*ignore*'
end
cat_greet(:owner) # "Purr!"
cat_greet(:dog) # "Hiss!"
cat_greet(:sitter) # "*ignore*"
                                    

cat_greet = fn
  :owner -> "Purr!"
  :dog -> "Hiss!"
  - -> "*ignore*"
end

cat_greet.(:owner) # "Purr!"
cat_greet.(:dog) # "Hiss!"
cat_greet.(:sitter) # "*ignore*"
                                    

If-else to case

Ruby Elixir

fizz = n % 3 == 0
buzz = n % 5 == 0

if fizz && buzz
  'FizzBuzz'
elsif fizz
  'Fizz'
elsif buzz
  'Buzz'
else
  n
end
                                

case {rem(n, 3), rem(n, 5), n} do
  {0, 0, _} -> "FizzBuzz"
  {0, _, _} -> "Fizz"
  {_, 0, _) -> "Buzz"
  {_, _, n} -> n
end
                                

Project

Tools

Ruby Elixir Ruby Elixir
gem list mix archive bundle help mix help
gem build *.gemspec mix archive.build gem help mix help
gem install *.gem mix archive.install rake -T mix help
gem uninstall NAME mix archive.uninstall NAME bundle outdated mix hex.outdated
rm *.gem mix clean gem owner mix hex.owner
bundle list mix deps gem push mix hex.publish
bundle install mix deps.get gem query mix hex.search
rm Gemfile.lock mix deps.unlock --all bundle gem mix new
bundle update mix deps.update --all rake spec mix test
rake TASK1 TASK2 mix do TASK1 TASK2

Ruby Elixir
gem install bundler
bundle gem example --coc --mit --test=rspec
mix new example

File Layout

Ruby Elixir
example.gemspec mix.exs
.gitignore .gitignore
Gemfile mix.exs
lib/example.rb lib/example.ex
lib/example/version.rb mix.exs
README.md README.md
spec/spec_helper.rb test/test_helper.exs
spec/example_spec.rb test/example_test.exs

Packaging

Ruby Elixir

# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'example/version'

Gem::Specification.new do |spec|
  spec.name          = "example"
  spec.version       = Example::VERSION
  spec.authors       = ["Luke Imhoff"]
  spec.email         = ["luke_imhoff@rapid7.com"]

  spec.summary       = %q{TODO: Write a short summary, because Rubygems requires one.}
  spec.description   = %q{TODO: Write a longer description or delete this line.}
  spec.homepage      = "TODO: Put your gem's website or public repo URL here."
  spec.license       = "MIT"

  spec.files         = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
  spec.bindir        = "exe"
  spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
  spec.require_paths = ["lib"]

  spec.add_development_dependency "bundler", "~> 1.10"
  spec.add_development_dependency "rake", "~> 10.0"
  spec.add_development_dependency "rspec"
end
                                    

defmodule Example.Mixfile do
  use Mix.Project

  def project do
    [app: :example,
     version: "0.0.1",
     elixir: "~> 1.0",
     deps: deps]
  end

  def application do
    [applications: [:logger]]
  end

  defp deps do
    []
  end
end
                                    

Dependency - Source

Source Ruby Elixir
*.gemspec Gemfile mix.exs
Packager spec.add_runtime_dependency 'mydep', '~> 1.2.3' gem 'mydep', '~> 1.2.3' {:mydep, "~> 0.3.0"}*
Github gem 'mydep', github: 'myorg/mydep', tag: 'v1.2.3' {:mydep, github: 'myorg/mydep', tag: "v1.2.3"}
Path gem 'mydep', path: 'path/to/mydep' {:mydep, path: "path/to/mydep"

*All mix dependencies are added to the [] in Example.MixFile.deps/0

Dependency - Environment

Environment Ruby Elixir
*.gemspec Gemfile mix.exs
development spec.add_development_dependency 'mydep', '~> 1.2.3' gem 'mydep', '~> 1.2.3', group: :development {:mydep, '~> 1.2.3', only: :dev}
test &10060; gem 'mydep', '~> 1.2.3', group: :test {:mydep, '~> 1.2.3', only: :test}

*All mix dependencies are added to the [] in Example.MixFile.deps/0

Dependency - Advanced Features

Name Ruby Elixir
Optional group optional: true {:mydep, '1.2.3', optional: true}
Override {:mydep, '1.2.3', override: true}

Code

Module

Ruby Elixir

module Math
  def self.sum(a, b)
    a + b
  end

  def self.square(x) do
    x * x
  end
end

Math.sum(1, 2) # 3
Math.square 3 # 9
                                    

defmodule Math do
  def sum(a, b) do
    a + b
  end

  def square(x) do
    x * x
  end
end

Math.sum(1, 2) # 3
Math.square 3 # 9
                                    

Writing Documentation

Ruby Elixir

# Simple math functions
module Math
  # Sums two numbers.
  #
  # @param a [Number]
  # @param b [Number]
  # @return [Number] Sum of `a` and `b`
  def self.sum(a, b)
    a + b
  end

  # Multiplies `x` by itself.
  #
  # @param x [Number]
  # @return [Number]
  def self.square(x) do
    x * x
  end
end
                                    

defmodule Math do
  @moduledoc "Simple math functions"

  @doc "Sums two numbers."
  @spec sum(number, number) :: number
  def sum(a, b) do
    a + b
  end

  @doc "Multiplies `x` by itself."
  @spec square(number) :: number
  def square(x) do
    x * x
  end
end
                                    

Reading Documentation

Function Overloading

Ruby Elixir

class Rectangle
  attr_reader :height, :width

  def initialize(width, height)
    @width = width
    @height = height
  end

  def area
    width * height
  end
end

Rectangle.new(2, 3).area # 6
                                    

class Circle
  attr_reader :radius

  def initialize(radius)
    @radius = radius
  end

  def area
    Math::PI * radius * radius
  end
end

Circle.new(3).area # 28.274333882308138
                                    

defmodule Geometry do
  def area({:rectangle, width, height}) do
    width * height
  end

  def area({:circle, radius}) when is_number(radius) do
    :math.pi * radius * radius
  end
end

Geometry.area({:rectangle, 2, 3}) # 6
Geometry.area({:circle, 3}) # 28.274333882308138
                                    

Struct


defmodule Rectangle do
  defstruct [:height, :width]

  def area(%__MODULE__{height: height, width: width}) do
    height * width
  end
end

defmodule Circle do
  defstruct [:radius]

  def area(%__MODULE__{radius: radius}) do
    :math.pi * radius * radius
  end
end

rectangle = %Rectangle{height: 3, width: 2}
Rectangle.area(rectangle) # 6
circle = %Circle{radius: 3}
Circle.area(circle) # 28.274333882308138
Circle.area(rectangle) # ** (FunctionClauseError) no function clause matching in Circle.area/1
                        

Protocol

Protocol Implementation
Inside Outside

Usage

rectangle = %Rectangle{height: 3, width: 2}
circle = %Circle{radius: 3}
Shape.area(rectangle) # 6
Shape.area(circle) # 28.274333882308138
                            

Recursion

Ruby Elixir

def sum_array(array, acc)
  if array.empty?
    acc
  else
    head, *tail = array
    sum_array(tail, acc + head)
  end
end

sum_array((0..9000).to_a, 0) # SystemStackError: stack level too deep
                                    

defmodule Recursion do
  def sum_list([head | tail], acc) do
    sum_list(tail, acc + head)
  end

  def sum_list([], acc) do
    acc
  end
end

0..9000 |> Enum.to_list |> Recursion.sum_list(0) # 40504500
                                    

Testing

Frameworks

Ruby Elixir Usage
minitest/unit ex_unit assertion based unit testing
shouldi Nested contexts for ex_unit
faker faker Fake data for tests
rspec espec BDD tests with expect, let, callbacks
cucumber white_bread Story-based BDD using gherkin syntax
Ruby Elixir
spec/spec_helper.rb test/test_helper.exs

RSpec.configure do |config|
  config.expect_with :rspec do |expectations|
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
  end

  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end

  config.filter_run :focus
  config.run_all_when_everything_filtered = true
  config.example_status_persistence_file_path = "spec/examples.txt"
  config.disable_monkey_patching!
  config.warnings = true

  if config.files_to_run.one?
    config.default_formatter = 'doc'
  end

  config.profile_examples = 10
  config.order = :random
  Kernel.srand config.seed
end
                                    

ExUnit.start()
                                    

Test File

Ruby Elixir
spec/example_spec.rb test/example_test.exs

RSpec.describe Example do
  it 'does not break addition' do
    expect(
      1 + 1
    ).to eq(3)
  end

  it 'does not break subtraction' do
    expect(
      1 - 1
    ).to eq(-1)
  end
end
                                    

defmodule ExampleTest do
  use ExUnit.Case,
      async: true

  test "addition" do
    assert 1 + 1 ==
           3
  end

  test "subtraction" do
    assert 1 - 1 ==
           -1
  end
end
                                    

Test Output

Ruby Elixir
rake spec mix test

Test Coverage

Type Ruby Elixir
Built-in (no builtin reporting from Coverage library)

mix test --cover
open coverage/*.html
                                    
Coverage report with ignores

# Gemfile
gem 'simplecov', group: :test, require: false

# spec/spec_helper.rb
require 'simplecov'
SimpleCov.start
                                    

rake spec
open coverage/index.html
                                    

# mix.exs
  def project do
    [ #...
      test_coverage: [tool: ExCoveralls]
    ]
  end

  def deps do
    [{:excoveralls, only: :test}]
  end

                                    

mix test
mix coveralls.detail
                                     
Upload coverage reports
to Coveralls.io
from Travis-CI.org

# Gemfile
gem 'coveralls', group: :test, require: false

# spec/spec_helper.rb
require 'coveralls'
Coveralls.wear!
                                    

rake spec
                                    

# mix.exs
  def project do
    [ #...
      test_coverage: [tool: ExCoveralls]
    ]
  end

  def deps do
    [{:excoveralls, only: :test}]
  end
                                    

mix coveralls.travis
                                    

Testing Documentation Examples

Code Commands

# lib/example.rb
defmodule Example
  @doc """
  Calculates line number

  ## Examples

      iex> Example.line(1, 2)
      3

  """
  def line(start, offset) do
    # correct for lines being 1-index to user
    start + offset + 1
  end
end
                                    

# test/example_test.ex
defmodule ExampleTest do
  use ExUnit.Case, async: true
  doctest Example
end
                                    

mix test
                                    

Metaprogramming

Macros

  • Run at compile
  • Any valid Elixir code
  • Captures and transforms AST

Keywords

Ruby Elixir
and Keyword and Macro
def Keyword def Macro
if Keyword if Macro
in Keyword in Macro
module Keyword defmodule Macro
not Keyword not Function
or Keyword or Macro
self Keyword self Function
unless Keyword unless Macro

Blocks

Block Keyword List

require MyModule
MyModule.my_def foo do
  IO.puts "It works!"
rescue
  IO.puts "Rescued!"
catch
  IO.puts "Caught!"
else
  IO.puts "Otherwise!"
after
  IO.puts "Afterwards!
end
                                    

require MyModule
MyModule.my_def foo, do: IO.puts "It works",
                     rescue: IO.puts "Rescued!",
                     catch: IO.puts "Caught!",
                     else: IO.puts "Otherwise!",
                     after: IO.puts "Afterwards!
                                    

AST Capture


defmodule ExampleTest do
  use ExUnit.Case

  test "the truth" do
    assert 1 + 1 != 2
  end
end
                        

Compile Data into Code


defmodule MimeTypes do
  HTTPotion.start
  HTTPotion.Response[body: body] = HTTPotion.get(
    "http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types"
  )

  Enum.each String.split(body, %r/\n/), fn (line) ->
    unless line == "" or line =~ %r/^#/ do
      [ mimetype | _exts ] = String.split(line)


      def is_valid?(unquote(mimetype)), do: true
    end
  end

  def is_valid?(_mimetype), do: false
end

MimeTypes.is_valid?("application/vnd.exn") #=> false
MimeTypes.is_valid?("application/json")    #=> true
                        

Operators


# deeply nested function calls
def main(argv) do
  output(process(parse_args(argv)))
end
                        

# single-use variables
def main(argv) do
  parsed_args = parse_args(argv)
  processed = process(parsed_args)
  output(processed)
end
                        

# Pipes
def main(argv) do
  argv
  |> parse_args
  |> process
  |> output
end
                        

defmacro left |> right do
  [{h, _}|t] = Macro.unpipe({:|>, [], [left, right]})
  :lists.foldl fn {x, pos}, acc -> Macro.pipe(acc, x, pos) end, h, t
end
                        

Concurrency

Message Passing


iex> pid = spawn_link fn ->
...>   receive do
...>     {:ping, client} -> send client, :pong
...>   end
...> end
#PID<9014.59.0>
iex> send pid, {:ping, self}
{:ping, #PID<0.73.0>}
iex> flush
:pong
:ok
                        

Process Memory


f = fn -> receive do
  after
    :infinity -> :ok
  end
end
{_, bytes} = Process.info(spawn(f), :memory)
bytes # 2680
                        

Process Time (1)


defmodule ElixirLunchAndLearn.Chain do
  def run(n) do
    {microseconds, result} = :timer.tc(__MODULE__, :create_processes, [n])
    IO.puts "#{result} (Calculated in #{microseconds} microseconds)"
  end
end
                        

Process Time (2)


defmodule ElixirLunchAndLearn.Chain do
  def create_processes(n) do
    last = Enum.reduce 1..n,
                       self,
                       fn(_, send_to) ->
                         spawn(__MODULE__, :counter, [send_to])
                       end

    # start the count by sending
    send last, 0

    # and wait for the result to come back to us
    receive do
      final_answer when is_integer(final_answer) ->
        "Result is #{inspect final_answer}"
    end
  end
end
                       

Process Time (3)


defmodule ElixirLunchAndLearn.Chain do
  def counter(next_pid) do
    receive do
      n ->
        send next_pid, n + 1
    end
  end
end
                        

Process Time (4)


> elixir --erl "+P 1000000" -r lib/elixir_lunch_and_learn/chain.ex -e "ElixirLunchAndLearn.Chain.run(1_000_000)"
Result is 1000000 (Calculated in 11658944 microseconds)
                        

Resources

Intros

Ruby Elixir
Official Intro Getting Started
How I Start Gem to referring to its own article Portal
Learn X in Y minutes Where X=ruby Where X=elixir

Help

Ruby Elixir
IRC (Freenode) #ruby-lang #elixir-lang
Google Group comp.lang.ruby elixir-lang-talk
Meetups Austin.RB Austin Elixir

Videos

Ruby Elixir
Screencasts rubytapas.com elixirsips.com
Conference Talks Confreaks on Youtube

Books

Book Topics
Programming Elixir Sequential Elixir, Concurrent Elixir, Intro to Macros
Elixir In Action Go from a simple, sequential command-line todo app to a concurrent, fault-tolerant todo web service.
Metaprogramming Elixir Advanced Macros

Websites

Ruby Elixir
Official ruby-lang.org elixir-lang.org
Projects by use Awesome Ruby Awesome Elixir

Editors

Ruby Elixir
Emacs ? Alchemist
Jetbrains Rubymine IntelliJ Elixir
Vim vim-ruby vim-elixir

Upcoming Dates

Start End Event Language
2015-08-05 2015-08-05 Elixir Austin Elixir
2015-08-13 2015-08-14 Phoenix Framework Training Elixir
2015-08-15 2015-08-15 Lone Star Ruby Ruby
2015-10-01 2015-10-03 Elixir Conf Elixir
2015-10-23 2015-10-23 Keep Ruby Weird Ruby
2015-11-15 2015-11-17 Ruby Conf Ruby