[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)