Re:Seemingly impossible functional programs (2007)
by skybrian on Tuesday November 17, 2015 @12:10PM
(#10582458)
This code combines several tricks having to do with lazy evaluation. We can define a type that requires a lazy function to ask for each bit of its input argument one at a time, in whatever order decided by the function's implementation. Furthermore, we can partially evaluate any number of lazy function invocations in parallel without finishing any of them that we don't need.
As a result, laziness reveals information about a function's implementation in a way that strict evaluation does not. It matters which parts of the input a function actually reads.
Similarly, in non-lazy languages, functions that take callbacks reveal information about themselves by calling the callback (or not). We can write tests demonstrating the order in which a function calls its callback.
Re:Seemingly impossible functional programs (2007)
by evanpw on Tuesday November 17, 2015 @02:25PM
(#10583370)
This is exactly right. The lazy version seems like magic, but it's really just exploiting the fact that if computing p(a) never evaluates a(n) for n >= m, then p(b) = p(a) whenever the first m elements of a and b agree, so you can cut off an infinite portion of the search space without checking them all.
I wrote up a Python gist (https://gist.github.com/evanpw/d69f1ccb5edd0b672e9e) which performs this same trick by explicitly snooping on the callbacks. It seems a lot less magical than the Haskell version.
EDIT: The main "cheat" seems to be the ability to identify the predicate which returns false on every input (e.g., by seeing if returns false without evaluating its argument). The continuity argument described in the article basically says that if p is not always false, then there's some input a with a(n) = 0 for all sufficiently large n that causes p to return true, so you could just enumerate all such inputs until one returns true.
Re:Seemingly impossible functional programs (2007)
by brianberns on Tuesday November 17, 2015 @11:36AM
(#10582171)
I don't understand how this program can ever terminate, since there is no base case. Every invocation of find_i starts by calling forsome, and every invocation of forsome calls find_i. How can it possibly end?
Perhaps this relies subtly on Haskell's laziness? If so, I don't think there is a "quick translation" to ML or OCaml.
Re:Seemingly impossible functional programs (2007)
by cousin_it on Tuesday November 17, 2015 @11:45AM
(#10582249)
Not every invocation of forsome calls find_i. It calls p(find_i(...)), but p can return a value without actually evaluating the inner find_i. So yeah, it relies on laziness, though you can translate it into a strict language by inserting lambdas in a bunch of places.
Re:Seemingly impossible functional programs (2007)
by brianberns on Tuesday November 17, 2015 @11:56AM
(#10582343)
Thank you. How does Haskell manage to avoid evaluating find_i(...) before invoking p? Since the inner value is the argument to p, it's hard to imagine how p can do anything until it has that value.
EDIT: I think I see the light. The result of find_i(...) is not what's sent to p. Instead, Haskell passes a thunk that might never need to be evaluated.
Re:Seemingly impossible functional programs (2007)
by Jaxan on Tuesday November 17, 2015 @10:25AM
(#10581581)
I have read this some years ago. I really enjoyed diving into this subject. My favourite part is:
> Common wisdom tells us that function types don’t have decidable equality. In fact, e.g. the function type Integer -> Integer doesn’t have decidable equality because of the Halting Problem, as is well known. However, common wisdom is not always correct, and, in fact, some other function types do have decidable equality, for example the type Cantor -> y for any type y with decidable equality, without contradicting Turing [...] This seems strange, even fishy, because the Cantor space is in some sense bigger than the integers. In a follow-up post, I’ll explain that this has to do with the fact that the Cantor space is topologically compact, but the integers are not.
Re:Seemingly impossible functional programs (2007)
by black_knight on Tuesday November 17, 2015 @05:13AM
(#10580161)
> It is natural to ask whether there are applications to program verification. I don’t know, but Dan Ghica and I speculate that there are, and we are planning to investigate this.
I wonder if they got any interesting results from this investigation. There are some practical applications which works on infinite data (such as streams), but I don’t see immediately how this applies there.
Re:Seemingly impossible functional programs (2007)
by hellofunk on Tuesday November 17, 2015 @04:43AM
(#10580061)
>I will use the language Haskell, but it is possible to quickly translate the programs to e.g. ML or OCaml.
The article predates C++11 by many years, and with functions-as-values and lambdas now in the language, would be interesting to see if it could also be translated to C++.
Re:Seemingly impossible functional programs (2007)
by lmm on Tuesday November 17, 2015 @05:00AM
(#10580114)
C++ still doesn't have (tagged) union types so you'd have to implement Maybe by hand, which is a little tedious. But I don't see anything in this code that requires laziness or the Haskell type system, so you can translate it pretty directly into pretty much any language (though as sibling mentions, in a language without TCO you might have to manually trampoline it - again tedious, and unperformant).
Re:Seemingly impossible functional programs (2007)
by lhecker on Tuesday November 17, 2015 @05:45AM
(#10580268)
AFAIK most compiler will do tail-recursion optimization if the method ends in a tail call to the current function (obviously) or to a "sibling" function (i.e. if a() calls b() which calls a() again and so on) and is sufficiently short to be suitable for inlining.
It's important to remember local objects with destructors will make this impossible, since destructors are run at the end of the function scope and thus after what's seemingly a perfect tail call.
As a result, laziness reveals information about a function's implementation in a way that strict evaluation does not. It matters which parts of the input a function actually reads.
Similarly, in non-lazy languages, functions that take callbacks reveal information about themselves by calling the callback (or not). We can write tests demonstrating the order in which a function calls its callback.
I wrote up a Python gist (https://gist.github.com/evanpw/d69f1ccb5edd0b672e9e) which performs this same trick by explicitly snooping on the callbacks. It seems a lot less magical than the Haskell version.
EDIT: The main "cheat" seems to be the ability to identify the predicate which returns false on every input (e.g., by seeing if returns false without evaluating its argument). The continuity argument described in the article basically says that if p is not always false, then there's some input a with a(n) = 0 for all sufficiently large n that causes p to return true, so you could just enumerate all such inputs until one returns true.
Perhaps this relies subtly on Haskell's laziness? If so, I don't think there is a "quick translation" to ML or OCaml.
EDIT: I think I see the light. The result of find_i(...) is not what's sent to p. Instead, Haskell passes a thunk that might never need to be evaluated.
> Common wisdom tells us that function types don’t have decidable equality. In fact, e.g. the function type Integer -> Integer doesn’t have decidable equality because of the Halting Problem, as is well known. However, common wisdom is not always correct, and, in fact, some other function types do have decidable equality, for example the type Cantor -> y for any type y with decidable equality, without contradicting Turing [...] This seems strange, even fishy, because the Cantor space is in some sense bigger than the integers. In a follow-up post, I’ll explain that this has to do with the fact that the Cantor space is topologically compact, but the integers are not.
I wonder if they got any interesting results from this investigation. There are some practical applications which works on infinite data (such as streams), but I don’t see immediately how this applies there.
Did this ever happen? I haven't seen it.
The article predates C++11 by many years, and with functions-as-values and lambdas now in the language, would be interesting to see if it could also be translated to C++.
As for Maybe, if it is basically an "optional" as in other languages, then the newer c++17 has <optional>, if I'm not mistaken.
It's important to remember local objects with destructors will make this impossible, since destructors are run at the end of the function scope and thus after what's seemingly a perfect tail call.