Or rather, there's a bunch of ways to encode them. But even so, the Go structural typing there is nice.
In industry, I have found this means Haskellers (especially beginners who just do that big Haskell book) don't program against interfaces. Which isn't a scalable way to write a large software project and results in a lot of unnecessary coupling, which in turn means compile times don't stay fast.
The Go GC is also pretty nice as far as latency-focused ones go. But Haskell already got a latency-oriented GC.
What are the differences you care about between programming against an interface in go and programming against a typeclass in Haskell?
From my industrial Haskell experience, programming against a typeclass is natural and common, and the differences that come to mind (no need to declare instances, pervasive ability to fallibly downcast) don't seem to support a claim that Haskellers "don't program against instances" except in the useless sense (that I don't think you intend) that what they program against aren't exactly go interfaces.
- Record of functions
- mtl classes
- extensible effects
- other weird hand-rolled things
I work in a normal boring "ReaderT IO" backend-y codebase and it's like pulling teeth to write neat code against interfaces. Doubly so to get less experienced Haskellers to do it.
Tbh if we used proper extensible effects (any of them), that would qualify and accomplish what I'm talking about. But even seasoned "simple" Haskellers push back on that for being to "fancy."
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`.
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.
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.https://hackage.haskell.org/package/bluefin-0.0.4.2/docs/Blu...