4 ms·
I was toying with Haskell a few years ago, and I found it unpractical. Sometimes it turns out you need to do something with IO somewhere, and now you have to c
by duckingtest 12y ago
I was toying with Haskell a few years ago, and I found it unpractical.
Sometimes it turns out you need to do something with IO somewhere, and now you have to change all the functions down the way to use it.
Similar issue is lack of exceptions - Haskell just has a type enhanced version of 'return -1' from C. Exceptions should be completely separate from successful execution paths. Throwing exception from a pure function with type like Int -> Int requires changes in everything that uses it.
Common Lisp's condition system is much, much better.
The third issue I remember was that all fields' names are global.
All of this makes it very hard to write the minimal working code now and extend it later. Instead, you have to provide scaffolding for possible future changes, or risk having to change everything.
Maybe there's some True Way of using Haskell which doesn't suffer from these issues, but I haven't found it.
- mark_l_watson 12y agoI have been on a four year journey for learning Haskell so I understand where you are coming from. I have recently starting doing almost all of my NLP and semantic web/linked data experiments in Haskell and that has helped get me over a learning curve hump, a bit. One thing that helps me, when possible, is to get the pure code working before worrying about network and file IO, etc. Working on pure functional code is really nice. Monads still cause me some grief and I look for examples on the web of what I need to do, and modify them. I have given up the idea of ever becoming an expert in Haskell, but I am enjoying it and getting stuff done.
- michaelochurch 12y agoSometimes it turns out you need to do something with IO somewhere, and now you have to change all the functions down the way to use it. Similar issue is lack of exceptions - Haskell just has a type enhanced version of 'return -1' from C. Exceptions should be completely separate from successful execution paths. Throwing exception from a pure function with type like Int -> Int requires changes in everything that uses it. That's the price of static typing. There are a lot of tricks to make that more bearable (monads and do-notation) but, you're right, refactoring requires some work until you make all the type signatures work. Exceptions are a pain in Haskell. It's hard to get them to work in an intuitive way with laziness, and it's unclear how they'd interact with a type system. In an ideal world, Int -> Int would mean a function that can't throw an exception. Of course, there are very few people who want to live in a world where division has a type signature of Int -> Int -> Maybe Int, and it's not clear what else to do (but throw an exception, or hard-crash) in the case of unhandled division-by-zero. Haskell has an exception system but it's hard to use because of laziness and some other factors. The more idiomatic error handling is to use the Either type: Either l r = Left l | Right r where Left corresponds to the erroneous case and Right to the successful path. Either is a monad in the latter argument so you can use do notation to make it "look imperative". The third issue I remember was that all fields' names are global. They're specific to the module you're writing in. If you import a whole module (which you generally shouldn't, except in ghci) then you'll often have collisions. All of this makes it very hard to write the minimal working code now and extend it later. I wouldn't necessarily use Haskell for a small project. Python or Clojure are great tools, and dynamic typing (IMO) is perfectly fine until you get into thousands of LoC. Of course, if you like static typing (which I do) and want to learn more about it, you can certainly use Haskell for small projects and it will work just fine... but it's more efficient (if you're not familiar with Haskell) to use Python or Clojure. Maybe there's some True Way of using Haskell which doesn't suffer from these issues, but I haven't found it. It took me a while to "get" the elegance of Haskell. It forces you to program in a different way: small functions, minimal interleaving of stateful computations (IO) and stateless ones. It can be very concise and beautiful, but it's very different from other languages, even other high-power ones like Clojure.
- hesselink 12y agoEach of your points contain mistakes: Exceptions in Haskell are separate from successful execution paths. It has `throw :: Exception e => e -> a`, which can be used in pure code. Only catching, i.e. handling exceptions, has to be in IO. The IO issue you mention is a feature to me: it means your code will have a very clean structure with IO at the top level, and pure domain code further down. But perhaps others prefer other structures, I don't know. Field names are not global. They are scoped the same way as everything else. They are in scope in the module, and you can manage visibility in other modules with exports and imports.
- jeremyjh 12y agoUnfortunately I found it really takes a lot of work on practical programs to get past the stage where you have these sort of doubts. Finding the motivation to do so can be a chicken-in-egg type of problem. Technically you are incorrect about exceptions needing to change the type of pure functions, but in fact I would always prefer to return an Either and so it does result in code changes up the stack. However it may not mean very many changes to signatures; a function used by only one parent shouldn't be top-level and doesn't need a signature. And the result is a function that captures failure modes in its type, which means other consumers can't forget to handle failure (though they can explicitly choose not to if they want to just throw an exception on failure). This may feel a bit verbose and unwieldy at first but it actually is very powerful once you know the techniques. The concern about IO - you don't say this but a lot of people worry about logging output to STDIO for debugging. But Debug.Trace will do this impurely for debugging purposes. Otherwise its actually pretty rare that you write a pure function that later turns out to need IO. IO should usually stay in top-level functions that provide the over-all program / routine structure. I'm only at a journeyman level with Haskell but I've written over 20,000 lines of real code and can't remember ever really running into this problem, though I did worry about it when just getting started.
- tel 12y agoExceptions in Haskell simply are not the same as in other languages. In particular, they probably should only ever occur in IO code. Why? Because right now you spend a lot of time trying to reason about non-local jumps in code due to exceptions. Purr Haskell code frees you from that concern by making everything local and explicit. You have to learn a new style, but once you do going back to pervasive exceptions is a pain! It's really hard to trust code that might bubble up an exception at any point. Finally, I've found that the times I genuinely want bubbling exceptions are exclusively IO scenarios anyway. Touching the real world is when you're likely to need to handle partial coverage of behavior amyay. So it all matches up and is wonderful, but is also quite different from what you might normally expect. In particular, you need to learn how to think about and reason within pure code—once you learn that you'll find that predicting the right type signatures is easy. They just explain exactly what your function really does. --- Here's a great /r/Haskell thread on the topic of modern error handling style: http://www.reddit.com/r/haskell/comments/2csaml/are_there_any_new_developments_in_the_last_seven/ http://www.reddit.com/r/haskell/comments/2csaml/are_there_an...
- dllthomas 12y agoThe "True Way" of using Haskell involves getting the type system to work for you. It is true that as your project scope changes in surprising ways, occasionally you'll discover you need some facility somewhere that the types don't currently make it available. Yes, you've got to go through and re-plumb things - but your compiler will quickly point you at everything that needs to change to that end. As michaelochurch says, keep everything small and composable - and I would add "abstract", with some caveats - and much of it will shift automatically where it's safe to do so. Edited to add: Regarding the "update the types so that I can do something I couldn't" issue - it's not really any different than writing any language and discovering that you need some information you don't have handy. Threading it through the various function calls is not hard and usually the right thing to do. "I can do anything anywhere" is the equivalent of making it a global.
- nightski 12y agoYea it's simple. Do not nest function calls when possible. Use the rich and widely available composition facilities of the language to compose functions instead of nest them. This will generally amount to a much cleaner design in the end anyways - in any language. But Haskell is one of the few languages that is abstract enough to give the ability to write these combinators.
- Ixiaus 12y agoSome of the responses to you are good and some aren't. Honestly I don't think the point here is to tell you what you don't know (which is a common strategy for Haskellers). I will tell you this, though. In order for your mind to be productive in Haskell you have to unlearn a lot of imperative and executional ways of thinking. Haskell excels in describing what something is (through the type system, also lookup "denotational semantics" - reading up on that helped me understand what the character difference is between Haskell and the more imperative languages I was brought up on). I definitely do think a language like Haskell has immense gains for the enjoyment of programming, business execution, and safety of the programs we write that the general public consumes. [EDIT] My own double negative confused even me...
- dragonwriter 12y ago> Sometimes it turns out you need to do something with IO somewhere, and now you have to change all the functions down the way to use it. You really shouldn't need that. > Exceptions should be completely separate from successful execution paths. Its easier to write code with a language that does that, but harder to analyze what existing code does and debug it. So, no, while I understand the initial attraction of that approach, I don't think you can validly say that the One True Way is the one Haskell doesn't use here. > The third issue I remember was that all fields' names are global. Well, like all functions, they are the top level of the module. But they ought to, outside of simple programs, be isolated in a module for the data type, which can then be imported qualified when used.
- coolsunglasses 12y ago>Sometimes it turns out you need to do something with IO somewhere, and now you have to change all the functions down the way to use it. Que no! Bloodhound, which is expressly about talking to an outside database, has >500 type signatures. Fewer than 40 have IO in them. You most certainly do not need to add IO to pure code just because something in IO invokes it. This is why we have things like Functor, monad transformers, etc. Want to know more? https://github.com/bitemyapp/learnhaskell https://github.com/bitemyapp/learnhaskell