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

推荐订阅源

H
Heimdal Security Blog
A
Arctic Wolf
K
Kaspersky official blog
V
Vulnerabilities – Threatpost
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Simon Willison's Weblog
Simon Willison's Weblog
L
LINUX DO - 热门话题
MongoDB | Blog
MongoDB | Blog
T
Threat Research - Cisco Blogs
D
Docker
爱范儿
爱范儿
T
Tenable Blog
C
Check Point Blog
B
Blog
C
Cisco Blogs
Vercel News
Vercel News
The Cloudflare Blog
T
Threatpost
NISL@THU
NISL@THU
T
Tor Project blog
V2EX - 技术
V2EX - 技术
P
Palo Alto Networks Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Tailwind CSS Blog
G
GRAHAM CLULEY
P
Privacy & Cybersecurity Law Blog
SecWiki News
SecWiki News
博客园 - 司徒正美
S
Security @ Cisco Blogs
GbyAI
GbyAI
S
Secure Thoughts
Microsoft Security Blog
Microsoft Security Blog
The Register - Security
The Register - Security
Recorded Future
Recorded Future
Cloudbric
Cloudbric
Webroot Blog
Webroot Blog
N
News and Events Feed by Topic
Y
Y Combinator Blog
博客园_首页
T
Troy Hunt's Blog
The Hacker News
The Hacker News
雷峰网
雷峰网
Google DeepMind News
Google DeepMind News
U
Unit 42
AWS News Blog
AWS News Blog
PCI Perspectives
PCI Perspectives
V
Visual Studio Blog
博客园 - 聂微东
有赞技术团队
有赞技术团队
酷 壳 – CoolShell
酷 壳 – CoolShell

Hacker News

Introducing Claude Opus 4.7 Qwen Studio The Future of Everything is Lies, I Guess: Where Do We Go From Here? GitHub - SeanFDZ/macmind: Single-layer transformer in HyperTalk for the classic Macintosh Show HN: Agent-cache – Multi-tier LLM/tool/session caching for Valkey and Redis Moving a large-scale metrics pipeline from StatsD to OpenTelemetry / Prometheus GitHub - Nightmare-Eclipse/RedSun: The Red Sun vulnerability repository GitHub - SethPyle376/hiraeth: Local AWS emulator focused on fast integration testing, with SQS support, SQLite-backed state, and a debug-friendly web UI. GitHub - macOS26/Agent: Any AI, replaces Claude Code, Cursor, OpenClaw. Over 18 LLM providers (Claude, OpenAI, Gemini, Ollama, Zai, HF, Qwen) wired into a native Mac app that writes code, builds Xcode projects, bumps versions, manages git, automates Safari, use AppleScript, JS or Accessibility, extend Agent! w/ MCP Servers, run tasks from your iPhone via Messages. YouTube now lets you turn off Shorts I Made a Terminal Pager Burgers | マクドナルド公式 Commands — HackerNews CLI documentation ChatGPT for Excel PiCore - Raspberry Pi Port of Tiny Core Linux Live Nation illegally monopolized ticketing market, jury finds Google Broke Its Promise to Me. Now ICE Has My Data. Founding Engineer at Adaptional | Y Combinator CRISPR takes important step toward silencing Down syndrome’s extra chromosome GitHub - saffron-health/libretto: The AI toolkit for building reliable browser automations US v. Heppner (S.D.N.Y. 2026) no attorney-client privilege for AI chats [pdf] Unexpected €54k billing spike in 13 hours: Firebase browser key without API restrictions used for Gemini requests Retrofitting JIT Compilers into C Interpreters IPv6 – Google The Accursèd Alphabetical Clock Cybersecurity Looks Like Proof of Work Now Fragments: April 14 Cal.com Goes Closed Source: Why AI Security Is Forcing Our Decision | Cal.com - Scheduling Software for Online Bookings Laravel raised money and now injects ads directly into your agent When moving fast, talking is the first thing to break Too much Discussion of the XOR swap trick – Heather Cafe Introduction to Spherical Harmonics for Graphics Programmers The Grand Line Building a Z-Machine in the worst possible language High-Level Rust: Getting 80% of the Benefits with 20% of the Pain GitHub - duguyue100/midnight-captain: Inspired by Midnight Commander, tailored to my taste. How to build a `git diff` driver · Jamie Tanna | Software Engineer Center for Responsible, Decentralized Intelligence at Berkeley The Local Universe’s Expansion Rate Is Clearer Than Ever, but Still Doesn’t Add Up - A new synthesis of astronomical measurements confirms a persistent mismatch that could point to physics beyond current models The air throughout our homes is infused with microplastics. But there are things you can do to breathe less of them The disturbing white paper Red Hat is trying to erase from the internet – OSnews The Future of Everything is Lies, I Guess: Annoyances ‘Abhorrent’: the inside story of the Polymarket gamblers betting millions on war Productive procrastination — Max van IJsselmuiden maps, territory and LMs 447 Terabytes per Square Centimetre at Zero Retention Energy: Non-Volatile Memory at the Atomic Scale on Fluorographane Show HN: Pardonned.com – A searchable database of US Pardons 20 Years on AWS and Never Not My Job The Seasons are Wrong Artemis II crew splashes down near San Diego after historic moon mission We gave an AI a 3 year retail lease in SF and asked it to make a profit | Andon Labs How a dancer with ALS used brainwaves to perform live On filing the corners off my MacBooks Installing every* Firefox extension OpenClaw’s memory is unreliable, and you don’t know when it will break Steve Blank Nowhere Is Safe Chimpanzees in Uganda locked in vicious 'civil war', say researchers watgo - a WebAssembly Toolkit for Go linux/Documentation/process/coding-assistants.rst at master · torvalds/linux GitHub - callumlocke/json-formatter: Makes JSON easy to read. Founding Product Engineer at Bild AI | Y Combinator A compelling title that is cryptic enough to get you to take action on it GitHub - Keychron/Keychron-Keyboards-Hardware-Design: Industrial design files for Keychron keyboards and mice. 100+ models with CAD assets in STEP, DXF, DWG, and PDF. Source-available, with commercial use allowed for original compatible accessories within the license terms. [ANNOUNCE] WireGuardNT v0.11 and WireGuard for Windows v0.6 Released 1D-Chess Helium Is Hard to Replace Cooperative Vectors Introduction | Evolve Keeping a Postgres queue healthy — PlanetScale Our response to the Axios developer tool compromise Do Americans read print books, e-books or audiobooks more? The Zettelkasten Method in Obsidian: A Practical Setup Guide Artemis II Is Competency Porn and We Are Starving For It WeakC4 Flight Viz — Cockpit View A Mexican surveillance giant you’ve never heard of is now watching the U.S. border Surelock: Deadlock-Free Mutexes for Rust RISC-V 101 – what is it and what does it mean for Canonical? | Ubuntu The Problem That Built an Industry How Much Linear Memory Access Is Enough? | Solidean Investigating Split Locks on x86-64 Simplest hash functions Sybilproof reputation mechanisms (2005) [pdf] What is a property? How Complex is my Code? Static code analysis in Kotlin — tools overview Toffoli gates are all you need PGLite evangelism dcmake: a new CMake debugger UI Clojure on Fennel part one: Persistent Data Structures Fragments: April 2 Python Release Python install manager 26.1 The Life and Death of the Book Review - Liberties Introducing Database Traffic Control — PlanetScale Bitcoin miners are losing $19,000 on every BTC produced as difficulty drops 7.8% God sleeps in the minerals Building slogbox Apple Silicon and Virtual Machines: Beating the 2 VM Limit Who was “Not Even Wrong” first? Pokemon Evolution Vs Darwinian Evolution The APL Programming Language Source Code
Rethinking modularity in Ruby applications
noteflakes.c · 2026-06-19 · via Hacker News

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.