The Handle Pattern - Haskell

Welcome to the Functional Programming Zulip Chat Archive. You can join the chat here.

tristanC

Hello, I recently discovered the handle pattern and it looks too good to be true :) I first tried the mtl style approach with the ReaderT pattern but I find it too difficult to implement custom instances for testing. With the Handle pattern it seems simple to create a new handle with pure functions to mock the IOs, without using special extensions or typeclass.

So I am trying this pattern with an application slightly more complicated than i am used to do in Haskell. I am working on a service to query monitoring alerts, then run custom command and send report to operators over irc... Is this a good design to begin with and would you recommend it?

James King

I don't know how you would measure, "good." If it was me I would be satisfied if it allowed me to update my application as it evolved without too much hassle.

If I'm building a web application and I can change the database serialization code without affecting the route-plumbing code then that's good!

If I end up having to structure my application in highly-coupled ways in order to satisfy the architecture... I find that not-so-good.

James King

If the Handle Pattern works for you, it's probably a good pattern. :)

TheMatten

I would argue that variant of such approach could actually be treated as full-fledged effect system, and that it isn't really that far of existing ones:
Let's have

data App m = App
  { createUser :: Text -> m User
  , getUserMail :: User -> m [Mail]
  , log :: DebugLevel -> Text -> m ()
  , ..
  }

Just by parametrizing handle over monad, you "recover purity" by abstracting out IO and easily get injection - you can run your app in mocked environment, multiple alternative environemts (e.g. on client and server) or easily modify or swap final monad if it no longer fits your needs. You simply use it like

foo :: App m -> .. -> m Stuff
foo h = do
  ..
  user <- createUser h "Tristan"
  log h "Created new user"
  ..

If your app grows in size and responsibilites of specific parts get more concrete, you can split it's fields into subhandles that you can then pass to specific subparts - this way you get more flexible if those subparts need different set of functionality from their monads.

One cherry on top - with RecordDotSyntax we'll be able to write

h.createUser "Tristan"

And another one - you can use ImplicitParams to recover implicit handle passing:

type HasApp m = ?app :: App m

main = let ?app = stuff in ..

foo :: HasApp m => .. -> m Stuff
foo = do
  ..
  user <- ?app.createUser "Tristran"
  ?app.log "Created new user"
  ..

bar :: HasApp m => .. -> m OtherStuff
bar = do
  ..
  stuff <- foo ..
  ..

All of that without traditional "class mess" :smile:

Torsten Schmits

isn't that tagless final?

TheMatten

Well, thing is, in Haskell, all "ad-hoc polymorphism" is some sort of "tagless final" at the end :big_smile:

tristanC

Thank you very much for the insightful comments. If I understand correctly, handles are not as fine grained as other effect system, but designing my application with handles works for me so far, and most importantly, it is easy to explain. I also like the consistency of module that exports a common Config, Handle and withHandle api.

Parametrizing the handle over monad looks like a great improvement, thank you for the snippets @TheMatten ! It's also nice to see how ImplicitParams can be used, though I rather avoid using extensions so that i don't have to explain them :-)

Joel McCracken

we're using it. So far i haven't found it to be too annoying

Joel McCracken

which is, i think, the kind of what you hope to have from a thing that is a whole-application infrastructure thing. because as soon as you touch it you're going to be fixing other parts of the codebase which have nothing to do with what you're working on, so guaranteed annoying

HateUsernames007

@tristanC could you contrast this handle pattern with Polysemy?

HateUsernames007

@Joel McCracken what would you consider switching to if migration wasn't a problem

tristanC

@HateUsernames007 I'm not very familiar with Polysemy, but it seems like the handle pattern is simpler (e.g. it does not requiresextensions or complex types), and I guess Polysemy enables more ergonomic composition by using type constrain instead of explicit arguments?

Joel McCracken

well since I wrote this we actually kinda switched to MTL style stubbing, but its not a pure one or the other.

Joel McCracken

and I am happy with this.

HateUsernames007

@tristanC There is a good tutorial that explains the workings quite well https://archive.vn/wip/fWVFR . Yes it does require many extensions that you need to be familiar with, but with a experienced programmer designing the structure I think a novice would be able to jump in and and changes even if they do not understand all of it. Lastly, I do like how it uses constraints so that the code is far more modular and not dependent on the central Handle type, are you able to easily swap handles for tests or sharing between apps?

HateUsernames007

@Joel McCracken so you are still using the handle but using MTL constraints for your function types?

tristanC

@HateUsernames007 thank you for the tutorial, effect systems like polysemy do look nice, but as you said, it requires experience... With the handle pattern, i'm passing the dependency explicitly and it is quite simple to create mock in tests.

HateUsernames007

Too many options in life. Thanks for the explanations. I'll look into handles more

Joel McCracken

We still have the handle pattern in part of the application, but now when we need to "stub" I/O we are reaching for MTL instead, which lets us keep the signatures smaller and more focused

Joel McCracken

Are you trying to come up with something to use for your own project?

Daniel Díaz Carrete

fwiw, I've recently released two packages "dep-t" and "dep-t-advice" for working with the ReaderT / record-of-functions / handle pattern http://hackage.haskell.org/package/dep-t http://hackage.haskell.org/package/dep-t-advice