hreinhardt/amqp

Lack of type safety is distrubing

Closed this issue · 4 comments

Coming at this library from a functional programmer perspective (and ignoring the typing and conventions inherent in the RabbitMQ community), the library feels a bit too untyped.

Would you accept a patch? I'm thinking of addtions along the lines of:

newtype Queue = Queue Text
newtype RoutingKey = RoutingKey Text
instance IsString Queue where fromString  = Queue . fromString
instance IsString RoutingKey where fromString = RoutingKey . fromString
instance Show Queue where show (Queue q) = show q
instance Show RoutingKey where show (RoutingKey k) = show k
-- Other instances as needed or is sensible (read, eq, ord, etc)
toText :: (Show a, IsString b) => a -> b  -- or substituting `Text` for type var `b`
toText = fromString . show

After this ground work we can have more structure to our calls, which should also make the library easier to learn and use. For example:

declareQueue :: Channel -> QueueOpts -> IO (Queue, Int, Int)
bindQueue :: Channel -> Queue -> Exchange -> RoutingKey -> IO ()
publishMsg :: Channel -> Exchange -> RoutingKey -> Message -> IO ()

And thanks to OverloadedStrings most if not all of the examples will work fine.

How is Queue Text a practical improvement over Text, say, for declareQueue? There is very little you can validate in queue name (that the prefix is not reserved and the fact that it is UTF-8, as of RabbitMQ 3.3).

If type safety does not lead to actual safety, it's just busy work.

How is Queue Text a practical improvement over Text, say, for declareQueue?

Consider the type-correct invalid code of:

(queue, _, _) <- declareQueue chan opts
-- ... ugly logic here can potentially confuses the queue and routingKey variables...
bindQueue chan routingKey exchange queue
--  ^^^ or the programmer can simply mis-order the arugments

If we had signatures of:

bindQueue :: Channel -> Queue -> Exchange -> RoutingKey -> IO ()
declareQueue :: Channel -> QueueOpts -> IO (Queue, Int, Int)

Then such bugs would be caught at compile time. Thanks to OverloadedStrings this change should also be invisible to users who want to just write literal strings for Queue and/or RoutingKey types.

I'm slightly against this proposal. While it would marginally increase type-safety, it would come at the cost of breaking backwards-compatibility and being a bit more annoying to use (for example if you wanted to append some Text to your RoutingKey).

I'm open to change my mind if more people demand this.

Please don't mistake me - I'm not so attached to this change that I'd push very hard when the maintainer is not in favor. It just struck me as a good change when learning and using the library.

That said, the append example strikes me as a minor nuisance. If you have a literal then the difference is one of imports and concat functions, T.concat a "b" and mconcat a "b". If you don't have a literal I agree things become syntactically less clean, T.concat a b vs mconcat a (Queue b).