BUCKYVERSE

Ruby to Elixir Migration Guide

This guide documents Ruby-specific features used in our codebase and their Elixir equivalents to assist in the migration process.

Command Execution

Ruby

# Using Open3
require 'open3'
stdout, stderr, status = Open3.capture3(command)

# Using system
success = system(command)

# Using backticks
output = `command`

Elixir

# Using System.cmd
{output, exit_code} = System.cmd("command", args, opts)

# Using Port for more complex interactions
port = Port.open({:spawn, "command"}, [:binary])
receive do
  {^port, {:data, data}} -> handle_data(data)
end

# Using Task for async execution
task = Task.async(fn -> System.cmd("command", args) end)
result = Task.await(task)

Error Handling

Ruby

begin
  # code that might raise an exception
rescue SomeError => e
  # handle specific error
rescue => e
  # handle any error
ensure
  # always executed
end

raise "Error message"
raise CustomError, "Error message"

Elixir

try do
  # code that might raise an exception
rescue
  e in SomeError -> # handle specific error
  e -> # handle any error
after
  # always executed
end

# Preferred pattern
case operation() do
  {:ok, result} -> handle_success(result)
  {:error, reason} -> handle_error(reason)
end

# With statement for multiple operations
with {:ok, a} <- operation_a(),
     {:ok, b} <- operation_b(a),
     {:ok, c} <- operation_c(b) do
  handle_success(c)
else
  {:error, reason} -> handle_error(reason)
end

File System Operations

Ruby

# Reading files
content = File.read(path)
content = File.read(path, encoding: 'UTF-8')

# Writing files
File.write(path, content)
File.write(path, content, mode: 'a')

# File existence
File.exist?(path)
File.directory?(path)

# Directory operations
Dir.mkdir(path)
FileUtils.mkdir_p(path)
Dir.glob("*.txt")

Elixir

# Reading files
{:ok, content} = File.read(path)
{:ok, content} = File.read(path, [:utf8])

# Writing files
:ok = File.write(path, content)
:ok = File.write(path, content, [:append])

# File existence
File.exists?(path)
File.dir?(path)

# Directory operations
:ok = File.mkdir(path)
:ok = File.mkdir_p(path)
Path.wildcard("*.txt")

Mutable State

Ruby

class MyClass
  def initialize
    @state = initial_value
  end
  
  def update_state(new_value)
    @state = new_value
  end
end

Elixir

# Using GenServer
defmodule MyModule do
  use GenServer
  
  def start_link(initial_value) do
    GenServer.start_link(__MODULE__, initial_value)
  end
  
  def update_state(pid, new_value) do
    GenServer.call(pid, {:update, new_value})
  end
  
  # Callbacks
  def init(initial_value) do
    {:ok, initial_value}
  end
  
  def handle_call({:update, new_value}, _from, _state) do
    {:reply, :ok, new_value}
  end
end

# Using Agent for simpler state
{:ok, agent} = Agent.start_link(fn -> initial_value end)
Agent.update(agent, fn _state -> new_value end)
current = Agent.get(agent, fn state -> state end)

String Manipulation

Ruby

# String interpolation
message = "Hello, #{name}!"

# String methods
parts = string.split(",")
new_string = string.gsub(/pattern/, "replacement")
trimmed = string.strip

# Regular expressions
if string =~ /pattern/
  # match
end

Elixir

# String interpolation
message = "Hello, #{name}!"

# String methods
parts = String.split(string, ",")
new_string = String.replace(string, ~r/pattern/, "replacement")
trimmed = String.trim(string)

# Regular expressions
if string =~ ~r/pattern/ do
  # match
end

Hash/Map Operations

Ruby

# Creating hashes
hash = { key: "value", another: 123 }
hash = { "key" => "value" }

# Accessing values
value = hash[:key]
value = hash.fetch(:key, default)

# Merging
merged = hash.merge(other_hash)

Elixir

# Creating maps
map = %{key: "value", another: 123}
map = %{"key" => "value"}

# Accessing values
value = map[:key]
value = Map.get(map, :key, default)

# Merging
merged = Map.merge(map, other_map)

# Using structs for domain objects
defmodule User do
  defstruct [:name, :email, age: 0]
end
user = %User{name: "John", email: "john@example.com"}

Iteration and Collection Processing

Ruby

# Each
collection.each { |item| process(item) }

# Map/Select/Reject
results = collection.map { |item| transform(item) }
filtered = collection.select { |item| item.valid? }
rejected = collection.reject { |item| item.invalid? }

# Reduce
sum = collection.reduce(0) { |acc, item| acc + item }

Elixir

# Each
Enum.each(collection, fn item -> process(item) end)

# Map/Filter
results = Enum.map(collection, fn item -> transform(item) end)
filtered = Enum.filter(collection, fn item -> valid?(item) end)
rejected = Enum.reject(collection, fn item -> invalid?(item) end)

# Reduce
sum = Enum.reduce(collection, 0, fn item, acc -> acc + item end)

# Pipe operator for chaining
collection
|> Enum.filter(&valid?/1)
|> Enum.map(&transform/1)
|> Enum.reduce(0, &(&1 + &2))

Logging

Ruby

require 'logger'
logger = Logger.new(STDOUT)
logger.level = Logger::INFO

logger.debug("Debug message")
logger.info("Info message")
logger.warn("Warning message")
logger.error("Error message")

Elixir

require Logger

Logger.debug("Debug message")
Logger.info("Info message")
Logger.warning("Warning message")
Logger.error("Error message")

# With metadata
Logger.info("Processing item", item_id: item.id, status: :started)

Testing

Ruby

# RSpec
describe MyClass do
  it "does something" do
    expect(subject.method).to eq(expected)
  end
end

# Mocking
allow(object).to receive(:method).and_return(value)

Elixir

# ExUnit
defmodule MyModuleTest do
  use ExUnit.Case
  
  test "does something" do
    assert MyModule.method() == expected
  end
end

# Mocking with Mox
Mox.expect(MockModule, :function, fn args -> result end)

Concurrency

Ruby

# Threads
thread = Thread.new do
  # concurrent operation
end
result = thread.join

# Thread pool
pool = Concurrent::FixedThreadPool.new(5)
pool.post do
  # task
end

Elixir

# Processes
pid = spawn(fn -> concurrent_operation() end)
Process.monitor(pid)

# Tasks
task = Task.async(fn -> concurrent_operation() end)
result = Task.await(task)

# Task supervision
children = [
  {Task.Supervisor, name: MyApp.TaskSupervisor}
]
Supervisor.start_link(children, strategy: :one_for_one)

# Start supervised task
Task.Supervisor.async_nolink(MyApp.TaskSupervisor, fn -> 
  concurrent_operation() 
end)

Configuration

Ruby

# YAML config
config = YAML.load_file('config.yml')

# Environment variables
api_key = ENV['API_KEY']

Elixir

# Application environment
config :my_app, :key, value

# Runtime config
api_key = Application.get_env(:my_app, :api_key)

# System environment
api_key = System.get_env("API_KEY")

Dependency Management

Ruby

# Gemfile
gem 'name', '~> 1.0'

Elixir

# mix.exs
defp deps do
  [
    {:name, "~> 1.0"}
  ]
end

Conclusion

This guide covers the most common Ruby patterns used in our codebase and their Elixir equivalents. When migrating code, consider not just translating syntax but embracing Elixir's functional paradigm and concurrency model to take full advantage of the platform.

Remember that Elixir encourages: