Hacker News new | ask | show | jobs
by jeremyjh 4607 days ago
Dabbling really is not enough to show you the problems with your assumptions. You are right that there is a cost, but it is really paid once up-front by each programmer.

I could have written this same comment five months ago, before I started down the long dark tunnel of doom which is to go beyond LYAH and trying to write a real application. I think it was two months before I saw a light at the end of that tunnel - and only recently that I have really begun to feel as productive in Haskell as I previously was in Ruby. At this point, the costs you speak of are paid up and I do not incur them when I write new code. Yes the type system does impose a lot of structure and some boilerplate, but the benefits are profound. Its the sort of statement I did not really fully credit before experiencing myself, but I will say that is profoundly amazing how frequently my code works perfectly once it compiles. This is particularly true when refactoring. I remember sometimes in Ruby despairing to refactor some code because I didn't want to fix all the tests...in Haskell once my tests are compiling they are passing, and the compiler errors are usually very easy to understand and fix (after several months of head-banging frustration).

2 comments

I don't doubt you, but your experience also provides little evidence. Have you done profiling and performance analysis yet? Have you tried working in a large team on a large project?

Different languages have different strengths. Some excel at quick and dirty prototyping or small projects/small teams. Others make a 50+ developer team more manageable. Some have good runtime performance, some have a shorter development time, while others have better runtime monitoring and maintenance tools. No language excels in all of these.

Certainly my experience is insufficient. I have only dabbled in profiling and performance analysis - enough to prove it can be done but that it can be time-consuming and that sometimes you may have to give up some elegance/purity in favor of performance. However the performance I get from naive code is so good (so far) that I have not yet actually had to delve into it.

I am dubious of Haskell's prospects in large teams but I was completely dubious of Haskell in general (like you) before I had experience in it. Perhaps after experience with large teams I would be more bullish on this point but I haven't had that experience so far. I am quite optimistic about the size of the problem space that a small, skilled team (say 5 developers) could solve with Haskell.

> Different languages have different strengths.

On the other hand, some things are better in all important respects than something else. Merely stating that among similar things "different ones have different strengths" is not much of a counterargument.

(This comment is not related to any language or technology in particular.)

I also started to study Haskell recently. In my view, it's great from mathematical perspective, but it has its problems.

Monads sort of force you to make plumbing visible, and it's not so neat as a result. For example, consider a big program that has two modules. The module A calls module B to do something. Now later, you want to add logging to the application. In normal languages, you can just call logging functions (which do IO), from within module B; module A doesn't have to know a thing. In Haskell though, you have to wire the IO monad (or some other monad that does the logging and encompasses it) all the way from A to B. Or say, you want now to access database from module B. Again, you have to wire your DB access up from A, because that's where the entry point is.

In normal languages, plumbing like log access, configuration, DB access can be accessed from any place via global variables or singletons, without imposing a dependency on the main module (or other modules). In Haskell, it's like a military installation - every interaction with outside world has to go through main gate. I am not really sure if there is any benefit to it, but there is certainly a downside that the plumbing is becoming visible in Haskell.

If there would be way to declare something as "plumbing" and have it always available (but still explicitly declared as part of function signature), without having to pass it along everywhere, it would be great compromise, I think. It could even make the programs more type safe, because instead of passing RWS or IO monad everywhere, you could make functions dependent on just DB monad for database access, for instance.

Or you could then configure the plumbing for specific modules, something like dependency injection.

Maybe I am missing something, but I tried to find some articles about how to write large scale programs in Haskell, but no one really seems to explain this.

I don't think the "Xy monad will taint all your code" stands.

I used to think that too, but if you have monadic code M and pure code P, if you need to tie P to M (say at a third callsite C), you just lift the P into the monad at C, and that's it. P stays pure, C of course gets monadic, but that is since it _is_ monadic.

Now logging: I guess people overpanic this. There are two separate sides of logging I think:

1) Effect logging: If you want to log effects (going to send the email, etc), you are already in IO, no worries.

2) App logic logging: This is more like debug-logging to verify that you logic works and flows as expected. If you need this in pure code, throw in a Writer monad for logging the stuff (can discard it if not needed).

2a) Eventually you'll get somewhere where you have IO, so you can dump the aggregated logs if you want.

2b) Or just use unsafePerformIO to send to a logging thread (beat me with a stick).

As a bonus, for app logging you might use a proper ADT for your log statements instead of string, which is great for testing, and even greater for persisting (in json, protobuf, whatever) and later inspection.

I am aware of lifting, but the question is if you have monadic code and pure code in different modules (or you need to go through function which was previously pure), where do you put the lift? If you put it outside the module, you break the modularity. If you put it inside, well then you might as well make the functions monadic in the first place. Basically if you have functions in module API (which may be in itself pure) that may eventually end up calling unpure functions, you have to provision for that somehow, either in the module by making them monadic, or in the caller via lifting. Either way, it's not as clean as it could be.

But I thought about it some more, and to me it seems that actually parametrizing the functions to outside world is not that bad; it's a kind of dependency injection, and seems fine. What is really problematic is returning all the IOs (or other monads) from them; especially since you cannot curry return parameters just like you can entry parameters. So even if that could be replaced by some other mechanism, it would be helpful.

But I didn't know unsafePerformIO, sounds like it can be helpful in some cases.

I agree there isn't a lot written that explains this but I have inferred and used the following pattern. I build a massive monad tower that includes the different things I need like a reader with configuration data, a resourcet process monad, a logging writer etc. However I rarely use that monad in my signatures, I generally use a more restricted type class. For example I have a ConfigReader type class that does exactly and only what you would expect. I have a typeclass for writing to the database, for call distributed process, etc. Now because some of those require MonadIO I do end up with a lot of code that could theoretically do any IO. That could be restricted at the cost of creating more and richer typelcass interfaces myself but I do not think it is necessary for the most part.