views:

358

answers:

7

Hi all,

I'm looking to learn functional programming with either Haskell or F#.

Are there any programming habits (good or bad) that could form as a result Haskell's lazy evaluation? I like the idea of Haskell's functional programming purity for the purposes of understanding functional programming. I'm just a bit worried about two things:

  1. I may misinterpret lazy-evaluation-based features as being part of the "functional paradigm".
  2. I may develop thought patterns that work in a lazy world but not in a normal order/eager evaluation world.

Thanks,

Mike

+8  A: 

You'll certainly learn about evaluation strategies. Non-strict evaluation strategies can be very powerful for particular kinds of programming problems, and once you're exposed to them, you may be frustrated that you can't use them in some language setting.

I may develop thought patterns that work in a lazy world but not in a normal order/eager evaluation world.

Right. You'll be a more rounded programmer. Abstractions that provide "delaying" mechanisms are fairly common now, so you'd be a worse programmer not to know them.

Don Stewart
A: 

Well, try to think of something that would work if lazily evaluated, that wouldn't if eagerly evaluated. The most common category of these would be lazy logical operator evaluation used to hide a "side effect". I'll use C#-ish language to explain, but functional languages would have similar analogs.

Take the simple C# lambda:

(a,b) => a==0 || ++b < 20

In a lazy-evaluated language, if a==0, the expression ++b < 20 is not evaluated (because the entire expression evaluates to true either way), which means that b is not incremented. In both imperative and functional languages, this behavior (and similar behavior of the AND operator) can be used to "hide" logic containing side effects that should not be executed:

(a,b) => a==0 && save(b)

"a" in this case may be the number of validation errors. If there were validation errors, the first half fails and the second half is not evaluated. If there were no validation errors, the second half is evaluated (which would include the side effect of trying to save b) and the result (apparently true or false) is returned to be evaluated. If either side evaluates to false, the lambda returns false indicating that b was not successfully saved. If this were evaluated "eagerly", we would try to save regardless of the value of "a", which would probably be bad if a nonzero "a" indicated that we shouldn't.

Side effects in functional languages are generally considered a no-no. However, there are few non-trivial programs that do not require at least one side effect; there's generally no other way to make a functional algorithm integrate with non-functional code, or with peripherals like a data store, display, network channel, etc.

KeithS
Juliet
@Juliet: Short-circuiting is often used as an example of a special case obviated by laziness, but I find it unsatisfying for pretty much that reason. A slightly better example in C# might be the two branches in a conditional operator, where only evaluating one is more likely to be important. Still kinda flimsy though, with everything else being so eager.
camccann
Juliet
camccann
@Juliet: The concepts are not orthogonal, i.e. independent. In a lazy language, built-in special short-circuting || is not needed.
sdcvvc
+6  A: 
  1. I may misinterpret lazy-evaluation-based features as being part of the "functional paradigm".

Lazy evaluation is an important part of the functional paradigm. It's not a requirement - you can program functionally with eager evaluation - but it's a tool that naturally fits functional programming.

You see people explicitly implement/invoke it (notably in the form of lazy sequences) in languages that don't make it the default; and while mixing it with imperative code requires caution, pure functional code allows safe use of laziness. And since laziness makes many constructs cleaner and more natural, it's a great fit!

(Disclaimer: no Haskell or F# experience)

Beni Cherniavsky-Paskin
Examples of explicit implementations: LINQ, the centerpiece of C# 3.0, makes heavy use of lazy sequences and took direct inspiration from Haskell. Lazy sequence building with a coroutine-style syntax (commonly some variant on a `yield` keyword) appears in multiple languages. OO languages with "properties" allow the technique of delaying expensive computations until the getter is called, then caching the value. In Haskell, you get all of that automatically as part of the language itself.
camccann
+1  A: 

To expand on Beni's answer: if we ignore operational aspects in terms of efficiency (and stick with a purely functional world for the moment), every terminating expression under eager evaluation is also terminating under non-strict evaluation, and the values of both (their denotations) coincide.

This is to say that lazy evaluation is strictly more expressive than eager evaluation. By allowing you to write more correct and useful expressions, it expands your "vocabulary" and ability to think functionally.

Here's one example of why: A language can be lazy-by-default but with optional eagerness, or eager by default with optional laziness, but in fact its been shown (c.f. Okasaki for example) that there are certain purely functional data structures which can only achieve certain orders of performance if implemented in a language that provides laziness either optionally or by default.

Now when you do want to worry about efficiency, then the difference does matter, and sometimes you will want to be strict and sometimes you won't.

But worrying about strictness is a good thing, because very often the cleanest thing to do (and not only in a lazy-by-default language) is to use a thoughtful mix of lazy and eager evaluation, and thinking along these lines will be a good thing no matter which language you wind up using in the future.

Edit: Inspired by Simon's post, one additional point: many problems are most naturally thought about as traversals of infinite structures rather than basically recursive or iterative. (Although such traversals themselves will generally involve some sort of recursive call.) Even for finite structures, very often you only want to explore a small portion of a potentially large tree. Generally speaking, non-strict evaluation allows you to stop mixing up the operational issue of what the processor actually bothers to figure out with the semantic issue of the most natural way to represent the actual structure you're using.

sclv
+14  A: 

There are habits that you get into when programming in a lazy language that don't work in a strict language. Some of these seem so natural to Haskell programmers that they don't think of them as lazy evaluation. A couple of examples off the top of my head:

f x y = if x > y then .. a .. b .. else c
  where
    a = expensive
    b = expensive 
    c = expensive

here we define a bunch of subexpressions in a where clause, with complete disregard for which of them will ever be evaluated. It doesn't matter: the compiler will ensure that no unnecessary work is performed at runtime. Non-strict semantics means that the compiler is able to do this. Whenever I write in a strict language I trip over this a lot.

Another example that springs to mind is "numbering things":

pairs = zip xs [1..]

here we just want to associate each element in a list with its index, and zipping with the infinite list [1..] is the natural way to do it in Haskell. How do you write this without an infinite list? Well, the fold isn't too readable

pairs = foldr (\x xs -> \n -> (x,n) : xs (n+1)) (const []) xs 1

or you could write it with explicit recursion (too verbose, doesn't fuse). There are several other ways to write it, none of which are as simple and clear as the zip.

I'm sure there are many more. Laziness is surprisingly useful, when you get used to it.

Simon Marlow
A: 

I'd expect bad habits.

I saw one of my coworkers try to use (hand-coded) lazy evaluation in our .NET project. Unfortunately the consequence of lazy evaluation hid the bug where it would try remote invocations before the start of main executed, and thus outside the try/catch to handle the "Hey I can't connect to the internet" case.

Basically, the manner of something was hiding the fact that something really expensive was hiding behind a property read and so made it look like a good idea to do inside the type initializer.

Joshua
I think this is a bad example. Non-strict evaluation makes the most sense in a purely functional setting, either by definition with the absence of side effects (e.g. Haskell) or by construction with that absence (i.e. by the discipline of the programmer to not place effectful actions behind thunks). Lazy initialization in languages with mutation is a tricky business and perhaps often wrong, but that's very distinct from anything resembling lazy evaluation in a purely functional setting.
sclv
@sclv: Speaking of bad examples, what about lazy IO in Haskell? Good old `unsafeInterleaveIO`...
camccann
There is a saying "Lisp users know the value of everything and the cost of nothing."
Joshua
Equivalently, you could say that C programmers learn bad habits because they leave dangling pointers. Just because your coworker used laziness incorrectly doesn't mean laziness teaches bad habits, and lazy I/O is often considered problematic in Haskell.
John
@Joshua: that saying, which is a [perlisism](http://www.cs.yale.edu/quotes.html) has nothing to do with lazy evaluation. It refers to the fact that in lisp all expressions are values (which is true of any functional language) in conjunction with the memory overheads associated with lisp at the time.
sclv
A: 

Recently, i found myself doing Haskell-style programming in Python. I took over a monolithic function that extracted/computed/generated values and put them in a file sink, in one step.

I thought this was bad for understanding, reuse and testing. My plan was to separate value generation and value processing. In Haskell i would have generated a (lazy) list of those computed values in a pure function and would have done the post-processing in another (side-effect bearing) function.

Knowing that non-lazy lists in Python can be expensive, if they tend to get big, i thought about the next close Python solution. To me that was to use a generator for the value generation step.

The Python code got much better thanks to my lazy (pun intended) mindset.

Lenny222