[Haskell-beginners] Re: Applicability of FP unchanging things to
real world changing things
Stephen Blackheath [to Haskell-Beginners]
mutilating.cauliflowers.stephen at blacksapphire.com
Thu Dec 24 07:58:54 EST 2009
Glurk,
You wrote:
> I think a lot of people, looking at the real world, would answer, yes, it
> clearly IS the same book, now in a different state. And yes, I CAN
step into
> the same river twice. This is how people see and think about the real
world.
I think this is a really interesting question, and I may be too new at
functional programming myself to do it justice. I've been doing a lot
of Haskell in a short time, so my experience is fairly intuitive and
this email is part of the process of making it more conscious.
Here's one way to structure the library:
lend :: BookID -> Library -> Library
actOn :: Request -> Library -> (Library, [Result])
actOn (Lend bookID) lib = (lend bookID lib, [])
...
-- | A library is a transformation from Request events (e.g. 'lend') to
Result events (e.g. overdue book email),
-- implemented using lazy lists.
library :: Library -> [Request] -> [Result]
library lib0 reqs = concat $ snd $ mapAccumL (\lib req -> actOn req lib)
lib0 reqs
Now, are we mutating a library, or are we not? In spite of what the
more Zen-oriented Haskellers might say, it seems to me we are. Call me
uneducated but I can't really see what is actually wrong with the idea
of mutation of state over time, where the abstraction is appropriate.
Perhaps the point is that in Haskell, this is one of many ways of
looking at it. Perhaps the essential difference here is that the
library goes round in an "eddy" with an inflow of events influencing it
as it goes round (and producing an outflow of Result events). This
difference accounts for more than it seems to. (Trying to put my finger
on it...) it's a much more restricted activity than mutating state
outright, that means that if you change your code locally (e.g. re-write
the I/O parts), your chance of breaking things is minimal to zero.
> I guess that brings up another point - that eventually we are drawn into
> monads, which to me seems a bit unfortunate, because it seems that it
leads us
> back to imperative programming with mutable state...
In many situations where I could use a state monad I don't (though I
went through a stage of using them). My advice would be just write the
code straight, and if the state management gets complex, a state monad
may be warranted, or some better way may present itself. The great
beauty of Haskell is that you have a lot of choices when a situation
like this arises, and going for a state monad before you've examined the
alternatives potentially restrict them.
In the library example, if Library is a complicated data structure, you
might want to have some lift functions that allow functions that affect
only parts of the library to be lifted into a 'Library -> Library' type,
e.g.
liftDueDates :: (DueDates -> DueDates) -> Library -> Library
liftDueDates f lib = lib { libDueDates = f (libDueDates lib) }
Or to take the concept further ... There may be situations where you
want to do some process where there are a number of parts of the library
you want to work with, but conceptually some other way of viewing it
makes sense. The "Haskell Way" would be to take the right parts of
Library and create a new data structure that reflects the conception
better. (In FP you often abstract things using data where you would use
code in imperative languages.) If some output from that process has to
contribute to a modified Library, then implement that as yet another
transformation. Putting things back together like this is the extra
work that Haskell makes you do - but the benefits outweigh the costs.
So I'd say this: If you write your code in really plain Haskell, you'll
tend towards a functional approach (e.g. why pass Library when it's
better to transform library in some way first?). In that process, a
state monad might suggest itself as a way of making the code more
readable, but the thinking is still "plain Haskell". Then you're using
a state monad with "right mindfulness" so to speak.
Way of looking at it #2: If you are using a state monad because it's
easier for you to understand the code that way, that's probably the
wrong reason to use one. If you're using it because your
straightforward Haskell is looking a bit unreadable, and the state monad
tidies it up, then that's probably the right reason to use one.
Way of looking at it #3: Your data needs to go through transformations
from one form to another. In functional programming, you turn that
process into a flow of data (or a sequence of transformations) from one
form into another. If the flow loops, it starts to look a bit like
mutating state, but in a way that's really only a surface appearance.
One difference between state monads and mutating state in place (as done
in OO languages) is that (in my experience) even with a state monad, the
state mutation never really gets a chance to dominate the program.
Haskellers have a saying "don't fear the monad - only IO is impure"
that's applicable here.
However, it's true that monads can lead to bad code, so they need to be
used only where they're actually appropriate. I had an example of an
excellent use for a monad recently: A parser where looking up an XML
tag by its id string is abstracted as a continuation (so it can use IO
if it wants) and it needs the ability to bail out if there's a parse
error. A monad allowed me to write this code purely and cleanly.
Another difference (state monad vs. mutating in place) is that a state
monad can never lead to race conditions.
Steve
More information about the Beginners
mailing list