-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Iteration versus enumeration #16
Comments
I think we need both. Some algorithms are best expressed as recursion (mostly divide and conquer algorithms like quicksort). Some are best expressed as iteration. Iteration has the advantage that it does not introduce a new stack frame. Index variables and arrays make for very clear vector and matrix operations that become a mess with recursion, and unreadable using higher order combinators. |
Conceptually I agree. We will need to dig into the details though. |
Do we support Your ideas for syntax? I think we should retain the ability to construct a |
I think that you can solve the mess of recursion with generators. Can you provide an example where recursion gets messy? I'll try to rewrite it with generators to explain how I see it working. |
function mystery(x) {
var a = [[]]
for (var i = 0; i < x.length; ++i) {
var b = []
for (var j = 0; j < a.length; ++j) {
for (var k = 0; k < x[i].length; ++k) {
b.push(a[j].concat([x[i][k]]))
}
}
a = b
}
return a
} |
Here's the same code written using higher order functions: function mystery(x) {
return x.reduce((a0, v0) => {
return a0.reduce((a1, v1) => {
return a1.concat(v0.map((v2) => {
return v1.concat([v2])
}))
}, [])
}, [[]])
} I find it harder to understand what this version is doing. I put this down to there being type-transformations being done by the higher order functions which you have to work out. With the imperative version the types of |
I just played around with it for a good long while and I agree that this example resists most methods of simplification and that as far as I can tell, the simple iteration method is probably preferable in this case. There's simply no clear way to un-nest the loops, as the accumulator loop stops almost any form of un-nesting. Nested lambdas are very hard to understand and iteration is preferable in that case. You can probably fix this by introducing some complex combinators, but that wouldn't increase simplicity of course. |
Additionally in general the implementation in an eager language with functors is much less efficient, because temporary collections are created. In the OP, I linked to some of my ideas for perhaps improving that efficiency. The obscurity of the algorithm in this pathological case is convincing towards preferring iterators. However the ability to compose meaningfully named functors can sometimes aid clarity. |
I agree there are some clear cases where higher order approaches are preferable. Mapping a function over a collection is straightforward. Anything with maths on the indexes loops are probably better. Another example where loops seem clearer is the Sieve of Eratosthenes (aside: someone recently made a breakthrough improving the memory usage of the sieve by a couple of orders of magnitude). |
List (and other collection) comprehensions offer a nice declarative solution to this (in Python): def perm xss
var tot = [[]]
for xs in xss
tot = [x:ys | x <- xs, ys <- tot]
return tot or Haskell: perm :: [[a]] -> [[a]]
perm [[]] = [[]]
perm xs:xss = [x:ys | x <- xs, ys <- perm xss] |
A recent LtU post and the ensuing discussion, brings back some of the points I was making to @keean in the debate between us about iterators versus enumerables at Rust forum this past May. |
Forcing people into a functional Style is not going to make the language popular. Also even people who advocate this programming style don't seem ro appreciate the real limitations. No optimiser can incorporate domain specific optimisations into representstiond, as this is effectively a huge search operation in an infinite search space. Even humans do this by referring to libraries of pervious results. I am not going to re-prove all the results of prime-number theory every time I need a new prime number for an encryption key, I have a library that incorporates this knowledge. Program transformation is mathematically equivalent to proof transformation, so optimisation is transforming one proof into a shorter, faster one. The solution is to give the programmer the ability to incorporate domain specific optimisations. We don't need a second language to transform the first, as that is just added complexity, we just need a first language that can represent the optimised operations. This means an imperative language with control over memory layout, evaluation order, the individual bits. |
I've been working on a language that allows you to hand-optimize blocks of code, with a theorem solver to guarantee correctness of the optimization, for years. It's a really complex problem and rabbit hole to go down into. I assume in a best-case scenario, it will make the feature (or even language) complex to work with. |
@keean wrote:
The tradeoff is a loss of genericity or generality. It is I guess of another one of those triplet rules where you can't have all three so pick 2: genericity performance extension. |
You can do this with typeclass specialisation, provide a generic implementations for something, and then specialise for different types. The library author declares a new type, say a matrix, and then overloads the genetic operators like '+' with highly optimised code for high performance matrix maths. |
The proposed
But for fair comparison, let’s rewrite the recursion example in Zer0 and not try to make it more sloppy than it needs to be:
Frankly I think the recursion example is now more declarative, easier to read and understand than the one employing imperative loops. Which can be even more concise:
|
@shelby3 I disagree, when I surveyed people about this, it was the fact that the types changed between functions that causes the problems, and neater syntax does not address this. Concat and reduce both do non-obvious things with types. What makes the imperative verison easier for people to understand is that we have objects (a and b) whose types remain the same throughout the algorithm. This is because in real life we don't have machines that a car goes into and and aeroplane comes out of. Instead we are happier removing passengers from the car and adding them to the aeroplane. |
@keean I don’t comprehend your complaint. The type of The higher-order functions (aka HOF) variant is much more declarative about the semantics of what the algorithm is doing. The imperative version uses instead generic loops which have no declarative meaning and must instead analyze the algorithm in imperative detail to figure out what it does. In short, imperative coding with mutability = spaghetti. We can't unravel any part of our program from any other part. It becomes one amorphous blob. EDIT: https://codeburst.io/a-beginner-friendly-intro-to-functional-programming-4f69aa109569 https://flaviocopes.com/javascript-functional-programming/ https://thenewstack.io/dont-object-ify-me-an-introduction-to-functional-programming-in-javascript/ Note to readers: The point of PFP (pure FP) is not to eliminate all mutability. We want to separate the declarative logic from the mutable side "effects", so that we can refactor the declarative part without creating spaghetti cascade of unintended (i.e. opaque) side effects to our intended semantics. That's why the transparent in referential transparency. I wrote in the OP of this thread:
In that linked Rust forum post from May 8, 2016 I wrote:
For another example of the bolded, italicized quoted compositional, declarative advantages of Haskell’s lazy evalution and memoization, c.f. the Fibonacci numbers example:
EDIT#2: I don’t know why readers fail to pay attention to the link I provided above that explains that imperative programming can’t be composed and FP can be composed. That is the entire fundamental point. If the reader misses that point, they might as well have not read anything else above: http://augustss.blogspot.com/2011/05/more-points-for-lazy-evaluation-in.html I’m also recommending that readers who are new functional programming (FP) also read the book Programming in Scala which appears to be available for free as a downloadable PDF online. EDIT#3: I had also discussed this issue on Feb 4, 2018. |
@shelby3 I surveyed people including seasoned functional programmers, and by far the majority found the imperative version easier to understand. You cannot argue against that, it is an empirative fact. You could conduct a larger survery to over turn the results, by showing you have a better sampling of the target audience of the language, but this is a measurement not an opinion. |
@keean I wasn’t disputing your survey results. Nor am I claiming that the results would be different with a larger sample. I think the results would only be different for a sampling of those who like functional programming. The point is that Zer0 should have both paradigms available. So for example one benefit is the functional programmers have an optional choice other than Haskell. Personally I much prefer the declarative variant and will try to teach that in the documentation and how-to guides for Zer0, discouraging the use of Do you have a problem with offering both paradigms? (I am preparing to show how the HOF can be done lazily employing iterators and generators to they are just as efficient as the imperative and also have the benefits of Haskell’s non-strict evaluation strategy.) |
Tikhon Jelvis explained why lazy evaluation is important for functional programming (and I explained up-thread that functional composition requires laziness):
I wrote:
I wrote in the OP of this thread:
In that linked Rust forum post from May 8, 2016 I wrote:
In the above quoted post I was trying to simulate the advantages of Haskell’s lazy evaluation and memoization in a non-lazy (i.e. eager evaluation) programming language. One of the reasons the above post (and actually the entire linked Rust thread) was so convoluted is that I was trying to figure out how to have GC nearly as efficiently as for C++ while achieving those desirable Haskell composition and declarative capabilities. Also the immutability issue is problematic in Rust because its lifetimes require exclusive mutability. Given the epiphany (c.f. near bottom of the linked post) about how to achieve near zero-cost resource allocation and deallocation in an Actor model without Rust’s lifetime woes, I think the above goal can be achieved much more elegantly because we simply don’t need to be concerned with encapsulating the stack frame and young generational heap from the top-down, because the deallocation (of everything not persisting) is performed at the Actor’s stack head instead (independent of the functional compositions inside the call hierarchy of the Actor’s function and stack). Simply have The closure makes these functions not pure (i.e. non-referentially transparent mutation) but remember a non-pure computation can be contained within a pure function. Note these lazy variants of those functions are less efficient when it’s known that the entire for example Obviously there are still all sorts of performance decisions to be made such as for example whether the model an array as a list or list as an array. The array has O(1) random access but needs to know its maximum size when allocated else copying is needed when the array size must be reallocated larger. But for a lazy computation, the array could potentially be unbounded in size (even though it will not all be computed). Whereas, lists have slower access performance and slow random access, but have O(1) when adding new elements because never have to reallocate. EDIT: I was contemplating whether this proposal might conflict with the value restriction, given the type parametrised |
So @keean what is your reaction to the prior post as now edited? Especially Tikhon Jelvis’s point. Do we need to adopt strategies for our libraries to provide lazy versions as well as eager versions? |
@shelby3 I think that generators using 'yield' are a better solution than lazyness by default. Lazyness leads to unpredictable performance in both time and space. Simple loops fill memory with thunks before doing a single thing. I thought about data-flow for a while, but I think that it is a culturally harder for people to think about. Algorithms are described by mathematicians as a step by step imperative process, going back 1000s of years to the ancient Egyptians. This tells us that imperative strict ordering is the way people naturally think of algorithms. So in the end, I think default strict with genrators to provide lazyness where you need are the right solutions, and offer all the possibilities of lazyness with none of the uncertainty (or maybe strict co-routines). Oleg goes through the whole reasoning in much more depth here: |
@keean wrote:
My longish post that I asked for your reaction to, actually proposes the use of iterators as the lazy glue to make function composition lazy and thus efficient. And iterator is sort of analogous to a generator, because it captures the necessary state to return to caller and restart the next time the caller calls it again. So I think we are actually in agreement on that point. I want to make sure you actually understand what my longish post is about.
Now you are reacting to a different facet of my longish post. That is the issue of whether functional composition (e.g. Sorry but I think you are just wrong on this issue. Yet some algorithms are too convoluted to express with functional composition, so in that case I still want to offer the ability to code imperative loops. |
@shelby3 simple functions work well with functional composition, like turn every int into a string. The problems with understanding happen when things go through several intermediate types. Python agrees with me :-) http://lambda-the-ultimate.org/node/587 Of course I favour declarative programming wherever possible, so list/array/map comprehensions like in python make sense to me. |
@keean Eric S. Raymond (the “LISP hacker”) threatened mutiny if Guido removed his lambdas from Python. Guido backed down and did not remove them. My guess is that Guido is not that senior of a programmer. I’m guessing he is more like mid-tier, not senior level like us (based only on a few blog posts of his I read, but I could be mistaken). And he ostensibly wants to dumb Python down to his level. Well maybe I shouldn’t characterize myself as senior, as that should be proven I guess (hopefully over next couple of years). Anyway, the point is that Python doesn’t want to be a functional programming language. This comment was particular poignant:
You can kiss my ass goodbye if we’re not creating a functional programming language here. Also @keean you seem to be forgetting that functional composition can be parallelised and optimised better by the compiler than some imperative spaghetti. The sort of people who might agree with you (actually he prefers Python’s list or generator comprehensions) and Guido also think that redundant semicolons are a good thing. Btw, for comments posted by JustSaying, that was me. |
In theory, but due to side effects in most languages like Lisp, you cannot actually parallelize, and the kind of optimisations needed for performance are not easy, and have not been successfully implemented in any language, so if you don't trust the optimiser with references (which do work well in Ada) then you should not be expecting the optimiser to help significantly here. Anyway my point was not that we should not allow higher order functions, but more that their use should be discouraged from creating unitelligably terse points-free compositions. I am considering disallowing closures though, which are un-functional anyway because they create side-effects when combined with mutation. |
@keean wrote:
I agree except the question is how you define the what is to be discouraged. Up-thread we disagreed on an example that I thought was better expressed in functional composition. So that is why I say I want both paradigms in Zer0 and then let the programmers decide. We can’t make that choice for them, unless we want to be like Python and not have pithy functional composition. Imperative coding does not compose. Monads do not compose. Side-effects do not compose. That is the point of functional programming.
As I recently stated in the Closures are bad? issues thread #33, I want them implicitly when they don’t escape the function where they were instantiated. Remember even you had noted in the past that a pure function can contain impure procedural code as long as the side-effects don’t leak outside (aka escape) the said containing pure function. I want closures only explicitly otherwise. |
I find the standard definition of "pure" too restrictive. The idea of pure functions is to provide deterministic functions, i.e. there is exactly one value for each pair of parameter values. I would also considering functions pure which cause mutations (effects) which escape the current context as long as the mutated value is not involved into the return value. If it is otherwise, then the function is only impure because of drawing non deterministically a value from a container (receiving effect) and not of subsequent mutation (sending effect). So receiving effects and include them into the return value should only create impure functions and not sending effects. Of course it doesn't suffice to compare functions for equality if they can be compared, for this you need an effect system. |
@sighoya algebraic effects allow more fine grained control than monads, and are my currently preferred solution. I also think actors with pure methods, somewhat like state-machines are an interesting point in the design space. We consider the method takes the arguments of the method plus the current state of the actor, and returns the new state would be pure, and yet sending a message to an actor is already impure, so nothing is lost by having impure state. @shelby3 as we discussed before algebraic effects compose, and can be modelled as an "EffectMonad". |
Guys I still have no understanding of the benefits of algebraic effects. After studying them it just looked like more abstraction thicket masturbation to me. In a few sentences, what is the point and compelling advantage?
I have no idea what any of this means. A pure function has a clear definition. Anything else is not a pure function. I wrote in the Why typeclasses? issues thread #30:
|
@shelby3 why effects? Effects allow control over side effects, but unlike monads the compose. |
|
Reminder that Go doesn’t offer tail call optimization (TCO), although note that may not matter as I wasn’t proposing to employ recursion for FP. Rather I proposed employing iterators to implement for example |
We will need to decide which models we provide. This may not be purely a library decision and may reach into the features we offer in the language.
To kick off discussion, I refer to my post from May 8 in the Rust forum.
The text was updated successfully, but these errors were encountered: