[HN Gopher] What Learning APL Taught Me About Python ___________________________________________________________________ What Learning APL Taught Me About Python Author : RojerGS Score : 33 points Date : 2023-08-15 11:16 UTC (1 days ago) (HTM) web link (mathspp.com) (TXT) w3m dump (mathspp.com) | hobs wrote: | The only thing this does for me is ask why its not named count | instead of sum. | pavon wrote: | numpy (which is inspired by Matlab which is inspired by APL) | does indeed have a count_nonzero function, which is intended to | be used in situations like this. Unfortunately, it (like most | of numpy) doesn't work with generators, just array-like objects | (aka numpy arrays and python lists), so it has the same memory | performance issues as filtering and using len. | | If your input was a numpy array to begin with you could skip | the array comprehension, and shorten it to | numpy.count_nonzero(ages > 17), since numpy automatically | broadcasts the comparison operation to each element of the | array. | heavyset_go wrote: | from collections import Counter total = | Counter(range(10)).total() assert total == 10 | ok_dad wrote: | In Python a Boolean true and false are often used in | mathematical formulas, so they will often implicitly be coerced | into the integers 1 and 0. Sum is the sum function, you can sum | a sequence of numbers, but in this case it's summing a bunch of | Boolean values which are coerced to 1 and 0. | scherlock wrote: | Because what is happening I the list of ages is being | transformed into a list of booleans where it's true if the age | is greater than 17. This list of booleans is then turned into a | list of integers where it's 1 if true, 0 if false. This list of | integers is then being summed. | kragen wrote: | because sum([1, 2]) is 3 and not 2 | Jtsummers wrote: | It is summing but being used for counting (in imitation of the | same style from APL) via punning on True/False as 1/0. | | Not what actually happens but conceptually: | ages = [17, 13, 18, 30, 12] sum(age > 17 for age in ages) | => sum([False, False, True, True, False]) => sum([0, 0, | 1, 1, 0]) => 2 # via conventional summing | | Since True and False are 1 and 0 for arithmetic in Python, this | is just a regular sum which also happens to produce a count. | naijaboiler wrote: | yeah if i ready the line using "sum", I would be expecting | the result 48 (18+30) not 2 | narrator wrote: | APL makes a lot of sense in the era of 110 baud teletypes in | which it was invented. Brevity was of extreme importance in that | era. | aynyc wrote: | I know nothing about APL. But I think I would write it the same | way as the OP. I also think use len is better to convey counting | operation: | | _len(age for age in ages if age > 17)_ | [deleted] | dragonwriter wrote: | You can't call len on a gen exp, though you could define a | count function. For an unsafe variant: def | count(it): return sum(1 for _ in it) | | Which is basically just putting a friendly name on the approach | from the article. | | For a safe version, you probably want to wrap it in another | generator that bails with an exception at a specified size so | you don't risk an infinite loop. def | safe_count(it, limit=100): # returns None if actual | length > limit nit = zip(range(limit+1), it) if | (l := sum(1 for _ in nit)) <= limit: return l | | Of course, you can just convert to a list and return the | length, but sometimes you don't want to build a list in memory. | vore wrote: | I don't think you can do that with a generator expression. You | would have to write: sum(1 for age in ages if | age > 17) | aynyc wrote: | ah, yes, of course, forgot the generator. | Nihilartikel wrote: | It would eat ram at scale but you could wrap the gen | expression with [] for a list comprehension and that would | work. | [deleted] | nomel wrote: | If you're going to go that route, I think this makes more | sense: count_over_17 = [age > 17 for age in | ages].count(True) | dragonwriter wrote: | For a very large sequence traversing it to build a list and | then traversing the list to do something you could do in | one traversal without creating a list may be undesirable. | [deleted] | ok_dad wrote: | I find that the more language you learn the better you can | utilize all of them. | | Also, Python is a wonderful functional language when used | functionally. | agumonkey wrote: | It really is a strong lesson. Every language will shift and | twist your mind and expand your horizon. You might hate your | colleagues then though. | heavyset_go wrote: | Python's lack of multi-line anonymous functions is a hindrance | to using it as a functional language, IMO. | dragonwriter wrote: | Most functional languages don't have statements at all, and | Python's anonymous functions can, as most, handle any single | expression, regardless of complexity or size. | | Python having a statement heavy syntax and making complex | expressions (while possible) awkward is the problem with its | anonymous functions, not the fact that its anonymous | functions are limited to a single expression. | nomel wrote: | I take the Beyonce approach to functions: if you like it you | should have put a name on it. | chriswarbo wrote: | Multi-line lambdas are fine: Python will accept newlines in | certain parts of an expression, and you can use '\' for | others; e.g. f = lambda x: [ x + y | for y in range(x) if y % 2 == 0 ] | >>> f(5) [5, 7, 9] | | Lambdas which perform multiple sequential steps are fine, | since we can use tuples to evaluate expressions in order; | e.g. from sys import stdout g = lambda | x: ( stdout.write("Given {0}\n".format(repr(x))), | x.append(42), stdout.write("Mutated to | {0}\n".format(repr(x))), len(x) )[-1] | >>> my_list = [1, 2, 3] >>> new_len = g(my_list) | Given [1, 2, 3] Mutated to [1, 2, 3, 42] >>> | new_len 4 >>> my_list [1, 2, 3, 42] | | The problem is that many things in Python require statements, | and lambdas cannot contain _any_ ; not even one. For example, | all of the following are single lines: >>> | throw = lambda e: raise e File "<stdin>", line 1 | throw = lambda e: raise e ^^^^^ | SyntaxError: invalid syntax >>> identity = lambda x: | return x File "<stdin>", line 1 identity = | lambda x: return x ^^^^^^ | SyntaxError: invalid syntax >>> abs = lambda n: -1 * (n | if n < 0 else return n) File "<stdin>", line 1 | abs = lambda n: -1 * (n if n < 0 else return n) | ^^^^^^ SyntaxError: invalid syntax >>> repeat = | lambda f, n: for _ in range(n): f() File "<stdin>", | line 1 repeat = lambda f, n: for _ in range(n): f() | ^^^ SyntaxError: invalid syntax >>> set_key = | lambda d, k, v: d[k] = v File "<stdin>", line 1 | set_key = lambda d, k, v: d[k] = v | ^^^^^^^^^^^^^^^^^^^^ SyntaxError: cannot assign to | lambda >>> set_key = lambda d, k, v: (d[k] = v) | File "<stdin>", line 1 set_key = lambda d, k, v: | (d[k] = v) ^^^^ | SyntaxError: cannot assign to subscript here. Maybe you meant | '==' instead of '='? | nbelaf wrote: | It is a poor functional language. List comprehensions (from | Haskell) are nice, but the rest is garbage. | | Crippled lambdas, no currying, "match" is a clumsy statement, | weird name spaces and a rigid whitespace syntax. No real | immutability. | dekhn wrote: | functools.partial is currying, right? | vore wrote: | No, it's partial application. Currying is when a 1-arity | function either returns another 1-arity function or the | result. | dekhn wrote: | hmm... that just sounds like a specific case of recursive | application of partial functions? At least that's how I | interepret the wikipedia explanation: | | "As such, curry is more suitably defined as an operation | which, in many theoretical cases, is often applied | recursively, but which is theoretically indistinguishable | (when considered as an operation) from a partial | application." | jasonwatkinspdx wrote: | Years ago I stumbled across http://nsl.com/papers/kisntlisp.htm | which is similar in sentiment. | | I think APL's ability to lift loop patterns into tensor patterns | is interesting. It certainly results in a lot less syntax related | to binding single values in an inner loop. | max_ wrote: | Kenneth E Iverson, the inventor of APL was truly a genius and his | primary mission was about how to bridge the world of computing | and mathematics. | | To do this he invented the APL notation. | | If you find the article interesting, you might enjoy my curation | of his work "Math For The Layman" [0] where he introduces several | math topics using this "Iversonian" thinking. | | [1] Look this up to install the J interpreter. | | [0]: https://asindu.xyz/math-for-the-lay-man/ | | [1]: | https://code.jsoftware.com/wiki/System/Installation/J9.4/Zip... | tcoff91 wrote: | I feel like this kind of operation on a list feels more naturally | expressed by filtering the list and taking the length of the | filtered list. | | Like this line of JS feels so much easier to read than that line | of python: ages.filter(age => age > 17).length | | Directly translating this approach to python: | len(list(filter(lambda age: (age > 17), ages))) | | Although a better way to write this in python I guess would be | using list comprehensions: len([age for age in | ages if age > 17]) | | which I feel is more readable (but less efficient) than the APL | inspired approach. Overall, none of these python versions seem as | readable to me as my JS one liner. Obviously if the function is | on a hot path iterating and summing with a number is far more | efficient versus filtering. In that case i'd probably still use | something like reduce instead of summing booleans because the | code would be more similar to other instances where you need to | process a list to produce a scalar value but need to do something | more complex than simply adding. | [deleted] | jph00 wrote: | It feels more natural to you because of familiarity. However, | if you've learned Iverson Bracket notation in math | (https://en.wikipedia.org/wiki/Iverson_bracket) then the APL | approach will probably feel more natural, because it's a more | direct expression of the mathematical foundations. Of course, | the actual APL version is by far the most natural once you're | familiar with the core ideas: +/ages>17 | WhiteRice wrote: | I didn't see it in the article so I thought I would add, | | The actual apl implementation: +/age>17 | | Apl implementation of taking the length(shape) of the filtered | list: [?](age>17)/age | Jtsummers wrote: | It's in there but near the end (80% or so of the way down the | page). The article would benefit from moving that to the top | and drawing the comparison to the APL code earlier. | adalacelove wrote: | If ages is a numpy array instead of a list: | (ages > 17).sum() | dTal wrote: | Numpy is something close to APL semantics with Python syntax. | There's no doubt it was heavily inspired by APL. One could | argue that numpy's popularity vindicates the array model | pioneered by APL, while driving a nail in the coffin of | "notation as a tool of thought", or APL's version of it at | any rate. Array programming has never been more popular but | there's no demand for APL syntax. | nbelaf wrote: | Wait until some core "developer" removes True/False as integers. | They have already removed arbitrary precision arithmetic by | default (you will have to set | sys.my_super_secure_integer_size_for_lazy_web_developers(0) to | get it back). | gorgoiler wrote: | This is completely off topic (though possibly still on the topic | of maximal readability) but the correct way to express this logic | is as follows: age >= 18 | | If your code is specifically about the magical age of adulthood | then it ought to include that age as a literal, somewhere. | | It becomes more obvious when you consider replacing the inline | literal with a named constant: CHILD_UPTO = 17 # | awkward | | compared with: ADULT = 18 # oh the clarity | | My fellow turd polishers and I would probably also add a tiny | type: Age = int ADULT: Age = 18 # mwah! | | (The article was a good read, btw.) ___________________________________________________________________ (page generated 2023-08-16 23:00 UTC)