To carry the philosophy of functional programming to its logical extreme is to deny the existence of time. Some physicists have seriously suggested models that don't include time, but in the milieu of common human existence things happen and stuff has state.
State and time are always going to rear their ugly heads for functional programmers (often in the form of "the i/o problem"). There's just no way around it. This isn't to discount functional programming as a tool in the toolbox, but "philosophically pure" functional programming is an ideal as platonic and unreachable as the math it is derived from.
Pure functional programming it is not about denying time. It's about separating the computing time from the programming model's time. Or equivalently, to separate the computing order from the order of real-world events.
That's why monads are an extremely powerful concept, and not just a hack to "avoid time". Using monads, you can compose evaluations in a certain order (model time), but that doesn't automatically mean they will be actually evaluated in that order (computing time). Well, if you use IO monads this will actually be the case. But you are free to define other monads which allow you to do strange things like looking into the future or checking multiple alternatives at once.
Pure functional programming forces you to always make an explicit distiction between these two kinds of time, and that feels buerocratic to those who are used to confound them. However, making that distinction usually yields to much better interfaces. So there is an extra effort, but also a big gain.
I'm more or less a novice with FP, yet to learn a language more functional then Python, but this, and the article, are along the lines of something I was thinking of. Why exactly is it that I/O is stateful? A program, after all, is something that takes in input and spits out output, right? Why can't it be:
[Byte] -> [Byte]
main [data:rest] = ...
(sorry if I'm making up syntax or types, haven't properly learned Haskell yet). Well, my friends tell me, the problem is that often times you have to deal with timing, exceptions, etc. Well, fine:
[(Byte, Integer, Error)] -> [Byte]
main [(data, time, err):rest] = ...
As you said, no reason actual evaluation has to correspond to the logical order of evaluation.
In Haskell they like it better if a function returns the same value if it is called with the same arguments later on. Just like sin( pi ) gives the same answer every time you call it. If you start writing functions that take a file name and return the bytes then that function might return a different value the next time it is called assuming the file contents changed on disk. The same line of reasoning applies to db calls, user i/o, anything with random numbers etc.
But I'm not taking a file name, I'm taking a stream of data, for which the output should always be the same. This should be good enough for applications that deal with standard in and standard out, yes?
Though that's a good point, when it comes time to be opening files on the disk, it inevitably becomes stateful again.
I think this roughly corresponds to the original I/O system in Haskell, based on lazy streams. They were, by all accounts, an absolute bitch to program with. Continuation based I/O was slightly better, but monads were still greeted as liberators when they arrived on the scene.
In fact, you can have functions of type [Byte] -> [Byte], however, the problem is when you need to get the first set of bytes from a file or stdin. You could sort of read this as a function of type FileHandle -> [Byte], which will only very rarely even return the same value twice, let alone guaranteed-always.
Beautifully said. I was going to post a comment saying the same thing, but you've put it in much better words than I would have. Thanks.
It's sad some programmer fall into the purity trap and try to make things as functional as possible and end up taking a lot more time than they normally would have, make the code incredibly hard to understand, etc.
I once fell into this trap and paid dearly. I'm a university student and for one of my semester projects I decided to try out pure functional programming. I ended up failing that class because I didn't finish the project, and doing bad in others because I spent ridiculous amounts of time doing the impossible project. (The project itself was easy - a simple 2D sidescroller game in C++, something I could have done in a week if I had been sensible; but writing a game in functional style in C++ proved to be incredibly hard.)
You shouldn't blame a particular programming style for the failure. Your real failure was an organizational one.
It seems that you have more experience with the imperative style than the functional style. And yes, it's always good to try a new style you aren't yet comfortable with, but you shouldn't do that in an important project. Also, you might have had better luck with Haskell or Lisp rather than C++. But here again, you shouldn't do that in a time contrained project if you aren't fit in that language yet.
Instead, you could have taken the week to implement it in the style you're comfortable with, and rewriting or refactoring it into another style to play around with it. That way, you would not only have eliminated the risk of failing. You would also have learned more, being able to compare both styles side-by-side.
You may have failed classes, but the lessons learned in working on something so improbable were probably worth the price of admission.
It's good to be too pure for learning purposes, because it makes you comfortable with a lot of nitpicky stuff. When you move towards commercial software, you won't get an environment that encourages you to take on the risky stuff. "Lean and mean" is the order of the day. So having the other experience gives you an edge in perspective.
Well... you actually can try out functional approaches in C++, and even gain something out of it. It's just that doing so requires a sophisticated understanding of C++.
Obviously you can't have truly pure functional code - I don't think anyone advocates that.
But non-functional code is much harder to debug (spooky action at a distance, etc) so a little bit more work upfront to isolate the non-functional code from the functional will make the debugging/maintenance phase (which are considerably longer than the 'initial writing' phase), easier.
It might make sense to say, "we can't build an effect-free computing machine", because we use transistors, and transistors get hot when they sink power, and things getting hot changes the entropy of the Universe... but it doesn't make sense to say, "we can't model an effect-free world, and program is if the world is effect-free".
Would you, then, agree with the statement: "There's no reason to write the sort of puritanically functional code that the article's author suggests we aspire to, because it would merely sit around and do nothing"?
Because apart from nothing, I'm not entirely sure what the author really thinks 100% pure functional code should do, when even input and output states are too impure to be allowed inside his Sanctified Garden of Jesus-Code, despite the fact that it's that mapping that we're explicitly interested in whenever we run a program. As far as I can tell, it's the "run a program" step that the author has a real problem with, and while he's welcome to subvert that paradigm, I don't see any hint of a way forward from it...if your program is meant to be fully composable at a level more granular than "program", shouldn't you be writing a library instead? Is that the suggestion?
From what I can tell, the author hasn't figured this out, either, he just thinks someone should.
These are very interesting slides. Slides 17 and 40 (et seq) seem to restate what I tried to explain as "computation time" vs. "model time" in http://news.ycombinator.com/item?id=1254329
Can anyone explain to me how the io monad gives referential
transparency?
So, say I have a function to read a file:
readFile :: String -> IO String
readFile filename = ...
At time t1, if I invoke it it returns say:
IO "Contents at t1"
At time t2, if I invoke it it returns say:
IO "Contents at t2"
Given that we supplied the function with the same
argument (i.e. fname), and it returns two different values,
how can they be said to be equal? I'm probably totally misunderstanding this but anyway....