Skip to content

Support memoizing methods with block parameters#15

Open
mjcbsn22 wants to merge 1 commit intomakandra:masterfrom
mjcbsn22:support-block-parameters
Open

Support memoizing methods with block parameters#15
mjcbsn22 wants to merge 1 commit intomakandra:masterfrom
mjcbsn22:support-block-parameters

Conversation

@mjcbsn22
Copy link
Copy Markdown

@mjcbsn22 mjcbsn22 commented Mar 18, 2026

Problem

Calling memoize on a method that has a &block parameter raises Memoized::CannotMemoize. This prevents memoizing methods that use blocks internally (e.g. transform_values(&:to_s)) even though the block doesn't affect cacheability.

Solution

  • Track &block parameters in Parameters instead of raising
  • Append the original block parameter to the generated method signature
  • Forward the block to the original method on cache miss

The block is not part of the cache key since blocks cannot be meaningfully hashed. On cache hit, the cached value is returned regardless of the block passed. This is correct when the block is part of the method's internal implementation rather than an external input that changes the return value.

Note: Methods using implicit yield without an explicit &block parameter are not affected by this change. Ruby does not report a :block parameter for such methods, so they remain unchanged.

Tests

Added specs covering:

  • Methods with only a block parameter
  • Methods with positional args and a block
  • Methods with keyword args and a block
  • Methods with all parameter types (req, opt, rest, keyreq, key, keyrest) and a block
  • Optional block: called with and without a block, in both orders
  • Cache hit returns cached value regardless of block
  • unmemoize works correctly for block methods
  • Private method visibility is preserved

All existing tests continue to pass.

@mjcbsn22 mjcbsn22 force-pushed the support-block-parameters branch 2 times, most recently from 2415501 to af8eb21 Compare March 18, 2026 21:56
Methods with `&block` parameters previously raised
`CannotMemoize`. This change strips the block from the
cache key (since blocks cannot be meaningfully hashed) and
forwards it to the original method on cache miss.

On subsequent calls, the cached return value is used
regardless of the block passed. This is the correct
behavior when the block is part of the method's internal
implementation rather than an external input that changes
the return value.
@mjcbsn22 mjcbsn22 force-pushed the support-block-parameters branch from af8eb21 to 52cbfe6 Compare March 18, 2026 21:57
@triskweline
Copy link
Copy Markdown
Member

Thanks for the PR @mjcbsn22 .

The block is not part of the cache key since blocks cannot be meaningfully hashed. On cache hit, the cached value is returned regardless of the block passed. This is correct when the block is part of the method's internal implementation rather than an external input that changes the return value.

I didn't understand why it would be safe to ignore a block argument for the cache key. Do you mean scenarios when the block is passed on to another method internally?

I really want to prevent memoized methods from silently caching uncacheable invocations, and err on the side of correctness when we cannot know.

@mak-dunkelziffer
Copy link
Copy Markdown
Contributor

@mjcbsn22 During the upgrade to Ruby 3 and adjustments for the new keyword arguments, we discussed support for blocks and rejected it.

  • We didn't see real use cases. Please provide code examples of your use cases.
  • We expected it to be more helpful to prevent accidental errors here. If we ever support blocks, then as an explicit opt-in memoize_with_block.
  • My original idea was that the only way to correctly memoize blocks would be via the object_id of the block. However, that was less helpful than you think (doesn't work with do ... end blocks, only with &: notation). Also, Ruby can reuse object_ids after garbage collection. So even that method isn't safe.
  • I think your approach to simply not put the block into the hash key is incorrect. I also don't understand, what you mean by internal implementation vs. external input. From the perspective of the memoized method the block is always an external input.

@triskweline
Copy link
Copy Markdown
Member

I did wonder whether blocks could make a cache key via their block.source_location.

@Kateba72
Copy link
Copy Markdown

I did wonder whether blocks could make a cache key via their block.source_location.

They can't. The result of (non-lambda) blocks can depend on the value of surrounding variables, e.g.

def foo(arg)
  bar { arg }
end

Here, the call to bar can't be memoized, because it depends on foo's variables. This is also the reason why do-end blocks can't be memoized.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants