Le samedi 28 novembre 2015 21:40:58 UTC+1, Paul Rubin a �crit�:
For example, if you wanted to explain the factorial to a student who
has never been exposed to the concept, chances are that your first
approach wouldn't be to write on a blackboard
"fact 1 = 1 -- [order of these two lines switched by me -Paul]
fact n = n * fact (n-1)"
But that's the mathematical definition of the factorial--this isn't
grade school. I assume I'm explaining it to a professional trying to do high-assurance programming so of course that's the definition I'd use. Treating the above as Haskell code that purports to compute the
factorial, it's easier to reason about than the imperative loop version:
Let P(n) be the proposition "fact(n)=n!". We want to verify that P(n)
is true for all n. We prove it by induction. First, P(1) is obviously
true. Second, assume P(n) is true. So by substitution, fact(n+1) =
(n+1) * fact(n). And since P(n) is true by the induction hypothesis, fact(n+1) = (n+1) * n! which is (n+1)!, which means we've proved P(n) => P(n+1). Therefore by induction, forall n. P(n), QED, so we've verified
the code, and this was very easy.
I'm out of my depth here, but I think to do the same thing with
imperative code is much messier, you have to put preconditions and postconditions around every statement to prove the loop invariant that
you're going through factorials, etc. I guess you can do that with
SPARK but I don't know how it's done.
I'm not sure I understand. Here, you show an example of functional code verification which is done by hand, then you say that it would be much more difficult to do it automatically with imperative code. That looks like a comparison of apples to orange to
me.
I can verify imperative code manually, too. In fact, that's one of my favorite debugging techniques : sitting down, taking a pen and a piece of paper, carefully reading through the code I've just written, manually going through a few iterations with a
simplified input, and wondering if it does what I want.
But if you want to go into automated verification, you need to somehow instruct the formal proof system about what you're trying to do, and this is the messy part. For simple functions like factorial, you will effectively end up duplicating your code in
the proof system's own language for preconditions/postconditions/invariants.
To the best of my knowledge, this is true no matter whether you're using a functional or imperative programming language. It's a fundamental limitation of formal proof (and possibly a reason why it has failed to become mainstream so far, outside of
niches like critical systems).
At the level of everyday coding though, going from loops to recursion is nothing to be bothered about, trust me. It's like when people freak out
by whitespace as syntax when they first see Python, or by the
parentheses when they first see Lisp. You get used to it quickly and it stops mattering. Or anyway, if it's really an obstacle, FP probably
isn't for you.
As I said on CLA, I agree that recursive code is relatively easy to write. My concern is how easy to *read* it is after a couple months of developper working on another project + code bitrot.
In imperative programming, recursion is just one tool in your toolbox among many others. When you meet a problem which recursion fits best, such as quicksort or tree traversal, you use recursion. When you encounter a problem which loops fit best, such as
traversing a data structure while accumulating some intermediary state or mutating a data structure, you use loops. When neither approach has a clear advantage, you use loops because they are easier to understand.
But in functional programming, it seems to me that people will often try to shoehorn recursion into problems which it doesn't fit well at all. This will result in complex code, where recursive functions have parameters which only serve to accumulate
state changes, and where multi-step algorithms are implemented using complex nested trees of pattern matching, possibly further obscured by gratuitous use of higher-order functions.
It is this kind of process which I am criticizing : it results in code which is about as easy to read as imperative code where all control flow is done using goto statements, and I am absolutely not convinced that codebases doing this will remain
readable once they aren't fresh in a developer's mind anymore.
Hence my opinion that the best attitude is probably that of functional programming languages embracing imperative constructs, like OCaml, or imperative programming languages embracing functional constructs, like C++ or Python. Do not force developers to
use the functional approach when it is suboptimal, and trust them to pick the option that fits their problem domain best.
In any case, if you are using a machine which can only perform one
binary operation at a time, you will always need an accumulator to
compute the final result.
I think that's an advanced, low level concept though. There's tons of
magic in Javascript: JIT compilation, garbage collection, built-in
objects that make RPC calls, etc. But there's zillions of
not-so-experienced Javascript programmers writing crappy web pages all
over the the world, who never think about that stuff and have never
heard of an accumulator. A programmer with deep knowledge doing
bleeding edge systems has to understand what the machine is doing, but someone just trying to bang out everyday tasks can usually stay at the
high level. No accumulators.
I'm sorry, I just cannot let Javascript be categorized as a beginner-friendly programming language. When a programming language lets you, without even an implementation warning, do atrocious things at runtime like rewriting the standard library or
summing values of different types, it may give the illusion of ease of use, but painful debugging experience will quickly correct this false impression.
Anyway, if you want to write even mildly efficient code, you need to have at least a basic idea of what the implementation is doing. Otherwise, you end up overloading the GC with thousands of throwaway objects, as in beginner-level Java code. Or you end
up crashing your program with stack overflows as soon as the datasets become non-trivial, as in beginner-level functional code. Or you end up using plenty of indirection in your data access when you could have used fast tables instead. And so on.
Not caring about the implementation is a luxury that is only available when your program requires far smaller compute capabilities than what the underlying machine+OS can provide. Though I will agree that it is certainly nice when you can do it :)
I'm writing programs for massively parallel GPU processors, and last
time I checked Blelloch's scan was still one of the most efficient algorithms
That's outside my area and maybe a niche, but I know some Haskell and FP
work has been done on this stuff (Data Parallel Haskell is inspired by Blelloch's NESL nested parallel work):
http://conal.net/blog/posts/parallel-tree-scanning-by-composition http://www.cs.cmu.edu/~rwh/papers/iolambda/short.pdf https://wiki.haskell.org/GHC/Data_Parallel_Haskell/References
There is also an array library with GPU support:
http://hackage.haskell.org/package/accelerate
Here, my point was not that it is impossible to use functional algorithms on GPUs, only that sometimes, mutable data structures and associated algorithms are the most effective way to achieve some goals on shared-memory architectures.
For example, in this post that you mentioned...
http://conal.net/blog/posts/parallel-tree-scanning-by-composition
...it takes the author 10 A4 pages of code (accompanied by a bit of discussion and mathematical concepts) in order to implement an algorithm which I can describe in one schematic, then implement in a few dozen lines of code which anyone familiar with
imperative constructs can understand. I hope we can agree that the signal-to-noise ratio is not in favor of the functional approach here.
The reason, I believe, can be found in one core tenet of eXtreme Programming and agile development in general: never use an algorithm which is more general than what you really need. Instead, generalize algorithms only when you find yourself doing
something closely related to what you've done before.
It seems to me that more often than not, the functional approach is rather to take the most general abstraction that will possibly fit, then painstakingly go through the difficult process of shoehorning this complex abstraction into a simple problem. It
is not without reminding how Java developers will sometimes design extremely complex class hierarchies to solve problems which are adressed by 10 lines of procedural code.
--- SoupGate-Win32 v1.05
* Origin: fsxNet Usenet Gateway (21:1/5)