sunny / actor

Composable Ruby service objects

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Positional arguments support?

zhengpd opened this issue · comments

It's not pleasant to type full keyword arguments all the time for actors with only one or two inputs. So I figured out a way to run actor with positional arguments:

class ApplicationActor < Actor
  class << self
    attr_reader :defined_run_args

    # Define positional args for Actor.run()
    # Should be called after all inputs defined
    #
    # class HiActor < ApplicationActor
    #   input :planet
    #   input :name
    #
    #   has_run_args :name, :planet
    #
    #   def call
    #     puts "Hi, #{name} from #{planet}"
    #   end
    # end
    #
    # HiActor.run!('Dan', 'Earth')
    #
    def has_run_args(*args)
      if args.size != inputs.keys.size
        raise ServiceActor::ArgumentError, 'Run args must match inputs'
      end

      const_set('RUN_ARGS', args.freeze)
      @defined_run_args = true
    end

    def run!(*args)
      raise ServiceActor::ArgumentError, 'Actor run args not defined!' unless defined_run_args

      args_h = self::RUN_ARGS.map.with_index { |key, idx| [key, args[idx]] }.to_h
      call(args_h)
    end
  end
end

Just a small run! method built on top of call.

Is positional arguments support on the roadmap of Actor?

Hi @zhengpd! I didn’t think this would be useful, but I guess why not.

Suggestions:

  • Instead of introducing an extra has_run_args, perhaps we could use the order in which inputs are added in the first place? (Internally this would be the inputs method.)
  • Perhaps we don’t need to raise an error if less args are given, which would allow optional arguments.
  • I guess we should also add an extra run method with no ! that would call result instead of call.

I’m open for a PR \o/

Hi @sunny , after a second thought, I think it's not much value in supporting positional arguments.

Case A: 4 and more inputs

It's not a good idea to define a method with so many positional arguments, so let's ignore this case.

Case B: 2 or 3 inputs

2 or 3 positional arguments seems reasonable. However, the implementation of positional method (eg. run!) could make the user's project code hard to maintain.

The implementation, going with has_run_args or relying on internal inputs order, would mean that developers have to carefully craft the order of defining inputs. If a developer re-sort the inputs in future, it might cause unnoticeable bug when inputs are the same type. For example:

class DivisionActor < Actor
  input :y
  input :x

  def call; y / x; end
end

Positional call is like Actor.run!(4, 2). Someday someone might think inputs should be defined in alphabetical ASC order and refactor to

class DivisionActor < Actor
  input :x
  input :y

  def call; y / x; end
end

Now the Actor.run!(4, 2) succeeds with incorrect result💥! Such change is easy to be overlooked because the logic is not changed and normally sorting code lines wouldn't cause error. It adds a burden to developers having to know inputs can't be rearranged or new input can't be inserted between old inputs' lines.

So, positional arguments like this just adds more complexity to coding 😞 .

Case C: 1 input

The difference between HiActor.run('Earth') and HiActor.run(planet: 'Earth') is pretty small. Not much value in supporting it by adding more complexity into Actor gem, which I prefer to avoid.

Conclusion

It's better to support positional arguments with some other new solution. I'll close this for now.

I appreciate the awesome thought process behind this, and happy that you are considering keeping the codebase simple. I agree with your points! Thank you 🎉