That said, the actual problem described in the article has this issue at the very core - we have more software we need written than we have people who can write good Haskell. This means that a large percentage of software is going to be written poorly - and the only real solution would be to simply have less software.
Supply-Demand overrides this completely, and so bad software will remain. The true challenge is therefore making software that can be written by the semi clueless and yet still be decipherable enough to maintain. Java and C# seem to have cornered this market to great effect in the business landscape from my experience.
I think current language development trends towards functional programming is a mistake as it caters to such a tiny audience, and more emphasis should be placed on new languages that can force people to write good code.
Is this possible? Dumb example: If someone is hacking together computations of circles without knowing about PI, is there any possible way for a language to help here?
I think a language that can make it clear what the code is trying to do, even when written by someone who has no clue, would go a long way to being able to filter out and fix bad code quicker. If your code is in Haskell, it's going to take a lot of effort to work out what bad code is even trying to accomplish.
I interviewed for jobs a few months back. When people asked me "solve this puzzle in any language you like", I usually used haskell. The solutions were just simpler and cleaner in Haskell than in Python/Java/etc. Apart from minor conventions they were unfamiliar with (e.g., "What's Maybe"), not a single interviewer had a problem understanding what I did, even though most had never used Haskell.
One company even had me demo some code I wrote (great interview technique, BTW), and I picked a Haskell project [1]. They literally stopped my detailed verbal explanations and told me: "no need to explain getFileHandleOrNothing, this all looks pretty clear and obvious. The only thing we needed an explanation of was your monad, the rest is clear."
You advocate that more emphasis should be placed on new languages that force people to write good code. That's what Haskell and other FP languages do.
Edit: actually when interviewing people I show them some Haskell code and ask what it does. Most of them do, some answer that they don't know the programming language. Of course I don't give them very difficult stuff, but still; a lot of well written Haskell code is incredible readable, even for people who never saw it. Also, I really assume if you have a CS degree that you played around with functional languages and recursion. My disappointment with a certain country was that almost none (and I interviewed 100s in the past 10 years) of the Masters of CS from that country even have a clue what recursion is, let alone functional or logic programming languages.
I'd probably fail it even though I do understand recursion. I don't know Haskell, and the above is rather cryptic to me. I can infer that it raises 2 to a power, but I'm not sure how it works -- both in how it's called (does that define a function? Pow(4) --> 16?) and the why (the last part defines the recursion I assume, but what happens when you need multiple args, and what does it mean?)
Edit: But to answer your question: yes, I would like to be asked. I did a lot of interviews from the employee side just because I was curious, and, at least in the Netherlands, they never gave me a test. Just looked at my (possibly fake if I wanted) credentials and said yes/no. No tests, at all.
I don't get to write haskell at work, but I see its principles everywhere with an additional layer of cruft over the top. Examples include doing functional style programming in python, linq in C#, passing around functions and callbacks in JavaScript and trying to reason about side effects.
http://uncyclopedia.wikia.com/wiki/Haskell#Haskell_Code_Exam...
It's like using code from the IOCCC to make a point that C code is not maintainable.
pow = 1 : [ 2*x | x <- pow] pow = iterate (* 2) 1In 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.
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!
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.
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..]
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
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.
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.By the way, I like the (fmap not . doesFileExist) bit. It´s cleaner and less cluttered than (\f -> doesFileExist f >>= return . not)
That's a feature, because it protects against spaghettification. Explained here: 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.
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?
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.
> 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.
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.
Now, X years later I still tell stories about that first job, and I always remember how I felt maintaining spaghetti code when I write code.
BUT, I find that most of my fellow programmers either have not worked as m.programmers; or have done so but didn't notice it, because I guess they don't have a built-in desire for arete or just didn't want to get upset about their work and chose to ignore the crappy code.
Good abstractions deliver a lot of value. Files and sockets, for example, should be treated as objects, because the client should (usually) be agnostic of how the things are actually implemented. So OOP isn't all bad.
What's messy is a paradigm that gives an unbounded right to inject complexity. Programmers don't have enough autonomy in most corporations to do things right, so the result in most cases of Brownian-motion requirements is an increasingly gnarly pile of hacks. What Haskell does well (being both functional and statically-typed) is punish that style of development while it occurs, making it so that the kludge isn't always the path of least resistance.
I otherwise agree with your b) and c) points.