Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I think there's a somewhat common failing in Haskell that because a larger vision is possible, smaller solutions get ignored.

Extensible effects (or doing tricky things with a monad stack, or...) promises to let you intercept (approximately) everything your function might do. Awesome. That's probably a thing you'd like in go when you say a function accepts an io.Writer.

It's not what you get in go, when you say your function accepts an io.Writer.

If your code mostly operates in `ReaderT ... IO`, then the simplest typeclass that's equivalent to io.Writer is a typeclass with a function that operates in that same `ReaderT ... IO`.



The problem with the TC approach is substituting implementations for testing ends up being annoying compared to Go. And substituting multiple different interfaces doesn't compose well (this is essentially the mtl problem).

I tend to just record-of-functions everything which is essentially Go interfaces. It does work very well! As functions args usually do :)

But those aren't espoused heavily enough by "authorities" so I find less experienced Haskellers don't lean into them or feel comfortable working with them.


> And substituting multiple different interfaces doesn't compose well (this is essentially the mtl problem).

I think you're talking here about abstracting over the underlying monad, with a different implementation for testing than for production. That can work well[1], but it's not what I was talking about.

With my Writer (nb: io., not Control.Monad.) example, how does Haskell make it harder? I'm not talking about replacing the underlying monad, but to define an interface in terms of that underlying monad.

Fleshing it out, something like:

    main :: IO ()
    main = flip runReaderT () $ do
        -- with Handle
        writeStrings stdout
  
        -- with IOVar
        var <- liftIO $ newMVar ([] :: [ByteString])
    writeStrings var
    liftIO $ print =<< takeMVar var

    writeStrings :: Writer w => w -> ReaderT r IO ()
    writeStrings w = do
        write w $ pack "oh hi\n"
        write w $ pack "uh\n"
        write w $ pack "bye then\n"

    class Writer w where
        write :: w -> ByteString -> ReaderT r IO ()

    instance Writer Handle where
        write w bs = liftIO $ hPut w bs
    
    instance Writer (MVar [ByteString]) where
        write w bs = liftIO $ modifyMVar_ w $ \ bss -> pure (bs:bss)



[1] A digression: Taking that approach, I think it's generally most comfortable to use a unique base type for each test (or natural clusters of them) and implement just the interfaces you need with an eye to the particular tests. The biggest problem I've seen is a tendency to push too much into that environment and thereby make too much implicit, which can make it confusing to distinguish and lead to things like the confused deputy problem.


If you like record-of-functions then you might like my new effect library Bluefin, where effects are passed around at the value level. Here's the documentation about how you use record-of-functions:

https://hackage.haskell.org/package/bluefin-0.0.4.2/docs/Blu...




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: