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