6 ms·
> simple linear code If you're talking about a sequence of statements, I've rarely come across such a thing. Firstly, the mere existence of statements in a la
by chriswarbo 4y ago
> simple linear code
If you're talking about a sequence of statements, I've rarely come across such a thing.
Firstly, the mere existence of statements in a language is an incredible complication:
- It forks the grammar into two distinct sub-languages (expressions and statements)
- Expressions compose with each other, and with statements; but statements don't compose with expressions.
- It introduces a notion of (logical) time into the semantics ('before' a statement is executed, and 'after' a statement is executed)
- The times of each statement need to be coordinated. Chaining one-after-another is easy (usually with ';'), but concurrency gives an exponential explosion of possible interleavings.
- The computer often cannot help us spot mistakes, and compilers have very little ability to optimise statements.
This is especially silly in Python, where many common tasks require statements, which often requires rewrites (e.g. we can't put statements in lambdas; hence we often need separate function defs (which themselves are statements); pulling those out may lose access to required lexically-scoped variables, requiring even more plumbing and wrapping; urgh.)
Compare this to functional code, where there's only a single language (expressions) where everything is composable; where there is no control flow (only data dependencies); there is no notion of time; mistakes are more obvious (e.g. 'no such variable' errors at compile time); compilers have lots of scope to optimise; and code is trivial to parallelise.
- chronial 4y ago> pulling those out may lose access to required lexically-scoped variables The only situation for this that I can think of is inside a list comprehension / generator expression. You are aware that you can define functions at any scope in python?
- chriswarbo 4y ago> You are aware that you can define functions at any scope in python? >>> lambda myVar: (def foo(): return myVar + 1, foo)[-1] File "<stdin>", line 1 lambda myVar: (def foo(): return myVar + 1, foo)[-1] ^ SyntaxError: invalid syntax
- chronial 4y agoI didn't mention lambdas because this is obviously transitive def f(myVar): def foo(): return myVar + 1 return foo
- chriswarbo 4y agoHere's some made up code: pruned = map( # Remove all elems whose foo is less than the number of elems lambda elems: list(filter( lambda elem: elem.foo < len(elems), elems )) ) Now let's say we want a running total of all the foos. We can insert an expression to do this: total_foos = [0] pruned = map( # Remove all elems whose foo is less than the number of elems lambda elems: list(filter( lambda elem: ( # Add elem.foo to our total_foos accumulator total_foos.append(total_foos.pop() + elem.foo), elem.foo < len(elems) )[-1], elems )) ) total_foos = total_foos.pop() This is rather awkward and "non-Pythonic"; ideally we would use 'total_foos += elem.foo', but that can't exist in a lambda. Hence: total_foos = 0 # Have to define this outside the map, since it's a statement def checkAndAccumulate(elems): """This checkAndAccumulate function is just a wrapper, for closing-over the elems variable (since it's not in-scope outside the map call). Returns the actual checking+accumulating function.""" def checker(elem): """The actual function we want to filter with""" total_foos += elem.foo return elem.foo < len(elems) return checker pruned = map( # Remove all elems whose foo is less than the number of elems lambda elems: list(filter( checkAndAccumulate(elems), elems )) )
- chronial 4y agoI can only quote OP on this: > But if you feel the need to transform simple linear code into a map of higher order functions because of some abstract benefits you're probably doing stuff that's too easy for you and you should get your mental challenge from solving a more difficult problem instead. This code is only complicated because you insist on following some abstract ideal. The actual way to solve this in python is: total_foos = sum(elem.foo for elem in elems) pruned = [e for e in elems if e.foo < len(elems)] Which is shorten than even your first code sample. If you directly translate your last example into sensible python, you get a nice example of some "simple linear code": total_foos = 0 pruned = [] for elem in elems: total_foos += elem.foo if elem.foo < len(elems): pruned.append(elem) The existence of statements in python clearly stands in the way of achieving ideals of pure functional programming. But I think aiming for such ideals is the exact opposite of the point OP was making.
- t43562 4y agoIt might be easier to explain things to oneself in terms of time though - we are not necessarily functional in thinking. e.g. I imagine trying to explain f(g(x)) versus g(f(x)) without using the word "first" or "then"
- chriswarbo 4y agoThat's data flow, which is fine: the dependency is explicit, checked by the compiler, and maintained regardless of where the code lives. For example, if 'foo = g(x)' is defined in one module, and another module does 'f(foo)', then the data flow is preserved. If we try to force things to be the wrong way round, we'll get compiler errors like 'No such variable foo'. Compare that to temporal ordering of statements; if one module executes 'g(x)' and another executes 'f()', how do we ensure the latter occurs after the former? How could the compiler tell us if they're the wrong way around? Very difficult.
- naasking 4y ago> e.g. I imagine trying to explain f(g(x)) versus g(f(x)) without using the word "first" or "then" Use "from" and "to", which talks about data dependencies rather than temporal dependencies. Most people assume dependency order implies temporal order, and then you introduce them to lazy evaluation to decouple even that.
- bazoom42 4y agoWorth noting Haskell ended up reinventing the statement/expression distincion in the do-notation. So apparently the distinction do have value. “Logical time” actually matters when you have side-effects. Of course we can agree code without side effects is simpler to reason about. Unfortunately you need side effects to do anything useful.
- chriswarbo 4y ago> “Logical time” actually matters when you have side-effects "Time" doesn't matter; causality/dependency is what matters. That is modelled quite well by passing around values. For example: let hostname = lookup("hostname") username = lookup("username") password = lookup("password") connection = connect(hostname, username, password) in query(connection, "SELECT foo FROM myTable") The 'query' call depends on the result of the 'connect' call, so it must "happen afterwards". The 'connect' call depends on the result of the three 'lookup' calls, so it must "happen afterwards". The 'lookup' calls don't depend on each other, so they're concurrent (note that concurrent does not necessarily imply parallel). This form of data dependency is no different than, say, arithmetic: to calculate '3 × (2 + 4)', the multiplication depends on the result of the addition, so it must "happen afterwards". (The imperative equivalent would be 'push 2; push 4; +; push 3; ×;') > Worth noting Haskell ended up reinventing the statement/expression distincion in the do-notation Yes, do-notation "desugars" into plugging return values into arguments, like above. This gets a little tedious for things like 'IO', where we pass around a unit/null value to represent "the real world". Still, a nice lesson from Haskell is that there's value in making things opt-in; e.g. many languages have Maybe/Option, but it's less useful in languages which allow 'null' anywhere; many languages have IO/Effect/Task/etc. but it's less useful in languages which allow I/O anywhere; etc.
- garethrowlands 4y agoThe real world type is a bit of an implementation detail in GHC. As a user, you shouldn't really have to deal with it. IO could be implemented differently under the hood, though real world works well in GHC.