惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
TaoSecurity Blog
TaoSecurity Blog
T
Threat Research - Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Tenable Blog
C
Cybersecurity and Infrastructure Security Agency CISA
GbyAI
GbyAI
Microsoft Security Blog
Microsoft Security Blog
N
Netflix TechBlog - Medium
AWS News Blog
AWS News Blog
Security Latest
Security Latest
L
LINUX DO - 热门话题
Blog — PlanetScale
Blog — PlanetScale
T
Threatpost
Stack Overflow Blog
Stack Overflow Blog
Vercel News
Vercel News
S
Securelist
I
Intezer
D
Darknet – Hacking Tools, Hacker News & Cyber Security
H
Heimdal Security Blog
T
The Exploit Database - CXSecurity.com
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
The Hacker News
The Hacker News
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
H
Hackread – Cybersecurity News, Data Breaches, AI and More
U
Unit 42
The Register - Security
The Register - Security
NISL@THU
NISL@THU
S
Schneier on Security
M
MIT News - Artificial intelligence
The Last Watchdog
The Last Watchdog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Hugging Face - Blog
Hugging Face - Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Hacker News - Newest:
Hacker News - Newest: "LLM"
Y
Y Combinator Blog
Know Your Adversary
Know Your Adversary
罗磊的独立博客
MongoDB | Blog
MongoDB | Blog
美团技术团队
W
WeLiveSecurity
P
Privacy International News Feed
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
小众软件
小众软件
博客园 - 【当耐特】
P
Proofpoint News Feed
P
Privacy & Cybersecurity Law Blog
T
Tor Project blog

Lobsters

CIFSwitch: a non-universal Linux local root vulnerability RIPE NCC session fixation: poaching logins with an Atlas probe GNOME 2.20 but its Web Components Agentic Search for Context Engineering – Leonie Monigatti Garnix is shutting down [not OC] akashina.tngl.sh/jjc Concerning Emacs (and Jazz) Nitpicking the shell history scene in ‘Tron: Legacy’ What's cooking on SourceHut? Q2 2026 The tenth OpenPGP email summit Package managers that package package managers Clojure on Fennel part three: parsing WordPress at 23 Finding Miscompiles for Fun, Not Profit GitHub - creusot-rs/creusot: Creusot helps you prove your Rust code is correct. Announcing Rust 1.96.0 | Rust Blog A Love Letter to Neovim sqlite AGENTS.md Am I a Bad Friend? CSS vs. JavaScript • Josh W. Comeau Erlang Ecosystem Foundation - Supporting the BEAM community A brief note about slot access cost in Common Lisp Keyboard latency probe Rethinking the GNOME clipboard issues Back to the Building Blocks’ Building Blocks Tech Notes: Theseus: translating win32 to wasm Fast is better than slow Content-addressed Rust builds (or, what kache actually caches) Intent to Prototype: Embedding API Canada’s Bill C-22 and the security cost of collecting more data 5 PostgreSQL locking behaviors that trip people up okmij.org Stop advertising in your commits! | AksDev GitHub - mplsllc/macsurf: A modern web browser for Classic Mac OS 9 PowerPC. Real CSS3, ES5 JavaScript, native HTTPS — built with CodeWarrior on the Carbon API. Introducing DoomBench - Can Your Data Stack Run DOOM? What are some of your favourite developer tools? Building a Scalable Ingestion Pipeline with Temporal (Part 1) Converting shallow Git bundles into normal repositories Are you a member of any professional associations? What is a harmonic? An interactive comic about additive synthesis How Virtual Tables Work in the Itanium C++ ABI Using SwiftUI to Build a Mac-assed App in 2026 Rust (and Slint) on a jailbroken Kindle. ~jack/lambda-on-lambda - Serverless Haskell on AWS - sourcehut git Human proof for FOSS contributions Extremely simple internet radio controlled via IRC Announcing BABLR Splitting Konsole views from Helix to run tools | AksDev GitHub - yugr/rust-slides Serving files over HTTP three ways: synchronous, epoll, and io_uring update docs with information about building with build.py (#979) · astral-sh/python-build-standalone@c9c40c5 A Simple Makefile Tutorial On C extensions, portability, and alternative compilers Switching to Colemak | Pedro Alves Just How Bad Was The Intel IAPX432? Nix's Substituter List Is Not a Routing Table Accelerating copy_if using SIMD Lambda on Lambda: Serverless Haskell on AWS | Blog Announcing feed-repeat v1.0 Scaling Akvorado BMP RIB with sharding EYG news: A host of CLI improvements, new guides and new effects The social contract of writing JS Crossword C array types are weird; and related topics Flatpak will depend on systemd – OSnews Migrating from Go to Rust | corrode Rust Consulting A portentous reunion Vivado Licensing Options How my minimal, memory-safe Go rsync steers clear of vulnerabilities the entropy layer of a wavelet codec, on its own GitHub - nferhat/fht-compositor: A dynamic tiling Wayland compositor. Debian SE Linux and PinTheft Does bulk memmove speed up std::remove_if? (No.) 声明式部分更新 | Blog | Chrome for Developers Fully in-browser container builds Dianne Skoll's Web Site - Remind The Architecture of Open Source Applications (Volume 1)Berkeley DB Pardon MIE? - ironPeak Blog “Long-Term Support” doesn’t mean what you think Jira IS Turing-Complete May I recommend thinking of Emacs as your Fortress of Solitude hershey Floodgap Gopher-HTTP gateway gopher://thelambdalab.xyz/1cuneiforth/ HP QuickWeb, Singular And Pointless That one time I used Go panics for flow control A new suite of modern tools coming for editing and publishing RFCs From the Tabletop… The Digital Antiquarian Building a Host-Tuned GCC to Make GCC Compile Faster Are we self-sovereign PKI yet? Claw Patrol: an open-source security firewall for agents | Deno Revised^7 Report on Scheme, Large: Procedural Fascicle Draft is now public A Network Allow-List Won't Stop Exfiltration — André Graf From AFSK to Goertzel – µArt.cz Software For My New Home Server Introducing Neptune: Direct3D virtualization for QEMU AI Agent Bankrupted Their Operator While Trying to Scan DN42 - Lan Tian @ Blog mimalloc: A new, high-performance, scalable memory allocator for the modern era Making wl_shm fast The Soul of Maintaining a New Machine - Third Draft | Books in Progress What is Git made of?
Rethinking modularity in Ruby applications
noteflakes.c · 2026-06-19 · via Lobsters

18·06·2026

I wrote recently about Syntropy, a new Ruby web framework I’m working on (it runs this site). Syntropy’s design is based around the idea of file-based routing, which means that the source files for route handlers (i.e. controllers) that make up the app are organized and named according to the app’s URL namespace. I also discussed the way Syntropy loads the different source files (referred to as modules), and I’d like to discuss this a bit more in detail.

Code Organization in Ruby on Rails

Now, if you’re a Rails developer, you know that Rails’ approach to code organization is based on auto-loading of the different source files that make up the app, performed by the Zeitwerk gem. This approach automates the loading of dependencies, using the directory structure as a representation of the app’s namespace (i.e. classes and modules).

In the Rails approach, all of the app’s classes and modules are global, nested according to the app’s directory structure. There’s no need to explicitly require dependencies, since any constant reference will be automatically loaded, and this means the dependencies between different parts of app are implicit.

This approach works very well, as long as you’re organizing your code in classes that follow certain conventions in terms of naming and file locations. A big advantage is that you don’t need to explicitly require the different source files in your app. Instead, they’re loaded automatically as constants are referenced by running code. Zeitwerk even supports automatic reloading of changed files in development mode.

One important disadvantage of the Rails approach is that you’re bound to the idea of one class per file, and of course the names for the different classes must match the source file’s location. This means that if you move a source file to a different directory, you’ll also need to rename the class to match its new location.

