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