Module: Roby::Models::Task

Includes:
MetaRuby::ModelAsClass, Arguments
Included in:
Task
Defined in:
lib/roby/models/task.rb

Defined Under Namespace

Classes: AsPlanProxy, Template, TemplateEventGenerator

Constant Summary collapse

@@exception_handler_id =
0

Constants included from Arguments

Arguments::NO_DEFAULT_ARGUMENT

Event Relations collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Arguments

#argument, #arguments, #default_argument, #meaningful_arguments

Class Method Details

.model_attribute_list(name) ⇒ Object

Declares an attribute set which follows the task models inheritance hierarchy. Define the corresponding enumeration methods as well.

For instance,

model_attribute_list 'signal'

defines the model-level signals, which can be accessed through

.each_signal(model)
.signals(model)
#each_signal(model)


253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/roby/models/task.rb', line 253

def self.model_attribute_list(name) # :nodoc:
    class_eval <<-EOD, __FILE__, __LINE__+1
        inherited_attribute("#{name}_set", "#{name}_sets", map: true) { Hash.new { |h, k| h[k] = Set.new } }
        def each_#{name}(model)
            for obj in #{name}s(model)
                yield(obj)
            end
            self
        end
        def #{name}s(model)
            result = Set.new
            each_#{name}_set(model, false) do |set|
                result.merge set
            end
            result
        end

        def all_#{name}s
            if @all_#{name}s
                @all_#{name}s
            else
                result = Hash.new
                each_#{name}_set do |from, targets|
                    result[from] ||= Set.new
                    result[from].merge(targets)
                end
                @all_#{name}s = result
            end
        end
    EOD
end

.model_relation(name) ⇒ Object



285
286
287
# File 'lib/roby/models/task.rb', line 285

def self.model_relation(name)
    model_attribute_list(name)
end

Instance Method Details

#abstractObject

Declare that this task model defines abstract tasks. Abstract tasks can be used to represent an action, without specifically representing how this action should be done.

Instances of abstract task models are not executable, i.e. they cannot be started.

See Also:

  • executable?


461
462
463
# File 'lib/roby/models/task.rb', line 461

def abstract
    @abstract = true
end

#all_modelsObject

Deprecated.

Use #each_submodel instead



221
222
223
# File 'lib/roby/models/task.rb', line 221

def all_models
    submodels
end

#as_plan(arguments = Hash.new) ⇒ Object

Default implementation of the #as_plan method

The #as_plan method is used to use task models as representation of abstract actions. For instance, if an #as_plan method is available on a particular MoveTo task model, one can do

root.depends_on(MoveTo)

This default implementation looks for planning methods declared in the main Roby application planners that return the required task type or one of its subclasses. If one is found, it is using it to generate the action. Otherwise, it falls back to returning a new instance of this task model, unless the model is abstract in which case it raises ArgumentError.

It can be used with

class TaskModel < Roby::Task
end

root = Roby::Task.new
child = root.depends_on(TaskModel)

If arguments need to be given, the #with_arguments method should be used:

root = Roby::Task.new
child = root.depends_on(TaskModel.with_arguments(id: 200))


208
209
210
211
212
213
214
215
216
# File 'lib/roby/models/task.rb', line 208

def as_plan(arguments = Hash.new)
    Roby.app.prepare_action(self, **arguments).first
rescue Application::ActionResolutionError
    if abstract?
        raise Application::ActionResolutionError, "#{self} is abstract and no planning method exists that returns it"
    else
        new(arguments)
    end
end

#can_merge?(target_model) ⇒ Boolean

Returns:

  • (Boolean)


826
827
828
# File 'lib/roby/models/task.rb', line 826

def can_merge?(target_model)
    fullfills?(target_model)
end

Establish model-level causal links between events of that task. These signals will be established on all the instances of this task model (and its subclasses).

Causal links are used during event propagation to order the propagation properly. Establish a causal link when e.g. an event handler might call or emit on another of this task’s event

Examples:

when establishing multiple relations from the same source use name-to-arrays

signal start: [:one, :two]

Parameters:

  • mappings (Hash<Symbol,Array<Symbol>>, Hash<Symbol,Symbol>)

    the source-to-target mappings



353
354
355
356
357
358
359
# File 'lib/roby/models/task.rb', line 353

def causal_link(mappings)
    mappings.each do |from, to|
        from = event_model(from).symbol
        causal_link_sets[from].merge Array[*to].map { |ev| event_model(ev).symbol }
    end
    update_terminal_flag
end

#clear_modelObject

Clears all definitions saved in this model. This is to be used by the reloading code



227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/roby/models/task.rb', line 227

def clear_model
    class_eval do
        # Remove event models
        events.each_key do |ev_symbol|
            remove_const ev_symbol.to_s.camelcase(:upper)
        end

        [@events, @signal_sets, @forwarding_sets, @causal_link_sets,
            @arguments, @handler_sets, @precondition_sets].each do |set|
            set.clear if set
        end
    end
    super
end

#compute_terminal_events(events) ⇒ Object



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/roby/models/task.rb', line 147

def compute_terminal_events(events)
    success_events, failure_events, terminal_events =
        [events[:success]].to_set, 
        [events[:failed]].to_set,
        [events[:stop], events[:success], events[:failed]].to_set

    event_set = events.values.to_set
    discover_terminal_events(event_set, terminal_events, success_events, events[:success])
    discover_terminal_events(event_set, terminal_events, failure_events, events[:failed])
    discover_terminal_events(event_set, terminal_events, nil, events[:stop])

    events.each_value do |ev|
        if ev.event_model.terminal?
            if !success_events.include?(ev) && !failure_events.include?(ev)
                terminal_events << ev 
            end
        end
    end

    return terminal_events, success_events, failure_events
end

#define_command_method(event_name, block) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Define the method that will be used as command for the given event

Parameters:

  • event_name (Symbol)

    the event name



565
566
567
568
569
570
571
572
# File 'lib/roby/models/task.rb', line 565

def define_command_method(event_name, block)
    check_arity(block, 1, strict: true)
    define_method("event_command_#{event_name}", &block)
    method = instance_method("event_command_#{event_name}")
    lambda do |dst_task, *event_context| 
        method.bind(dst_task).call(*event_context) 
    end
end

#define_event_methods(event_name) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Define support methods for a task event

Parameters:

  • event_name (Symbol)

    the event name



579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
# File 'lib/roby/models/task.rb', line 579

def define_event_methods(event_name)
    event_name = event_name.to_sym
    if !method_defined?("#{event_name}_event")
        define_method("#{event_name}_event") do
            @bound_events[event_name] || event(event_name)
        end
    end
    if !method_defined?("#{event_name}?")
        define_method("#{event_name}?") do
            (@bound_events[event_name] || event(event_name)).emitted?
        end
    end
    if !method_defined?("#{event_name}!")
        define_method("#{event_name}!") do |*context| 
            (@bound_events[event_name] || event(event_name)).call(*context)
        end
    end
    if !respond_to?("#{event_name}_event")
        singleton_class.class_eval do
            define_method("#{event_name}_event") do
                find_event_model(event_name)
            end
        end
    end
end

#discover_terminal_events(events, terminal_set, set, root) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/roby/models/task.rb', line 127

def discover_terminal_events(events, terminal_set, set, root)
    stack = [root]
    while !stack.empty?
        vertex = stack.shift
        for relation in [EventStructure::Signal, EventStructure::Forwarding]
            for parent in vertex.parent_objects(relation)
                if !events.include?(parent)
                    next
                elsif parent[vertex, relation]
                    next
                elsif !terminal_set.include?(parent)
                    terminal_set  << parent
                    set   << parent if set
                    stack << parent
                end
            end
        end
    end
end

#enum_eventsObject

:nodoc



640
641
642
643
# File 'lib/roby/models/task.rb', line 640

def enum_events # :nodoc
    Roby.warn_deprecated "#enum_events is deprecated, use #each_event without a block instead"
    each_event
end

#event(event_name, options = Hash.new, &block) ⇒ Hash<Symbol,TaskEvent>

The events defined by the task model

Returns:



524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
# File 'lib/roby/models/task.rb', line 524

def event(event_name, options = Hash.new, &block)
    event_name = event_name.to_sym

    options = validate_options options,
        controlable: nil, command: nil, terminal: nil,
        model: find_event_model(event_name) || Roby::TaskEvent

    if options.has_key?(:controlable)
        options[:command] = options[:controlable]
    elsif !options.has_key?(:command) && block
        options[:command] = define_command_method(event_name, block)
    end
    validate_event_definition_request(event_name, options)

    # Define the event class
    new_event = options[:model].new_submodel task_model: self,
        terminal: options[:terminal],
        symbol: event_name, command: options[:command]
    new_event.permanent_model = self.permanent_model?

    setup_terminal_handler = false
    old_model = find_event_model(event_name)
    if new_event.symbol != :stop && options[:terminal] && (!old_model || !old_model.terminal?)
        setup_terminal_handler = true
    end

    events[new_event.symbol] = new_event
    if setup_terminal_handler
        forward(new_event => :stop)
    end
    const_set(event_name.to_s.camelcase(:upper), new_event)

    define_event_methods(event_name)
    new_event
end

#event_model(model_def) ⇒ Model<TaskEvent>

Accesses an event model

This method gives access to this task’s event models. If given a name, it returns the corresponding event model. If given an event model, it verifies that the model is part of the events of self and returns it.

Returns:

  • (Model<TaskEvent>)

    a subclass of Roby::TaskEvent

Raises:

  • (ArgumentError)

    if the provided event name or model does not exist on self



666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
# File 'lib/roby/models/task.rb', line 666

def event_model(model_def) #:nodoc:
    if model_def.respond_to?(:to_sym)
        ev_model = find_event_model(model_def.to_sym)
        unless ev_model
            all_events = each_event.map { |name, _| name }
            raise ArgumentError, "#{model_def} is not an event of #{name}: #{all_events}" unless ev_model
        end
    elsif model_def.respond_to?(:has_ancestor?) && model_def.has_ancestor?(Roby::TaskEvent)
        # Check that model_def is an event class for us
        ev_model = find_event_model(model_def.symbol)
        if !ev_model
            raise ArgumentError, "no #{model_def.symbol} event in #{name}"
        elsif ev_model != model_def
            raise ArgumentError, "the event model #{model_def} is not a model for #{name} (found #{ev_model} with the same name)"
        end
    else 
        raise ArgumentError, "wanted either a symbol or an event class, got #{model_def}"
    end

    ev_model
end

#find_event_model(name) ⇒ Object Also known as: has_event?

Find the event class for event, or nil if event is not an event name for this model



652
653
654
# File 'lib/roby/models/task.rb', line 652

def find_event_model(name)
    find_event(name.to_sym)
end

#forward(mappings) ⇒ Object

Establish model-level forwarding between events of that task. These relations will be established on all the instances of this task model (and its subclasses).

Forwarding is used to cause the target event to be emitted when the source event is.

Examples:

# A task that is stopped as soon as it is started
class MyTask < Roby::Task
  forward start: :stop
end

Parameters:

  • mappings (Hash<Symbol,Array<Symbol>>, Hash<Symbol,Symbol>)

    the source-to-target mappings

See Also:

  • #forward
  • EventGenerator#forward.
  • the forwarding relation.


378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'lib/roby/models/task.rb', line 378

def forward(mappings)
    mappings.each do |from, to|
        from    = event_model(from).symbol
        targets = Array[*to].map { |ev| event_model(ev).symbol }

        if event_model(from).terminal?
            non_terminal = targets.find_all { |name| !event_model(name).terminal? }
            if !non_terminal.empty?
                raise ArgumentError, "trying to establish a forwarding relation from the terminal event #{from} to the non-terminal event(s) #{targets}"
            end
        end

        forwarding_sets[from].merge targets
    end
    update_terminal_flag
end

#from(object) ⇒ Object

Helper method to define delayed arguments from related objects

Examples:

propagate an argument from a parent task

argument :target, default: from(:parent).target


401
402
403
404
405
406
407
# File 'lib/roby/models/task.rb', line 401

def from(object)
    if object.kind_of?(Symbol)
        Roby.from(nil).send(object)
    else
        Roby.from(object)
    end
end

#from_state(state_object = State) ⇒ Object

Helper method to define delayed arguments from the State object

Examples:

get an argument from the State object

argument :initial_pose, default: from_state.pose


413
414
415
# File 'lib/roby/models/task.rb', line 413

def from_state(state_object = State)
    Roby.from_state(state_object)
end

#fullfills?(models) ⇒ Boolean

Returns:

  • (Boolean)


812
813
814
815
816
817
818
819
820
821
822
823
824
# File 'lib/roby/models/task.rb', line 812

def fullfills?(models)
    if models.respond_to?(:each)
        models = models.to_a
    else models = [models]
    end

    models.each do |m|
        m.each_fullfilled_model do |test_m|
            return false if !has_ancestor?(test_m)
        end
    end
    return true
end

#instantiate_event_relations(template) ⇒ Object



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/roby/models/task.rb', line 58

def instantiate_event_relations(template)
    events = template.events_by_name

    all_signals.each do |generator, signalled_events|
        next if signalled_events.empty?
        generator = events[generator]

        for signalled in signalled_events
            signalled = events[signalled]
            generator.signals signalled
        end
    end

    all_forwardings.each do |generator, signalled_events|
        next if signalled_events.empty?
        generator = events[generator]

        for signalled in signalled_events
            signalled = events[signalled]
            generator.forward_to signalled
        end
    end

    all_causal_links.each do |generator, signalled_events|
        next if signalled_events.empty?
        generator = events[generator]

        for signalled in signalled_events
            signalled = events[signalled]
            generator.add_causal_link signalled
        end
    end

    # Add a link from internal_event to stop if stop is controllable
    if events[:stop].controlable?
        events[:internal_error].signals events[:stop]
    end

    terminal_events, success_events, failure_events =
        compute_terminal_events(events)

    template.terminal_events = terminal_events
    template.success_events   = success_events
    template.failure_events  = failure_events
    start_event = events[:start]

    # WARN: the start event CAN be terminal: it can be a signal from
    # :start to a terminal event
    #
    # Create the precedence relations between 'normal' events and the terminal events
    root_terminal_events = terminal_events.find_all do |ev|
        (ev != start_event) && ev.root?(Roby::EventStructure::Precedence)
    end

    events.each_value do |ev|
        next if ev == start_event
        if !terminal_events.include?(ev)
            if ev.root?(Roby::EventStructure::Precedence)
                start_event.add_precedence(ev)
            end
            if ev.leaf?(Roby::EventStructure::Precedence)
                for terminal in root_terminal_events
                    ev.add_precedence(terminal)
                end
            end
        end
    end
end

#interruptibleObject

Declare that tasks of this model can be interrupted by calling the command of Task#failed_event

Raises:

  • (ArgumentError)

    if Task#failed_event is not controlable.



434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/roby/models/task.rb', line 434

def interruptible
    if !has_event?(:failed) || !event_model(:failed).controlable?
        raise ArgumentError, "failed is not controlable"
    end

    event(:stop) do |context| 
        if starting?
            start_event.signals stop_event
            return
        end
        failed!(context)
    end
end

#invalidate_templateObject



40
41
42
# File 'lib/roby/models/task.rb', line 40

def invalidate_template
    @template = nil
end

#match(*args) ⇒ Object

Returns a TaskMatcher object that matches this task model



796
797
798
799
800
801
802
803
804
# File 'lib/roby/models/task.rb', line 796

def match(*args)
    matcher = Queries::TaskMatcher.new
    if args.empty? && self != Task
        matcher.which_fullfills(self)
    else
        matcher.which_fullfills(*args)
    end
    matcher
end

#on(*event_names) {|context| ... } ⇒ Object

Adds an event handler for the given event model. The block is going to be called whenever some events are emitted.

Unlike a block given to EventGenerator#on, the block is evaluated in the context of the task instance.

Parameters:

  • event_names (Array<Symbol>)

    the name of the events on which to install the handler

Yield Parameters:

  • context (Object)

    the arguments passed to Task#emit when the event was emitted



703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
# File 'lib/roby/models/task.rb', line 703

def on(*event_names, &user_handler)
    if !user_handler
        raise ArgumentError, "#on called without a block"
    end

    check_arity(user_handler, 1, strict: true)
    event_names.each do |from|
        from = event_model(from).symbol
        if user_handler 
            method_name = "event_handler_#{from}_#{Object.address_from_id(user_handler.object_id).to_s(16)}"
            define_method(method_name, &user_handler)

            handler = lambda { |event| event.task.send(method_name, event) }
            handler_sets[from] << EventGenerator::EventHandler.new(handler, false, false)
        end
    end
end

#on_exception(matcher) {|exception| ... } ⇒ Object

Defines an exception handler.

When propagating exceptions, ExecutionException goes up in the task hierarchy and calls matching handlers on the tasks it finds, and on their planning task. The first matching handler is called, and the exception propagation assumes that it handled the exception (i.e. won’t look for new handlers) unless it calls Task#pass_exception

Examples:

install a handler for a TaskModelViolation exception

on_exception(TaskModelViolation, ...) do |task, exception_object|
    if cannot_handle
        task.pass_exception # send to the next handler
    end
    do_handle
end

Parameters:

Yield Parameters:



775
776
777
778
779
780
781
# File 'lib/roby/models/task.rb', line 775

def on_exception(matcher, &handler)
    check_arity(handler, 1, strict: true)
    matcher = matcher.to_execution_exception_matcher
    id = (@@exception_handler_id += 1)
    define_method("exception_handler_#{id}", &handler)
    exception_handlers.unshift [matcher, instance_method("exception_handler_#{id}")]
end

#poll(&block) ⇒ Object

Declares that the given block should be called at each execution cycle, when the task is running. Use it that way:

class MyTask < Roby::Task
  poll do
    ... do something ...
  end
end

If the given polling block raises an exception, the task will be terminated by emitting its failed event.



743
744
745
746
747
748
749
# File 'lib/roby/models/task.rb', line 743

def poll(&block)
    if !block_given?
        raise ArgumentError, "no block given"
    end

    define_method(:poll_handler, &block)
end

#precondition(event, reason, &block) ⇒ Object



722
723
724
725
# File 'lib/roby/models/task.rb', line 722

def precondition(event, reason, &block)
    event = event_model(event)
    precondition_sets[event.symbol] << [reason, block]
end

#provided_servicesObject

Returns the lists of tags this model fullfills.



728
729
730
# File 'lib/roby/models/task.rb', line 728

def provided_services
    ancestors.find_all { |m| m.kind_of?(Models::TaskServiceModel) }
end

#query(*args) ⇒ Object



785
786
787
788
789
790
791
792
793
# File 'lib/roby/models/task.rb', line 785

def query(*args)
    q = Queries::Query.new
    if args.empty? && self != Task
        q.which_fullfills(self)
    else
        q.which_fullfills(*args)
    end
    q
end

#signal(mappings) ⇒ Object

Establish model-level signals between events of that task. These signals will be established on all the instances of this task model (and its subclasses).

Signals cause the target event(s) command to be called when the source event is emitted.

Examples:

when establishing multiple relations from the same source use name-to-arrays

signal start: [:one, :two]

