6 ms·
I recently learned Haskell and am writing code in it on a daily basis, although I'm still a novice. In my limited experience, Haskell is like any other PL, you
by Maro 14y ago
I recently learned Haskell and am writing code in it on a daily basis, although I'm still a novice.
In my limited experience, Haskell is like any other PL, you can write clean, readable code or a horrible mess.
My major concern with Haskell right now is with maintainability/changability of code. One recent example that bit me in the ass was: I wrote an elegant solution to check whether a list of files exists, using sth like foldr (&&) True doesFileExist. Then I realized I really need to print out which file is actually missing, but only the first N to avoid flooding the user with error messages. At this point FP becomes a pain IMO, and it'd be much easier to add a counter variable and printfs in the imperative program and move on. Actually this is fairly common, think about adding printfs for debugging: not so easy in Haskell, as IO changes the type signatures of the function and its callers. So, in my limited experience, changing code in Haskell is expensive and its effect are not localized.
- emgreen 14y agoFrom my experiences with Haskell, it takes a long while to get used to Haskell, but once you do it really pays off, especially in the area of maintainability. It's actually one of the reasons I love Haskell so much. I've been where you are though, and I feel your pain - if you keep at it, it'll get good, promise. Some things I find help with maintainance : * the type system allows the compiler do a lot of work for you, * types have to be concrete rather than abstract, * if a little effort is applied the unit tests can be amazing, * everything's clearly defined, * codebases tend to be a bit smaller, and once you're used to it, more readable. In the particular situation you describe, maybe using the "take" function could work? Yes, the IO stuff can be tricky, I agree, but with practise it's not too bad, and separating IO from pure code gives all sorts of goodness back. There's always Debug.Trace if you need to do a quick bit of debugging by print statements. I feel the article was being unfair on Haskell there, after all the code example was being deliberately unreadable, and that's possible in any language. It's not really that hard: map (2^) [1..]
- michaelochurch 14y agoSo, in my limited experience, changing code in Haskell is expensive and its effect are not localized. That's a feature, because it protects against spaghettification. Explained here: http://michaelochurch.wordpress.com/2012/12/06/functional-programs-rarely-rot/ http://michaelochurch.wordpress.com/2012/12/06/functional-pr... When a manager walks in with a new requirement, you'd rather be able to do it in 15 minutes than 2 hours, because managerial injections are annoying and you'd rather get back to more interesting work. Sure. I get that. However, once enough hands have passed over code, with hackish fixes thrown in to satisfy dipshit flavor-of-the-month requirements, then if it's easy to inject complexity without consideration of the loss of conceptual integrity, people will do so. What you get over time is spaghetti. It doesn't happen overnight. Almost no one sits down and says, "I'm going to write a bunch of shitty spaghetti code." Spaghetti code is rarely written by one person. It's an "all of us is dumber than any of us" phenomenon. Even Google has some spaghetti code, and I've never met an incompetent engineer at Google. (Project and people management would be a different discussion.) The nice thing about FP is that changing behavior (except for performance) actually requires altering an interface. In a statically typed language, this requires that you make changes elsewhere in the code to accomodate the new information flow. Yes, it's time-consuming, but it also forces you to think about how your change is going to affect the whole system. That's a good thing. Writing code shouldn't be easy, because reading average-case code is impossible. If it takes 50% longer to write code, but the result is that people can still comprehend it at scale, I call that a major win.
- Maro 14y ago> to think about how your change is going to affect the whole system. That's a good thing. I don't think you've made a coherent argument why that's a good thing. Putting in a printf for debugging is not spaghettification and does/should not affect the whole system. > Writing code shouldn't be easy Eh?
- michaelochurch 14y agoYou can printline for debugging using the trace function in Haskell. Or use unsafePerformIO, which is usually outlawed for production code but just fine in a transient state for debugging. On "writing code shouldn't be easy", you're right that this statement, out of context, seems ridiculous. What I mean is that, when there are trade-offs between making it easy to write vs. read code, generally the reader's interests should be favored, because legible code is so rare. Obviously it's better for it to be easy to write code and to read it, but ease-of-writing is a secondary priority for large, production systems.
- Maro 14y agoI pretty much agree with everything you say here. Thanks for the trace and unsafePerformIO tips. > ease-of-writing is a secondary priority In my experience, once you have enough experience and witnessed the suck of maintaining crappy code, writing good code is second nature and doesn't take conscious effort.
- michaelochurch 14y agoThere's a lot of truth in what you're saying. However, if maintaining shit code turned people into good coders, then there'd be a lot less bad code (because the precursor is so damn common). I think it also requires curiosity, desire to do things right, and access to the right information (which is now trivial if one knows where to look). What I would actually say is: the differential in difficulty between writing good code and writing bad code should be minimal or, if possible, negative.
- danidiaz 14y agoFor printf debugging you can use the "trace" function from Debug.Trace. A "first n files that don't exist" function could be something like: nexist :: Int -> [FilePath] -> IO [FilePath] nexist n l = take n . map fst . filter (not.snd) . zip l <$> mapM doesFileExist l
- Maro 14y agoI hope you don't take offense at what I'm about to say, but the code snippet you provided is not maintainable in the sense of not being readable. It sacrifices maintainability for cleverness. As someone above pointed out, sometimes it's better to write 5 lines instead of 2 lines. We don't write code primarily for the compiler, if we would then we wouldn't be having this discussion because the compiler doesn't mind spaghetti. We write code for the next guy who will try to read, understand and modify the code. As an experienced programmer I can tell you, but you probably know anyway, that in all languages you can write very concise but completely incomprehensible snippets which even you won't understand in 2 weeks.
- mifrai 14y agoHonestly, I don't think danidiaz was trying to be clever so much as not knowing about the existence of filterM. A good chunk of the code was just an implementation of it: filterM' f xs = map fst . filter snd . zip xs <$> mapM f xs So, given filterM, the code would have looked like nexist n xs = take n <$> filterM (fmap not . doesFileExist) xs Which doesn't look clever at all and it's fairly easy to understand.
- gnuvince 14y agoRegarding your example, I think that one part of the problem (which you mentioned in the first paragraph of your comment) is that you are still a novice. I cannot claim to be an expert in FP or Haskell, but here's how I would go about your problem. First, I think your problem really has two tasks to perform: getting a list of files that don't exist, and selecting only a part of those inexistant files. It would make sense to decompose these two tasks into two different functions, and since Haskell has non-strict evaluation, we don't have to worry about generating too much data (list of inexistant files) for nothing. Starting with the function to select inexistant files: inexistantFiles :: [FilePath] -> IO [FilePath] inexistantFiles files = filterM (\f -> doesFileExist f >>= return . not) files Some Haskellers may prefer to write their functions completely point-free, but I like to have my variables in the code if it helps me read the coder better. And then we can write another small function to select only the `n` first inexistant files: takeInexistantFiles :: Int -> [FilePath] -> IO [FilePath] takeInexistantFiles n files = takeM n (inexistantFiles files) where takeM n = liftM (take n) (Surely, we'd prefer to just write `takeM` and call it directly on the result of `inexistantFiles`, but I prefered to go with the specialized function.) Someone who knows Haskell better than I do could certainly come up with a cleaner solution, but I think the code is readable, solves the problem you mentioned and isn't any harder to understand than the equivalent imperative code. Have a Merry Christmas!
- Maro 14y agoThanks, great comment.
- mifrai 14y agoand since Haskell has non-strict evaluation, we don't have to worry about generating too much data (list of inexistant files) for nothing. Be careful here. Because you're in the IO monad, the entire list of files will always be computed first (ie. doesFileExist will be called for every file given). Non-strict doesn't really buy you much here. The "expensive" operation here will always be executed. At most, you're avoiding converting some list-consing-thunks to a list.
- deleted 14y ago