A further disadvantage is that since everything is global, you might risk touching or referencing classes you shouldn’t, and since dependencies are implicit, those “references by error” may go unnoticed and cause some unexpected (read: undefined) behaviour. For example, since any singleton object (and singleton objects are quite handy in web apps) is defined as a global, your code might accidentally access that global object, or even change its state.

In my experience I have found that when an app reaches a certain level of complexity, and its source code is split across a large number of files, explicitly expressing dependencies between different parts of the source code is a tremendous help to understanding the structure of the code and the relationships of the different parts of the app.

I have also come to appreciate the possibility of expressing interfaces not only in terms of classes and modules, but also with procs/lambdas (i.e. closures) and other singleton objects. In many cases you’ll need to access some “global” service or state object, such as a database connection pool, a background job store, a mailer object, or even a configuration hash. Being able to do that with explicit references means that you don’t need to rely on these objects being available as global constants. This in turn makes it easier to implement dependency injection, and it also greatly simplifies testing.

Code Organization in Syntropy

Syntropy is based on the idea that the app’s directory structure reflects its URL namespace. For example, a simple blog app would have a directory structure resembling the following:

files               URLs
=====               ====
+ app/
  + _lib/
    + storage.rb
  + _layout/
    + default.rb
  + index.rb        /
  + about.rb        /about
  + posts/
    + [id]/
      + index.rb    /posts/[id]
      + edit.rb     /posts/[id]/edit
      + index.rb    /posts
      + new.rb      /posts/new

The example above consists of both controller code (which maps to URLs) and internal dependencies which are typically put in directories such as _lib or _layout (any directory or file whose name starts with an underscore is considered internal and is not exposed by the Syntropy router).

How do those different source files get loaded by the application? In the case of controllers, they’re loaded automatically when the server receives a request with a URL that matches the source file location. For example, a GET /posts request will be routed to app/posts/index.rb.

A second principle in Syntropy is that all dependencies are explicit. In order to load any dependency inside the app, we call import. For example, here’s how the posts index controller may look:

# app/posts/index.rb
@storage = import '/_lib/storage'
@layout = import '/_layout/default'
@template = @layout.apply { |posts:, **|
  posts.each { |post|
    article {
      h1 { a post.title, href: post.url }
      p post.body
    }
  }
}

export ->(req) {
  posts = @storage.get_all_posts
  req.respond_html(@template.render(posts:))
}

In the above example, we import two dependencies, a @storage object which serves as the model interface, and a @layout object which serves as a Papercraft layout template. We then create a template that renders a list of posts, and finally we export the controller lambda, which takes an incoming HTTP request, retrieves the list of posts from @storage renders an HTML response by rendering @template.

Since we can export any object from a module, and that will serve as its value, we might express the layout module as follows:

# app/_layout/default.rb
export template {
  html {
    head { ... }
    body {
      render_children
    }
  }
}

In this module the layout template (a Papercraft template) is the actual interface of the module, such that each time you import it, you get the same object that was exported. Here’s how the storage module might look:

# app/_lib/storage.rb
export self

def get_posts
  ...
end

...

In the case of the storage module, we export the module itself as a singleton, which lets us call any of its methods:

storage = import '/_lib/storage'
storage.get_posts

Loading code in this way has some important advantages:

  • Loading the module code has zero side effects on global state: all constants, and instance variables are completely local to the module’s context, and do not leak to the global context.
  • A module’s interface can be any object, not just a class, which makes it so much easier to use singletons.
  • Dependencies are explicit, which makes it easier to understand how the different pieces of code fit together and where everything is located.
  • The fact that each module is completely self-contained and doesn’t “leak” into the global namespace makes it much easier to test it in separation.
  • It also makes it much easier to implement code reloading. Since all dependencies are known at runtime, we can track calls to import, and if any module’s underlying file has changed, we can simply discard the old module, reload the new code, then recursively reload all of its reverse-dependencies.

Injecting State into Modules

Since each module is evald in a separate, anonymous context, this means we can also easily inject state into the module when loading it. This is especially useful for injecting an environment hash (holding the app’s configuration), or other app concerns that may be used by the module, such as the app object itself, the underlying UringMachine instance, or any other global service. This too may simplify the task of unit testing those modules. This is done by setting instance variables that are available to the module. Here’s how a Syntropy module may access the app’s configuration:

# app/_lib/mailer.rb
require 'my_mailer'

export MyMailer.new(@env[:config][:smtp_server])

Here, we access @env, which is injected into the module when it’s loaded. And of course, we could easily inject a custom @env object if we wanted to, for testing purposes.

How Module Loading Works in Syntropy

In the previous article presenting Syntropy, I discussed the basic mechanics of how Syntropy loads modules. Let’s look in more detail at how Syntropy achieves this. The entire Syntropy module loader code is completely self-contained and does not depend on any other part of Syntropy, it just needs to be provided with a few dependencies that are passed to the modules being loaded.

# Greatly simplified version:
class ModuleLoader
  def initialize(env)
    @env = env
    @app_root = env[:app_root]
    @modules = {} # maps ref to module entry
  end

  def load(ref)
    ref = "/#{ref}" if ref !~ /^\//
    if !(entry = @modules[ref])
      entry = load_module(ref)
      @modules[ref] = entry
    end
    entry[:export_value]
  end

  private

  def load_module(ref)
    fn = File.expand_path(File.join(@app_root, "#{ref}.rb"))
    code = read_file(fn)
    env = @env.merge(module_loader: self, ref: clean_ref(ref))
    mod = ModuleContext.new(env, code, fn)

    {
      fn: fn,
      module: mod,
      export_value: mod.__export_value__,
    }
  end
end

The module loader is initialized with an env object representing the app’s environment (including its configuration). The #load method reads the source code for the module, then instantiates a ModuleContext object that’s going to serve as the context in which the module’s source code is going to run:

# Greatly simplified version:
class ModuleContext
  def initialize(env, code, fn)
    @env = env
    @module_loader = env[:module_loader]
    @ref = env[:ref]
    instance_eval(code, fn)
  end

  private

  def export(v)
    @__export_value__ = v
  end
  
  def import(ref)
    @module_loader.load(ref)
  end
end

The entire mechanism of module loading is represented in these few lines: we create a module context instance, evaluate the source code using #instance_eval, and the import/export methods are defined as private methods and allow explicit dependencies between the different modules, and the usage of any object as a module interface.

Here’s how we may use this in a testing situation:

class FooTest < Minitest::Test
  def setup
    @module_loader = ModuleLoader.new(
      # injected environment hash
      {
        app_root: File.join(__dir__, 'fixtures')
        foo: 'foo'
      }
    )
  end

  def test_foo_module
    foo = @module_loader.load('foo')
    refute_nil foo

    # suppose the foo module has an #env method:
    assert_equal 'foo', foo.env[:foo]
  end
end

As you can see in the code above, the fact that there’s no global state makes writing tests so much easier: our tests do not have a side-effects (at least in terms of changing the global namespace), and we can inject arbitrary values into the environment passed to the loaded modules. Also, since the interfaces for the ModuleLoader and ModuleContext classes are so minimal, it’s easy to extend or compose them into more complex behaviours, or simply to override the stock behaviour.

Breaking Out of the OOP Jail

If there’s one thing I adore about Ruby is the fact that it’s so flexible and so amenable to different styles of writing code and to different techniques. The common approach to writing Ruby code is based on sticking to more or less strict OOP principles: encapsulation, inheritance, code organized into classes, and classes organized into a single global topology.

But there are other ways to write Ruby code - and a lot of times, expressing a functionality as a closure (i.e. a Proc) is so much more powerful, expressive and concise than the usual approach. This is especially true when we’re dealing with building apps on top of a web framework. The framework expects the app’s controllers to have a certain shape, but instead of expressing those controllers as classes, we can express them as procs:

# The Rails way:
class PostsController < ApplicationController
  def index = ...
  end
end

