Skip to content

Instantly share code, notes, and snippets.

@adamlogic
Created November 24, 2019 01:45
Show Gist options
  • Save adamlogic/3b1a8b9ebb3e35c7c4174869763f22a5 to your computer and use it in GitHub Desktop.
Save adamlogic/3b1a8b9ebb3e35c7c4174869763f22a5 to your computer and use it in GitHub Desktop.
Presentation given at RubyConf 2019. This is the source for my slides, used with Deckset (paid Mac app).

build-lists: true

In the beginning
there was require


confession

^ 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


![](/Users/adam/Downloads/Screen Shot 2019-11-17 at 9.53.36 AM.png)

Cargo culting

^ 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


![](/Users/adam/Downloads/no idea.jpg)

12 years

[.footer: photo by John Nebbia]

^ I didn't know how to require code πŸ‘‡ Wrote the proposal for this talk


fit

^ All about requiring code in ruby We all do everyday My goal is to fill that knowledge gap πŸ‘‡ Introduce myself


Who am I?

  • Adam McCrea
  • @adamlogic
  • You Need A Budget (YNAB)
  • Rails Autoscale

^ Ruby 12 years, software for 18 years πŸ‘‡ Requiring code... dependencies


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


PHP

include 'banana.php';

NodeJS

const fs = require("fs");
const banana = require("./banana.js");

ES6

import fs from "fs";
import banana from "./banana.js";

Ruby

.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


fit

^ Left, right, top, bottom πŸ‘‡ Let's talk about require


fit

^ Require is the foundation πŸ‘‡ To understand, let's see it in action


Require a standard library

.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


Require a gem

.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


Gem activation

.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?


Kernel#require from rubygems

  • 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


Require within a 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?


Require within a project

~/project
β”œβ”€β”€ main.rb
└── lib
    └── example.rb

# main.rb
require '/Users/Adam/project/lib/example'

^ Absolute file system path Starts with / Brittle, not portable


Require within a project

.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?


add to load path

~/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


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


alter load_path at runtime

~/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?


fit

^ πŸ‘‡ Revisit relative path example


require relative

.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


fit


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


fit

^ πŸ‘‡ To illustrate, revisit our minitest example


gem versions

.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...


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


fit

^ Skipping require_dependency for now Entering lazy territory with autoload πŸ‘‡ Let's see what that means


Kernel#autoload

.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


fit

^ 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

autoload_paths

app/controllers
app/models
app/*
app/*/concerns

^ Missing constant (User) Convention: user.rb Paths within app AND gems πŸ‘‡ LOAD_PATH != autoload_paths


$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


fit

^ 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


fit

^ 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


fit

^ What led me down this path... Now irrelevant πŸ‘‡ Takeaways


fit

Takeaways


standard libraries

require 'csv'

^ Even in Rails


gems

require 'minitest'

# OR

Bundler.setup
require 'minitest'

# OR

Bundler.require

^ Done for you in Rails


within a project (non-Rails)

require_relative 'lib/example'

# OR

$LOAD_PATH.unshift File.join(File.dirname(__FILE__), 'lib')
require 'example'

# OR

autoload :Example, 'example'

within a project (Rails)

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


Thank you!

Comments/Questions: @adamlogic (or come talk to me)

ynab.com railsautoscale.com

^ Shirts, stickers Thanks so much

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment