Non-recursive let [Was: GHC bug? Let with guards loops]

Andreas wrote:
The greater evil is that Haskell does not have a non-recursive let. This is source of many non-termination bugs, including this one here. let should be non-recursive by default, and for recursion we could have the good old "let rec".
Hear, hear! In OCaml, I can (and often do) write let (x,s) = foo 1 [] in let (y,s) = bar x s in let (z,s) = baz x y s in ... In Haskell I'll have to uniquely number the s's: let (x,s1) = foo 1 [] in let (y,s2) = bar x s1 in let (z,s3) = baz x y s2 in ... and re-number them if I insert a new statement. BASIC comes to mind. I tried to lobby Simon Peyton-Jones for the non-recursive let a couple of years ago. He said, write a proposal. It's still being written... Perhaps you might want to write it now. In the meanwhile, there is a very ugly workaround: test = runIdentity $ do (x,s) <- return $ foo 1 [] (y,s) <- return $ bar x s (z,s) <- return $ baz x y s return (z,s) After all, bind is non-recursive let.

Hi Oleg, just now I wrote a message to haskell-prime@haskell.org to propose a non-recursive let. Unfortunately, the default let is recursive, so we only have names like let' for it. I also mentioned the ugly workaround (<- return $) that I was shocked to see the first time, but use myself sometimes now. Cheers, Andreas On 10.07.2013 09:34, oleg@okmij.org wrote:
Andreas wrote:
The greater evil is that Haskell does not have a non-recursive let. This is source of many non-termination bugs, including this one here. let should be non-recursive by default, and for recursion we could have the good old "let rec".
Hear, hear! In OCaml, I can (and often do) write
let (x,s) = foo 1 [] in let (y,s) = bar x s in let (z,s) = baz x y s in ...
In Haskell I'll have to uniquely number the s's:
let (x,s1) = foo 1 [] in let (y,s2) = bar x s1 in let (z,s3) = baz x y s2 in ...
and re-number them if I insert a new statement. BASIC comes to mind. I tried to lobby Simon Peyton-Jones for the non-recursive let a couple of years ago. He said, write a proposal. It's still being written... Perhaps you might want to write it now.
In the meanwhile, there is a very ugly workaround:
test = runIdentity $ do (x,s) <- return $ foo 1 [] (y,s) <- return $ bar x s (z,s) <- return $ baz x y s return (z,s)
After all, bind is non-recursive let.
-- Andreas Abel <>< Du bist der geliebte Mensch. Theoretical Computer Science, University of Munich Oettingenstr. 67, D-80538 Munich, GERMANY andreas.abel@ifi.lmu.de http://www2.tcs.ifi.lmu.de/~abel/

In my opinion, when you are rebinding a variable with the same name, there is usually another way to structure your code which eliminates the variable. If you would like to write: let x = foo input in let x = bar x in let x = baz x in instead, write baz . bar . foo $ input If you would like to write let (x,s) = foo 1 [] in let (y,s) = bar x s in let (z,s) = baz x y s in instead, use a state monad. Clearly this will not work in all cases, but it goes pretty far, in my experience. Edward Excerpts from Andreas Abel's message of Wed Jul 10 00:47:48 -0700 2013:
Hi Oleg,
just now I wrote a message to haskell-prime@haskell.org to propose a non-recursive let. Unfortunately, the default let is recursive, so we only have names like let' for it. I also mentioned the ugly workaround (<- return $) that I was shocked to see the first time, but use myself sometimes now.
Cheers, Andreas
On 10.07.2013 09:34, oleg@okmij.org wrote:
Andreas wrote:
The greater evil is that Haskell does not have a non-recursive let. This is source of many non-termination bugs, including this one here. let should be non-recursive by default, and for recursion we could have the good old "let rec".
Hear, hear! In OCaml, I can (and often do) write
let (x,s) = foo 1 [] in let (y,s) = bar x s in let (z,s) = baz x y s in ...
In Haskell I'll have to uniquely number the s's:
let (x,s1) = foo 1 [] in let (y,s2) = bar x s1 in let (z,s3) = baz x y s2 in ...
and re-number them if I insert a new statement. BASIC comes to mind. I tried to lobby Simon Peyton-Jones for the non-recursive let a couple of years ago. He said, write a proposal. It's still being written... Perhaps you might want to write it now.
In the meanwhile, there is a very ugly workaround:
test = runIdentity $ do (x,s) <- return $ foo 1 [] (y,s) <- return $ bar x s (z,s) <- return $ baz x y s return (z,s)
After all, bind is non-recursive let.

oleg@okmij.org wrote:
Hear, hear! In OCaml, I can (and often do) write
let (x,s) = foo 1 [] in let (y,s) = bar x s in let (z,s) = baz x y s in ...
In Haskell I'll have to uniquely number the s's:
let (x,s1) = foo 1 [] in let (y,s2) = bar x s1 in let (z,s3) = baz x y s2 in ...
This isn't a case for non-recursive let. It is one of the rare cases where you might actually consider using a state monad. Greets, Ertugrul -- Not to be or to be and (not to be or to be and (not to be or to be and (not to be or to be and ... that is the list monad.

On 10.07.2013 10:16, Ertugrul Söylemez wrote:
oleg@okmij.org wrote:
Hear, hear! In OCaml, I can (and often do) write
let (x,s) = foo 1 [] in let (y,s) = bar x s in let (z,s) = baz x y s in ...
In Haskell I'll have to uniquely number the s's:
let (x,s1) = foo 1 [] in let (y,s2) = bar x s1 in let (z,s3) = baz x y s2 in ...
This isn't a case for non-recursive let. It is one of the rare cases where you might actually consider using a state monad.
Except when you are implementing the state monad (giggle): http://hackage.haskell.org/packages/archive/mtl/2.1/doc/html/src/Control-Mon... -- Andreas Abel <>< Du bist der geliebte Mensch. Theoretical Computer Science, University of Munich Oettingenstr. 67, D-80538 Munich, GERMANY andreas.abel@ifi.lmu.de http://www2.tcs.ifi.lmu.de/~abel/

On 10/07/2013, at 8:42 PM, Andreas Abel wrote:
Hear, hear! In OCaml, I can (and often do) write
let (x,s) = foo 1 [] in let (y,s) = bar x s in let (z,s) = baz x y s in ...
I really wish you wouldn't do that. After reading Dijkstra's paper on the fact that we have small heads many years ago -- long enough to forget the actual title, sorry -- I realised that I too was a Bear of Very Little Brain and Get Confused Very Easily. I find that that when the same name gets reused like that I get very confused indeed about which one I am looking at right now. If the variable is hidden (as by the DCG transformation in Prolog, or a state monad, I don't get confused about the variable because it isn't visible. If each instance of the variable is labelled with a sequence number, I don't get confused because each variable has a different name and I can *see* which one this is. Yes, sequence numbering variable states is a chore for the person writing the code, but it's a boon for the person reading the code. Me, I'd be perfectly happy with setup (x,s) = state (\_ -> (x,s)) (setup $ foo 1 []) >>= \x -> bar x >>= \y -> baz x y >>= \z -> ... One reason for this is that it makes refactorings like extracting bar ... >>= ... baz ... thinkable. A long sequence of updates is probably crying out for such a refactoring.

I think that a non-non recursive let could be not compatible with the pure
nature of Haskell.
Let is "recursive" because, unlike in the case of other
languages, variables are not locations for storing values, but the
expressions on the right side of the equality themselves. And obviously it
is not possible for a variable-expression to be two expressions at the same
time. The recursiveness is buildt-in. It comes from its pure nature.
For a non recursive version of let, it would be necessary to create a new
closure on each line, to create a new variable-expression with the same
name, but within the new closure. A different variable after all. That is
what the example with the Identity (and the state monad) does.
So I think that the ugly return example or the more elegant state monad
alternative is the right thing to do.
2013/7/10 Ertugrul Söylemez
oleg@okmij.org wrote:
Hear, hear! In OCaml, I can (and often do) write
let (x,s) = foo 1 [] in let (y,s) = bar x s in let (z,s) = baz x y s in ...
In Haskell I'll have to uniquely number the s's:
let (x,s1) = foo 1 [] in let (y,s2) = bar x s1 in let (z,s3) = baz x y s2 in ...
This isn't a case for non-recursive let. It is one of the rare cases where you might actually consider using a state monad.
Greets, Ertugrul
-- Not to be or to be and (not to be or to be and (not to be or to be and (not to be or to be and ... that is the list monad.
_______________________________________________ Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://www.haskell.org/mailman/listinfo/haskell-cafe
-- Alberto.

quoth Alberto G. Corona,
Let is "recursive" because, unlike in the case of other languages, variables are not locations for storing values, but the expressions on the right side of the equality themselves. And obviously it is not possible for a variable-expression to be two expressions at the same time. The recursiveness is buildt-in. It comes from its pure nature.
I'm surprised that it would come down to purity. It looks to me like simply a question of scope. I had to write an example program to see what actually happens, because with me it isn't "intuitive" at all that the name bound to an expression would be "visible" from within the expression itself. I suppose this is considered by some to be a feature, obviously to others it's a bug. I've gone to some trouble to dig up an nhc98 install (but can't seem to find one among my computers and GHC 7 won't build the source thanks to library re-orgs etc.) Because, I vaguely recall that nhc98's rules were different here? Anyone in a position to prove me wrong? thanks, Donn

Donn Cave
Let is "recursive" because, unlike in the case of other languages, variables are not locations for storing values, but the expressions on the right side of the equality themselves. And obviously it is not possible for a variable-expression to be two expressions at the same time. The recursiveness is buildt-in. It comes from its pure nature.
I'm surprised that it would come down to purity. It looks to me like simply a question of scope. I had to write an example program to see what actually happens, because with me it isn't "intuitive" at all that the name bound to an expression would be "visible" from within the expression itself. I suppose this is considered by some to be a feature, obviously to others it's a bug.
In a non-strict-by-default language like Haskell it's certainly a feature. A sufficiently smart compiler can figure out whether a definition is recursive or not and apply the proper transformation, so from a language-theoretic standpoint there is really no reason to have a non-recursive let. I think the proper solution is to identify the underlying problem: general recursion. Haskell does not enforce totality. I'd really love to see some optional totality checking in Haskell. If Oleg decides not to use a state monad, he will still have to be careful not to confuse the numbers, but if he does, then the compiler will reject his code instead of producing <<loop>>ing code. Greets, Ertugrul -- Not to be or to be and (not to be or to be and (not to be or to be and (not to be or to be and ... that is the list monad.

Totality checking will generate a lot of false positives. One would like an analysis that prints an error message if an expression is *definitely* looping in all cases. While I have studied termination, I have not studied non-termination analyses. It is harder than termination. For termination checking, you can over-approximate the control-flows and just scream if you find a *potential* control flow that *might* lead to non-termination. If you do not find such a control flow you can be sure things are terminating. This is how Agda does it. To be sure that something is definitely non-terminating, you need to show it is non-terminating on all *actual* control flows. But usually you cannot statically tell whether in "if c then d else e" d or e is evaluated, so a non-termination analysis without false positives seems very restricted. Still it might be could useful. Having said this, having a termination analysis is not the "proper solution" to the lack of a non-recursive let, it does not establish shadowing behavior I want. Cheers, Andreas On 10.07.13 7:44 PM, Ertugrul Söylemez wrote:
Donn Cave
wrote: Let is "recursive" because, unlike in the case of other languages, variables are not locations for storing values, but the expressions on the right side of the equality themselves. And obviously it is not possible for a variable-expression to be two expressions at the same time. The recursiveness is buildt-in. It comes from its pure nature.
I'm surprised that it would come down to purity. It looks to me like simply a question of scope. I had to write an example program to see what actually happens, because with me it isn't "intuitive" at all that the name bound to an expression would be "visible" from within the expression itself. I suppose this is considered by some to be a feature, obviously to others it's a bug.
In a non-strict-by-default language like Haskell it's certainly a feature. A sufficiently smart compiler can figure out whether a definition is recursive or not and apply the proper transformation, so from a language-theoretic standpoint there is really no reason to have a non-recursive let.
I think the proper solution is to identify the underlying problem: general recursion. Haskell does not enforce totality. I'd really love to see some optional totality checking in Haskell. If Oleg decides not to use a state monad, he will still have to be careful not to confuse the numbers, but if he does, then the compiler will reject his code instead of producing <<loop>>ing code.
Greets, Ertugrul
-- Andreas Abel <>< Du bist der geliebte Mensch. Theoretical Computer Science, University of Munich Oettingenstr. 67, D-80538 Munich, GERMANY andreas.abel@ifi.lmu.de http://www2.tcs.ifi.lmu.de/~abel/

On 10.07.13 6:00 PM, Donn Cave wrote:
quoth Alberto G. Corona,
Let is "recursive" because, unlike in the case of other languages, variables are not locations for storing values, but the expressions on the right side of the equality themselves. And obviously it is not possible for a variable-expression to be two expressions at the same time. The recursiveness is buildt-in. It comes from its pure nature.
@Alberto: you must have misunderstood my proposal.
I'm surprised that it would come down to purity. It looks to me like simply a question of scope. I had to write an example program to see what actually happens, because with me it isn't "intuitive" at all that the name bound to an expression would be "visible" from within the expression itself. I suppose this is considered by some to be a feature, obviously to others it's a bug.
Value-recursion *is* useful in a lazy language, e.g. let xs = 0 : xs builds an infinite (in fact, circular) list of 0s. But it is not always meaningful, e.g. let x = x + 1 simply loops. I would like to be in the position to tell Haskell what I mean, whether I want recursion or not.
I've gone to some trouble to dig up an nhc98 install (but can't seem to find one among my computers and GHC 7 won't build the source thanks to library re-orgs etc.) Because, I vaguely recall that nhc98's rules were different here? Anyone in a position to prove me wrong?
I would doubt that nhc98 would interpret let xs = 0 : xs differently than ghc if it implemented anything close to the Haskell 98 standard. But I am not in a position to prove you wrong. Cheers, Andreas -- Andreas Abel <>< Du bist der geliebte Mensch. Theoretical Computer Science, University of Munich Oettingenstr. 67, D-80538 Munich, GERMANY andreas.abel@ifi.lmu.de http://www2.tcs.ifi.lmu.de/~abel/

quoth Andreas Abel
I would doubt that nhc98 would interpret let xs = 0 : xs differently than ghc if it implemented anything close to the Haskell 98 standard.
What I (so vaguely) remember was a compile error, for some reuse of an identifier where GHC permitted it. I suppose you're right, anyway, probably something else - maybe unambiguous nested shadowing? let x = t + 1 in let y = x in let x = y + 1 in x GHC allows this, and of course there's no recursion. Donn

On 11/07/2013, at 11:09 AM, Donn Cave wrote:
let x = t + 1 in let y = x in let x = y + 1 in x
Still no cigar. nhc98 v1.16 Program: main = print $ (let t = 0 in let x = t + 1 in let y = x in let x = y + 1 in x) Output: 2

On 11/07/2013, at 4:00 AM, Donn Cave wrote:
I've gone to some trouble to dig up an nhc98 install (but can't seem to find one among my computers and GHC 7 won't build the source thanks to library re-orgs etc.) Because, I vaguely recall that nhc98's rules were different here? Anyone in a position to prove me wrong?
I have a copy of nhc98 running (v1.16 of 2003-03-08). Program: main = let ones = 1 : ones in print $ take 10 $ ones Output: [1,1,1,1,1,1,1,1,1,1] So no, nhc98's rules were _not_ different. It would have been no use as a Haskell98 compiler if they had been.
participants (7)
-
Alberto G. Corona
-
Andreas Abel
-
Donn Cave
-
Edward Z. Yang
-
Ertugrul Söylemez
-
oleg@okmij.org
-
Richard A. O'Keefe