# The Syntropy way
export ->(req) { ... }

There are important differences here, some of them were discussed above, and the question of boilerplate is actually the least interesting one. The Rails controller is globally named, while the Syntropy controller is anonymous. In Rails, an instance of the controller is allocated for each request and its entrypoint method is then called. In Syntropy the controller is the entrypoint that’s called on each request (with the request as an argument), and there’s no object allocation when the controller is invoked.

Also, the fact that controllers are procs gives us both freedom to organize our code as we want, and an almost magical ability to dynamically generate custom controllers, using a variety of techniques. Lastly, using closures (i.e. procs) as units of modularity encourages us to create clean interfaces. When the surface area of a certain functionality is reduced to a single method interface (i.e. #call), you start thinking differently about how to build your app.

Take a look at a controller from the blog example in the Syntropy repository. This is a barebones site that allows you to view, add, edit and delete blog posts:

# app/posts/index.rb
@posts = import '_lib/posts'
@layout = import '_layout/default'
@template = @layout.apply {
  ...
}

export dispatch_by_http_method

def get(req)
  posts = @posts.get_all
  req.respond_html(@template.render(posts:, flash: req.flash))
end

def post(req)
  data = req.get_form_data
  title = req.validate(data['title'], String, /.+/)
  body = req.validate(data['body'], String, /.+/)
  id = @posts.create(title, body)

  req.flash[:notice] = 'Post was successfully created.'
  req.redirect("posts/#{id}")
end

We start by importing dependencies and assigning them to instance variables: @posts is the model, @layout is the Papercraft template used as layout, and @template generates HTML to show the list of posts. We then export an expression: dispatch_by_http_method. This is actually a method call, here’s its implementation:

def dispatch_by_http_method
  ->(req) {
    sym = req.method.to_sym
    raise Syntropy::Error.method_not_allowed if !respond_to?(sym)

    send(sym, req)
  }
end

Here we get to taste some of the power of using procs as closures: we generate a closure that knows how to dispatch a request based on the HTTP method by calling the corresponding Ruby method on itself. It is automatically bound to the module context (i.e. self), and with just a few lines of code we expressed a very common behaviour - the request will be dispatched to the method corresponding to the request’s HTTP method, and if the method is not implemented, a METHOD NOT ALLOWED status code is returned to the client.

So once we call dispatch_by_http_method to generate the controller’s entrypoint, all that’s left is to implement the handlers for the HTTP methods we want to accept, namely GET and POST:

export dispatch_by_http_method

def get(req)
  ...
end

def post(req)
  ...
end

We could in fact use the same technique to implement a Rails-style controller (it will need to be a wildcard route because a Rails controller actually handles multiple different URLs by default):

# app/posts+.rb
export dispatch_action

def index(req) = ...

def show(req) = ...

def new(req) = ...

# €tc...

Now take a look at the code above. To quote a famous Rails person, “look at all the things I’m not doing!” I’m not defining any classes, I’m not namespacing anything, I’m defining global constants. The only reference to this code is its location in the app’s directory structure, and the dispatch_by_action directive dynamically creates a closure that wraps our code, along with a little bit of logic to dispatch the request to the correct method.

So, while we still organize our controller logic using methods, the glue code, is implemented using procs/closures. Now I’m not saying that all Ruby code should look like this. Of course there are many situations where the classic OOP approach is beneficial. This is especially true for Ruby gems, where we need to have clearly defined APIs, and also some type information. But in an app-on-top-of-a-framework situation, where the app code is driven by the framework, coding the app’s entrypoints as closures brings with it multiple benefits: less boilerplate, clearer dependencies, more dynamic behaviour, and expressivity.

I think there’s still a lot to be discovered in writing Ruby code in a more functional style. In the last few years I’ve been exploring just that - bringing a more functional approach to Ruby. Some of my work has lead to the development of Papercraft, which brings a functional approach to HTML templating. Syntropy now brings a functional approach to web controllers. And I think the same could also be done for models, which I hope to discuss in a future article.