Parameters:

  • mappings (Hash<Symbol,Array<Symbol>>, Hash<Symbol,Symbol>)

    the source-to-target mappings

Raises:

  • (ArgumentError)

    if the target event is not controlable, i.e. not have a command



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/roby/models/task.rb', line 320

def signal(mappings)
    mappings.each do |from, to|
        from    = event_model(from)
        targets = Array[*to].map { |ev| event_model(ev) }

        if from.terminal?
            non_terminal = targets.find_all { |ev| !ev.terminal? }
            if !non_terminal.empty?
                raise ArgumentError, "trying to establish a signal from the terminal event #{from} to the non-terminal events #{non_terminal}"
            end
        end
        non_controlable = targets.find_all { |ev| !ev.controlable? }
        if !non_controlable.empty?
            raise ArgumentError, "trying to signal #{non_controlable.join(" ")} which is/are not controlable"
        end

        signal_sets[from.symbol].merge targets.map { |ev| ev.symbol }
    end
    update_terminal_flag
end

#templateObject

The plan that is used to instantiate this task model



45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/roby/models/task.rb', line 45

def template
    return @template if @template

    template = Template.new
    each_event do |event_name, event_model|
        template.add(event = TemplateEventGenerator.new(event_model.controlable?, event_model, plan: template))
        template.events_by_name[event_name] = event
    end

    instantiate_event_relations(template)
    @template = template
end

#terminal_eventsObject

Get the list of terminal events for this task model



646
647
648
649
# File 'lib/roby/models/task.rb', line 646

def terminal_events
    each_event.find_all { |_, e| e.terminal? }.
        map { |_, e| e }
end

#terminatesObject

Declare that tasks of this model can finish by simply emitting stop, i.e. with no specific action.

Examples:

class MyTask < Roby::Task
  terminates
end


425
426
427
428
# File 'lib/roby/models/task.rb', line 425

def terminates
    event :failed, command: true, terminal: true
    interruptible
end

#to_coordination_task(task_model) ⇒ Object



830
831
832
# File 'lib/roby/models/task.rb', line 830

def to_coordination_task(task_model)
    Roby::Coordination::Models::TaskFromAsPlan.new(self, self)
end

#to_execution_exception_matcherQueries::ExecutionExceptionMatcher

Returns an exception match object that matches exceptions originating from this task.

Returns:



808
809
810
# File 'lib/roby/models/task.rb', line 808

def to_execution_exception_matcher
    Queries::ExecutionExceptionMatcher.new.with_origin(self)
end

#update_terminal_flagObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Update the terminal flag for the event models that are defined in this task model. The event is terminal if model-level signals (#signal) or forwards (#forward) lead to the emission of #stop_event



471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# File 'lib/roby/models/task.rb', line 471

def update_terminal_flag # :nodoc:
    events = each_event.map { |name, _| name }
    terminal_events = [:stop]
    events.delete(:stop)

    loop do
        old_size = terminal_events.size
        events.delete_if do |ev|
            if signals(ev).any? { |sig_ev| terminal_events.include?(sig_ev) } ||
                forwardings(ev).any? { |sig_ev| terminal_events.include?(sig_ev) }
                terminal_events << ev
                true
            end
        end
        break if old_size == terminal_events.size
    end

    terminal_events.each do |sym|
        if ev = self.events[sym]
            ev.terminal = true
        else
            ev = superclass.event_model(sym)
            unless ev.terminal?
                event sym, model: ev, terminal: true, 
                    command: (ev.method(:call) rescue nil)
            end
        end
    end
end

#with_arguments(arguments = Hash.new) ⇒ Object

If this class model has an ‘as_plan’, this specifies what arguments should be passed to as_plan



171
172
173
174
175
176
177
# File 'lib/roby/models/task.rb', line 171

def with_arguments(arguments = Hash.new)
    if respond_to?(:as_plan)
        AsPlanProxy.new(self, arguments)
    else
        raise NoMethodError, "#with_arguments is invalid on #self, as #self does not have an #as_plan method"
    end
end