NAME

Sidef::Object::LazyMethod - Deferred, chainable method calls.

DESCRIPTION

A LazyMethod object records a sequence of method calls (names and arguments) without running any of them. The very first call in the chain also carries the object it will eventually be called on; every later call in the chain runs on whatever the previous one returned. Nothing actually executes until the chain is triggered -- either explicitly, via "call" or "run", or implicitly, by calling any other not-already-defined method name on it (see "IMPLICIT EXECUTION").

LazyMethod objects are immutable: "method" always returns a new LazyMethod with one more step appended, leaving the original chain untouched. This also means a chain can safely be triggered more than once -- each run replays the whole sequence of calls from scratch (assuming the underlying object and methods involved have no side effects that would make repeated calls behave differently).

LazyMethod objects aren't normally created directly; they come from calling .method(name, *args) or .methods(*args) on any Sidef object, both provided by the common base class -- see "method" in Sidef::Object::Object and "methods" in Sidef::Object::Object. Once created, the object supplied to that very first call is fixed for the lifetime of the chain: there's no way to later swap in a different starting object and replay the same chain against it. What extra arguments given to "call"/"run" can do is extend the arguments of the last step in the chain -- see "call" for details.

SYNOPSIS

# Build a chain with the 'method' method, then run it with 'call'
var lazy = [1, 2, 3, 4, 5].method('map', {|x| x * 2 })
lazy = lazy.method('grep', {|x| x > 5 })
lazy = lazy.method('sum')

say(lazy.call)    #=> 30

# Or chain and execute in one expression
say([1, 2, 3, 4, 5].method('map', {|x| x ** 2 }).method('sum').call)    #=> 55

# Using '.methods()' to get a Hash of every method, pre-bound to the object
var array_methods = [1, 2, 3].methods
say(array_methods{:sum}.call)    #=> 6

INHERITS

Inherits methods from Sidef::Object::Object.

CONSTRUCTION

new

LazyMethod.new(call, call, ...)

Creates a LazyMethod object directly from a list of raw internal call-descriptors (hashes with method/args keys, and an obj key on the first one). This is an implementation detail used internally by "method" in Sidef::Object::Object and by "method" below -- user code should build chains with .method(name, *args) instead of calling this directly.

CHAINING

method

self.method(name, *args)

Returns a new LazyMethod with one more step appended to the end of the chain: when eventually triggered, this step will call the method named name, with args, on whatever the previous step produced (or, if this is the very first step in the chain, on the object the chain was originally created from). self itself is left unchanged.

var lazy = [1, 2, 3, 4, 5].method('map', {|x| x ** 2 })
lazy = lazy.method('grep', {|x| x > 10 })
lazy = lazy.method('sum')

say(lazy.call)    #=> 41  # 16 + 25

EXECUTION

call

self.call(*extra_args)

Runs every step of the chain in order -- the first step's method is called on the chain's original object, and each following step's method is called on the previous step's result -- and returns the result of the last step.

Any extra_args given to call are appended after the last step's own pre-configured arguments (they do not go to the first step, even though the first step is where the chain's object was set):

var array_methods = [1, 2, 3].methods
# array_methods{:map} is a chain whose only step is 'map', with no args yet
say(array_methods{:map}.method('sum').call({|x| x * 2 }))    #=> 12
# 'map' runs first with no block (its own configured args), THEN
# 'sum' runs with the block passed to call() appended to its own args

call is not a separately implemented method -- it's one of the two names (see "run") specially recognized by the same fallback mechanism described in "IMPLICIT EXECUTION".

Aliases: run

IMPLICIT EXECUTION

Calling any method name on a LazyMethod object that isn't already defined -- neither on LazyMethod itself ("new", "method", "call", "run") nor inherited from Sidef::Object::Object (class, dump, is_a, and so on) -- triggers the chain via Perl's AUTOLOAD mechanism: the whole chain runs exactly as "call" would (but without forwarding any arguments to the last step), and then the requested method is called, with whatever arguments were given, on that final result.

var lazy = [1, 2, 3].method('map', {|x| x * 2 })

# These are equivalent:
say(lazy.call.sum)    # explicit: run the chain, then call .sum
say(lazy.sum)         # implicit: 'sum' isn't a chain method, so it
                      # triggers execution and is then called on the result

Because inherited methods are resolved normally before AUTOLOAD is ever consulted, calling something like .class or .dump on a LazyMethod object reports on the LazyMethod object itself (its class, its internal structure) rather than triggering the chain -- only names with no existing definition anywhere in the inheritance chain fall through to this behavior.

EXAMPLES

Deferred computation

Build a chain without running it, and only trigger it when the result is actually needed:

func create_processor(data) {
    data.method('grep', {|x| x.is_prime }) \
        .method('map', {|x| x ** 2 }) \
        .method('sum')
}

var lazy_result = create_processor(1..1000)
# No computation has happened yet

if (user_wants_result) {
    say(lazy_result.call)    # now it executes
}

Method exploration

.methods() returns every available method pre-bound to the object, letting you look up and try one by name:

var str_methods = "hello".methods

say(str_methods.keys.sort.first(10))

say(str_methods{:uc}.call)       #=> "HELLO"
say(str_methods{:reverse}.call)  #=> "olleh"
say(str_methods{:chars}.call)    #=> ["h", "e", "l", "l", "o"]

Functional composition

Build up a multi-step pipeline one .method() call at a time:

var process = (1..20).method('grep', {|x| x.is_even }) \
                     .method('map', {|x| x ** 2 }) \
                     .method('grep', {|x| x > 50 }) \
                     .method('sum')

say(process.call)    # sum of even squares greater than 50

IMPLEMENTATION NOTES

  • A LazyMethod object stores the complete chain of method-call descriptors (name + arguments); only the first one also carries the original object.

  • Each call to "method" returns a new LazyMethod with the extended chain -- LazyMethod objects are immutable.

  • Execution is sequential: the first step's method runs on the original object, and each following step runs on the previous step's result.

  • "call" and "run" are the exact same underlying code path -- there is no behavioral difference between them.

  • There's no way to swap out the original object bound at chain creation and re-run the same chain against a different starting value.

SEE ALSO