Interactors in Hanami 2.1

Hi,

I have a general questions regarding interactors. I’m starting with 2.1 and previously only used 1.3 with interactors. Since there has been quite some changes regarding the app structure, what is the general view on/approach to interactors in 2.1? I don’t see any reference in the guides.

Best, seba

Hey,

There is no reference because they have been dropped from the framework. I saw no mention of a preferred solution, but I saw plenty of comments from solnic and others from core team, about leaving the choice up to the developer.

Since a large portion of Hanami is built upon dry-rb family of gems, I would assume dry-monads is a nice fit, I however usually prefer to build something upon POROs and see where it gets me. After spending few years with trailblazer operations I aim for the simplest, minimalistic solutions first. Might be an approach that fits the hanami philosophy.

This is the only other topic directly touching on interactors not being transfered from 1.3 to 2.x

Thank you for the link and explanation. I see dry-monads and dry-transactions mentioned. Excuse the question, but could components replace interactors out of the box or am I misreading the guides?

Component is used as a general name, for objects that “get stuff done”, execute your business domain logic etc. Those could be dry-monads, dry-transactions, PORO, service objects, trailblazer operations etc.

You could probably port interactors to be used as components, since it is a general term, that does not force anything specific. I see that guides use the example of “operation” and “rendered” both, being components. Same goes for interactors if you want it to.

The guides say that Hanami handles managment of components, it does not handle implementation of said components. That is up to you.

Hopefuly my explanation is clear (and correct, :stuck_out_tongue: )

Thank you, again. :slight_smile:
I’ll probably stick with the name interactors and put them in the app directory and try to keep it as simple as possible. Browsing for the topic, I’ve just stumbled upon this gem (pun intended) :grinning:.

I’ve built very complex business logic atop dry-monad’s do-notation. All you need is dependency injection from the framework + monad unwrapping from dry-monad. This is a better foundation for PORO service objects than any other framework I have tried.

1 Like

Thank you for all the great conversation here, folks! :heart_eyes:

For the current moment, my recommendation would be to follow @alassek’s advice:

As for Hanami providing something like this out of the box, we plan to have the in-development dry-operation gem completed and included in Hanami apps by default for v2.2.

dry-operation is just a slightly higher-level abstraction over dry-monads, so if you got started there in the meantime, that will place you in good stead to migrate in the future.

I’ll also make sure we announce it here in this forum when dry-operation is ready for some beta testing.

3 Likes

I cobbled together an object to handle this role for me. I call it ProtoInteractor. I don’t love the name, but it was a prototype to replace the old Hanami Interactor, so . . .

You can read about it here and find it on GitLab here.

My goal was:

  • The params functionality of Hanami Actions. This is because I want to manage parameters at the interactor level, not the action level.
  • Do notation from dry-monads.
  • Result objects, also from dry-monads for return values.

ProtoInteractor is opinionated in two ways:

  1. Handling parameters in the interactor, instead of the actions. I do this because parsing of params is a business concern, not a framework concern (in my view, obviously :nerd_face: ).
  2. The interactor expects to receive two argument objects: a user, and params. I do this because I would like to handle authorization in the interactor. That is, I want the interactor to know “who’s asking,” even if the answer is a unauthenticated user or a “system” user.

ProtoInteractor is working well for me so far, but I’m not sure if it’s ready for general release–or meets a community need–so I haven’t officially published it as a gem. Plus I don’t really like the name.

Feel free to try it out and let me know what you think. If you disagree with any of my opinions, you can read how I went about it in my article and modify it however you like.

As @alassek and @timriley pointed out, you can get pretty far with just Do notation and Result objects. My article gives a brief overview of both, if you are not familiar.

Good luck!

Here’s a helper method I make extensive use of: it’s called either and its purpose is to facilitate wrapping error results in tuples.

My Failure results always take the form of Failure[:error_name, *args] unless I am wrapping an exception. This makes pattern-matching errors from the outside much easier.

  # Utility helper to avoid .to_monad.or {} chaining
  #
  # @param [#to_monad] result, Dry::Monads::Result or object that responds to `to_monad`
  # @param [Dry::Monads::Result::Failure, #to_proc, { :error => Symbol }] error
  #
  # @example With Yield
  #   user = yield fetch_user.(id).or { |err| Failure[:not_found, err] }
  #
  # @example With Either
  #   user = either fetch_user.(id), Failure(:not_found)
  #   user = either fetch_user.(id), ->(err) { Failure[:not_found, err] }
  #   user = either fetch_user.(ud), error: :not_found
  #
  # @raise [Dry::Monads::Do::Halt]
  #
  # @return the unwrapped successful value
  def either(result, error)
    failure =
      case error
      in { error: error_name }
        proc { |err| Failure[error_name, err] }
      in T::Procable
        error
      else
        proc { error }
      end

    Dry::Monads::Do.bind result.to_monad.or(&failure)
  end

T::Procable is just a type defined as

Procable = Interface(:to_proc)
4 Likes

This post is purely to help get you started on your new blogging career. :wink:

I would like to know more about your use cases for either. I find it interesting that the first argument to either is already a Result object or something else that responds to to_monad.

From the examples in the rdoc, fetch_user.(id), you seem to be calling another object (perhaps a repository). Do you structure your other objects to return Result objects? I am mostly only using Result objects in my commands/operations/interactors.

Can you discuss that? and perhaps a use case for each of the three examples in the rdoc? The first use case looks similar to some things I’m doing, but when would you pass a proc or a hash as the error value?

As a related topic, I have just begun using Do notation in my ProtoInteractor and I’m curious about other “best practices” or idiomatic patterns that have emerged.

For example, I often yield to private methods that encapsulate some task and return a Result object. Some of these can’t fail in any meaningful sense, like retrieving all comments by a user. Assuming that the user exists, then getting all comments will never “fail,” but it might return an empty array if no comments exist. In this case I always return a Success (assuming the situation still calls for a Result object).

def get_comments(user_id)
  comments = comments_repo.fetch_by_user_id(user_id)

  Success(comments)
end

If I’m looking for a single record, and the absence of a match is considered a failure, then I return Success or Failure, as appropriate. But my pattern for selecting the return value has evolved.

I could do:

def get_user(id)
  user = user_repo.by_id(id)

  user ? Success(user) : Failure(:not_found)
end

I also like (for maximum clarity):

  if !user.nil?
    Success(user)
  else
    Failure(:not_found)
  end

But today I decided to add Maybe to ProtoInteractor so I could do this instead:

  Maybe(user).to_result(:not_found)

In similar cases where a single match is not found and I’m not returning a Result object, but I would prefer to return some kind of empty object to returning nil, I could use Maybe like this:

  Maybe(user).value_or({})

Where the empty hash could be replaced with an empty struct, if appropriate.

Several ideas occur to me in relation to using Maybe in this way, such as:

  • Similar to one of your use cases for either, you could replace my one-line private methods with yield Maybe(user_repo.by_id(id)).to_result(:not_found) right in main method (using Do).

  • The general use case for ternaries-to-manage-nil (possible_nil ? possible_nil : alternative_to_nil) could be replaced with Maybe(possible_nil_value).value_or(replacement_for_nil) (or .to_result(:nil_error), if appropriate).

What do you (or anyone else! trying to learn here!) think of these patterns? I’m using Result and Maybe in my interactors. Is anyone using them anywhere else? Has Maybe or Result found broader usage in Ruby? What do you think about Maybe and Result replacing existing awkward patterns in Ruby, like (arguably) ternaries?