build-lists: true
^ Story that led to this talk Custom processing video uploads Extend ActiveStorage π Came across these lines
[.footer: https://pxhere.com/en/photo/458183]
class ActiveStorage::Blob < ActiveRecord::Base
require_dependency "active_storage/blob/analyzable"
require_dependency "active_storage/blob/identifiable"
require_dependency "active_storage/blob/representable"
# ...
^ Ruby? Rails? Shoved aside to complete task (lines were inconsequential) Rails engine Where it goes, how to organize and require Unfamiliar π Find other engine examples, use them without understanding

^ More often than I'd like to admit Got it working without understanding the glue The knowledge gap was uncomfortable π Writing Ruby professionally for 12 years

[.footer: photo by John Nebbia]
^ I didn't know how to require code π Wrote the proposal for this talk
^ All about requiring code in ruby We all do everyday My goal is to fill that knowledge gap π Introduce myself
- Adam McCrea
- @adamlogic
- You Need A Budget (YNAB)
- Rails Autoscale
^ Ruby 12 years, software for 18 years π Requiring code... dependencies
- standard library
- Rubygems
- within a project
[.footer: Photo by Iker Urteaga on Unsplash]
^ Three flavors Csv, open-url, logger Activerecord, rspec, minitest π Every lang has a way
include 'banana.php';
const fs = require("fs");
const banana = require("./banana.js");
import fs from "fs";
import banana from "./banana.js";
.code-highlight: 1-1 .code-highlight: 1-2 .code-highlight: 1-3 .code-highlight: 1-4 .code-highlight: 1-5 .code-highlight: 1-6 .code-highlight: all
require
require_relative
require_dependency
load
autoload
Bundler.require
β¨ Rails β¨
^ Simple topic, not so simple Blindsided by how little I knew π As I learned, made a mental model
^ Left, right, top, bottom π Let's talk about require
^ Require is the foundation π To understand, let's see it in action
.code-highlight: 1-2 .code-highlight: 1-5 .code-highlight: 1-8 .code-highlight: all
> CSV
# NameError (uninitialized constant CSV)
> require 'csv'
# true
> CSV
# CSV
> require 'csv'
# false
^ Requiring stdlib is the simplest example π What is require?
> Kernel.methods.sort
# [ :attr_accessor,
# :attr_reader,
# :attr_writer,
# ...
# :puts,
# :raise,
# :require,
# :require_relative,
# ...
^ Just a method Can be overridden (see later) π What happens when we pass "csv"?
> $LOAD_PATH.inspect
# [ "/Users/adam/.asdf/plugins/ruby/rubygems-plugin",
# ...
# "/Users/adam/.asdf/installs/ruby/2.6.4/lib/ruby/2.6.0",
# ...
^ Load path set by Ruby We can add to it (later) π Another global var...
> $LOADED_FEATURES.inspect
# [ ...
# "/Users/adam/.../ruby/2.6.0/csv/writer.rb",
# "/Users/adam/.../ruby/2.6.0/csv/version.rb",
# "/Users/adam/.../ruby/2.6.0/csv.rb"]
^ Require return true/false csv.rb requires its own dependencies π Requiring a gem is similar
.code-highlight: 1-2 .code-highlight: 1-5 .code-highlight: all
> Minitest
# NameError (uninitialized constant Minitest)
> require 'minitest'
# true
> Minitest
# Minitest
^ Must have gem installed π Looks the same, subtle diff
.code-highlight: 1-2 .code-highlight: 1-5 .code-highlight: all
> $LOAD_PATH.grep(/minitest/)
# []
> require 'minitest'
# true
> $LOAD_PATH.grep(/minitest/)
# [".../ruby/2.6.4/lib/ruby/gems/2.6.0/gems/minitest-5.11.3/lib"]
^ Rubygems overrides Kernel#require π What does it do?
- Check LOADED_FEATURES
- Check LOAD_PATH
- Check for a matching installed gem
- Add gem's lib* dir to LOAD_PATH
- Resume default require behavior
^ When you require something you're using the override But don't override it yourself π Not necessarily lib dir
Gem::Specification.new do |spec|
spec.require_path = "lib"
...
^ π 3rd thing we might want to require is within project
.code-highlight: 1-5 .code-highlight: 1-7 .code-highlight: all
~/project
βββ main.rb
βββ lib
βββ example.rb
# main.rb
require 'example'
require 'lib/example'
^ Goal: require example from main What happens? π Alternatives?
~/project
βββ main.rb
βββ lib
βββ example.rb
# main.rb
require '/Users/Adam/project/lib/example'
^ Absolute file system path Starts with / Brittle, not portable
.code-highlight: 1-7 .code-highlight: all
~/project
βββ main.rb
βββ lib
βββ example.rb
# main.rb
require './lib/example'
$ ruby project/main.rb
^ Relative file system path Starts with ./ Better, but with a gotcha π How to reliably require example?
~/project
βββ main.rb
βββ lib
βββ example.rb
# main.rb
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), 'lib')
require 'example'
^ Works in an working directory Good for common root dir π Another way to alter load_path at runtime
~/project
βββ main.rb
βββ lib
βββ example.rb
$ ruby -I lib main.rb
^ Same behavior we just saw π Very useful for running tests
~/project
βββ main.rb
βββ test
βββ test_helper.rb
βββ example_test.rb
# example_test.rb
require 'test_helper'
$ ruby -I test test/example_test.rb
^ π What if we don't want to mess with load path?
^ π Revisit relative path example
.code-highlight: 1-7 .code-highlight: 1-6,8
~/project
βββ main.rb
βββ lib
βββ example.rb
# main.rb
require './lib/example'
require_relative 'lib/example'
^ Execute from anywhere Useful Sometimes load path is easier π Quickly... Load
.code-highlight: 1-3 .code-highlight: 1-5 .code-highlight: all
> load './lib/example.rb'
# true
> load './lib/example.rb'
# true
> load './lib/example.rb'
# true
^ Note the file extension Re-evaluates target file Not useful for most apps π Bundler
^ π To illustrate, revisit our minitest example
.code-highlight: 1-2 .code-highlight: 1-5 .code-highlight: all
> require 'minitest'
# true
$ gem list minitest
# minitest (5.12.2, 5.11.3)
> Minitest::VERSION
# "5.12.2"
^ How do we know which version gets used? How do we ensure others use the same version? π Bundler...
.code-highlight: 1-3 .code-highlight: all
# Gemfile
gem 'minitest', '~> 5.11.0'
β― bundle install
# Resolving dependencies...
# Using minitest 5.11.3
^ Bundle install installs and locks π When locking, creates Gemfile.lock
# Gemfile.lock
GEM
specs:
minitest (5.11.3)
^ Collaborators or prod environments will install the same version π Now that we've run bundle install, let's try to require it again
.code-highlight: 1-2 .code-highlight: all
> require 'minitest'
# true
> Minitest::VERSION
# "5.12.2"
^ We've required it, which version did we get? Why didn't we get the locked version? Gem activation always uses latest installed version π We need bundlers help to require
.code-highlight: 1-2 .code-highlight: 1-7 .code-highlight: all
> require 'bundler'
> Bundler.setup
> $LOAD_PATH
# [ "/Users/adam/.asdf/plugins/ruby/rubygems-plugin",
# ".../ruby/2.6.4/lib/ruby/gems/2.6.0/gems/minitest-5.11.3/lib",
# ...
> require 'minitest'
# true
> Minitest::VERSION
# "5.11.3"
.code-highlight: 1-2 .code-highlight: all
> require 'bundler'
> Bundler.require
> Minitest::VERSION
# "5.11.3"
^ Can also require specific groups π Can opt out in Gemfile
# Gemfile
gem 'minitest', '~> 5.11.0', require: false
^ No effect unless Bundler.require Must explicitly require π That's Bundler
^ Skipping require_dependency for now Entering lazy territory with autoload π Let's see what that means
.code-highlight: 1-4 .code-highlight: 1-8 .code-highlight: all
# lib/example.rb
class Example
puts "Example has loaded"
end
# main.rb
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), 'lib')
autoload :Example, 'example'
Example
# Example has loaded
^ Autoload calls require (LOAD_PATH) Reduces initial load π Handy for libraries (like rack)
# rack-2.0.6/lib/rack.rb
autoload :Builder, "rack/builder"
autoload :BodyProxy, "rack/body_proxy"
# ... 40 more autoloads
^ Gem activation puts lib in load path π This autoloading built into Ruby Rails autoloading different beast
^ Implicit quadrants, both Rails No explicit requires for app code π What does it look like?
.code-highlight: 1-2 .code-highlight: 1-5 .code-highlight: all
> defined? User
# nil
> User.new
# #<User:0x00007fd6547a88e8>
> defined? User
# "constant"
^ π Let's us write code like this
class UsersController < ApplicationController
def new
@user = User.new
end
end
^ No require for ApplicationController or User Where are they required from?
.code-highlight: 1-2 .code-highlight: all
app/controllers
app/models
app/*
app/*/concerns
^ Missing constant (User) Convention: user.rb Paths within app AND gems π LOAD_PATH != autoload_paths
# lib/example.rb
class Example
end
# users_controller.rb
require 'example'
^ Lib not autoloaded Rails does add it to LOAD_PATH Require from lib, not app What if we added lib to autoload_paths? Come back to it. π Autoload in dev, prod is different
^ Dev optimized for fast boot, reloading Prod optimized for fast requests Eager require all files in autoload_paths Eager require lib can be dangerous π Sometimes Rails needs eager loading ...even in dev
^ Back where my journey began Part of Rails, not Ruby Explicit, eager, for Rails π Why?... back to ActiveStoarge
.code-highlight: 1-5 .code-highlight: all .code-highlight: 7
# app/models/active_storage/blob.rb
class ActiveStorage::Blob < ActiveRecord::Base
require_dependency "active_storage/blob/analyzable"
require_dependency "active_storage/blob/identifiable"
require_dependency "active_storage/blob/representable"
include Analyzable
include Identifiable
include Representable
# ...
end
^ Rails would autoload these, so why eager load them? What if our app defined Analyzable? Why not require? π Irony, not needed in Rails 6
^ What led me down this path... Now irrelevant π Takeaways
require 'csv'
^ Even in Rails
require 'minitest'
# OR
Bundler.setup
require 'minitest'
# OR
Bundler.require
^ Done for you in Rails
require_relative 'lib/example'
# OR
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), 'lib')
require 'example'
# OR
autoload :Example, 'example'
embrace implicit autoloading
expclicitly require files in lib
^ App directories autoload, donβt require them Name your files and classes appropriately π I hope this talk has filled knowledge gap
[.footer: Photo by Laurie-Anne Robert on Unsplash]
^ Next time you encounter a gap, dive in! No shame π Questions
Comments/Questions: @adamlogic (or come talk to me)
ynab.com railsautoscale.com
^ Shirts, stickers Thanks so much