Michael Snoyman: Generalizing Type Signatures

综合编程 2017-04-20

In each of the following, which is the best version of the type signature (assume Monad m

when :: Bool -> m () -> m ()
when :: Bool -> m a  -> m ()

mapM_ :: (a -> m ()) -> [a] -> m ()
mapM_ :: (a -> m b)  -> [a] -> m ()

runConduit :: ConduitM () Void m r -> m r
runConduit :: ConduitM i  o    m r -> m r

In each of these cases, the second, more generalized version of the signature can easily be provided, but the question is, should it be? I know of the following arguments:

Pro generalized:

  • Why would we artificially limit the generality of our functions?
  • Having to sprinkle a bunch of void
    s around is a code smell

Against generalized:

  • It can allow accidental bugs to slip in by unintentionally ignoring values
  • In some cases, the generalized version hasworse performance

I don't think these points bring anything new to the table: it seems to me that these trade-offs are fairly well understood, even if not talked about explicitly often. The other thing I'd observe is that, ignoring the issue of performance, this is just a rehashing of the much more general argument of explicit vs implicit. We can all acknowledge that with liberal application of void
and similar functions, it's always possible to rewrite code relying on the generalized version to use the specialized version. The question comes down to that annoying need for throwing in void

How do we determine whether we should use generalized or specialized functions? I'm starting to follow this procedure:

  1. If there's a performance concern, let that take precedence. Having accidentally slower code due to a desire to make code shorter/more beautiful is a bad trade-off.
  2. If there's no performance issue (like with runConduit
    ), it's a completely subjective decision. The facts I'd look for are examples of bugs people run into with the generalized version.

On the bug front, I think it's important to point out that, in my experience, the bugs are less likely to appear during initial code writing, but during code review and refactoring. When you write something like when foo getLine
, you've probably gone through the thought process "I'm just trying to give the user a chance to hit , and I'm intentionally ignoring whatever the user entered." But during refactoring that may not be so obvious, and some ignored user input may be surpring. By contrast, when foo (void getLine)
stands out more blatantly.

Finally, in comparing this to the general discussion of explicit vs implicit, I want to point out that there's no "one right answer" here. This falls into the same category of "do I define a typeclass/interface?", which is always a judgement call. You can give general guidelines, but I think we can all agree that both extremes:

  • Never define a typeclass for any purpose
  • Define every single function to be a member of a typeclass

Are ridiculous oversimplifications that we should not advocate. Same thing applies here: there are times when a generalized type signature makes sense, and times when it doesn't.


As an aside, if anyone is wondering where this random blog post came from, while working on a presentation on Conduit and Yesod, I revisited an issue from a few months ago
about deprecating older type synonyms ( PR now available
), and was reminded of the ongoing debate around which of the following is the correct runConduit

1: ConduitM ()   Void m r -> m r -- today
2: ConduitM i    o    m r -> m r -- most general
3: ConduitM Void Void m r -> m r -- standardize on Void
4: ConduitM ()   ()   m r -> m r -- standardize on unit

The current situation of ()
for input and Void
for output has been around for a while, and originated with discussions around the conduit/pipes dichotomy. (And in fact, pipes today has the same split
.) I'm not convinced that the split between input and output is a great thing, so the arguments in favor of each of these signatures seem to be:

  1. Backwards compatibility
  2. Generalize, avoiding any need for explicit conversion ever, and avoid the ()
    / Void
    debate entirely * Note that we won't really
    avoid the debate entirely, since other parts of conduit will still need to indicate "nothing upstream" or "nothing downstream"
  3. Most explicit about what we're doing: we guarantee that there will never be any real values yielded from upstream, or yielded downstream. You can look through the conduit codebase for usages of absurd
    to see this play out.
  4. More explicit, but less cognitive overhead of learning about Void

I think for now I'm leaning towards (1), as backwards compat has been a big guiding principle for me, but the debate is still raging for me.

责编内容by:Michael Snoyman's blog (源链)。感谢您的支持!


在 2018 年初,让我们再谈谈大前端的趋势... 上一次写前端趋势这一类的东西,是在去年的这个时候。一年多过去了,又发生了怎样的变化呢? One JavaScript:移动应用 ...
浅析 Secondary NameNode(辅助namenode) 浅析 Secondary NameNode(辅助namenode) 在初学Hadoop时,有个让人疑惑的概念:Secondary NameNode,也叫辅助...
HEVC代码学习23:xTZ8PointDiamondSearch函数 今天来学习xTZ8PointDiamondSearch函数。 xTZ8PointDiamondSearch是xTZSearch调用的一个重要函数,实现的是菱...
Deploying More Reliable Apps with Uber Engineering... Unlike server-side programming, mobile code cannot be retracted once shipped and...
Optimizing BitBlt by generating code on the fly The initial implementation of the Bit­Blt function in 16-bit Windows did...