Hacker News new | past | comments | ask | show | jobs | submit login
APL Demonstration (1975) [video] (youtube.com)
78 points by emersonrsantos on Sept 29, 2019 | hide | past | favorite | 51 comments



Just in case, Aaron Hsu made a bunch of talks about APL

https://www.youtube.com/results?search_query=aaron+hsu

It's a bit caustic to the mainstream (beauty, terseness over ease and variable names) but I personally find it tickles all my boxes.


Aaron Hsu's work has truly inspired me, and expanded my horizon when it comes to code and programming in general.



Thanks I missed those


I'll also note, I emailed him a random question and he spent a non-trivial amount of time answering it and just came off as a legitimately decent and helpful person. So piling on, I'm also a fan now.


I too want my algorithms to fit in a less than a java method name size


The clarity, simplicity and minimal didactic approach towards making old instructional videos is gone now. We have subscribe/hit-the-bell reminders, annotations, background music, sponsorships, intro and exit greetings and all kinds of unneeded bullshit in video production.

Commercialization of video has ruined this art form. Just compare early Mythbuster episodes to something from 2016. Production quality has horrendously degraded. The money is where the masses are and the masses want entertainment, not education.


As cool as this is, APL has improved a lot in the 34 years since that video was made. Most notably, these days, APL has `direct functions`, which enable a degree of concision and tacit-programming support unmatched in any other programming language (yes, including Haskell).

As a minor taste of the power of direct fns, here's a naive implementation of the Fibonacci sequence with one: `fib←{1≥⍵:⍵⋄(∇⍵-1)+∇⍵-2}`


Trying to pick up some APL over the last year, I've found it mildly annoying to find APL code without explanation. I'm expecting that's fine for experienced users, even trying to work it out is useful, but it's hard to get to a point of being able to do that without some hints.

This fib code in the parent comment translates to:

    def fib(n):
        if (1 >= n):
            return n
        else:
            return fib(n-1) + fib(n - 2)
and it does so with:

    fib ← {  "scriptblock" function declaration
             implicit variable name ⍵ (omega) for the right side argument

    1≥⍵ : ⍵   guard case, if 1>=⍵, return ⍵ 

    ⋄          statement separator, like ; in C-languages

               if no guard clauses matched, remaining code runs

    ∇          recursive call to the function it appears in

    ∇(⍵-1)+∇⍵-2   fib (⍵-1)  +  fib (⍵-2)

    }

More notably, the kind of tacit programming support looks like this:

    (××∘⌊|+1≤2||) N Rounding to nearest even number (favouring away from 0)
which evaluates to a function that takes N on the right, and the tacit train looks like this, from TryAPL.org visualization:

    ┌─┼────┐          
    × ∘  ┌─┼───┐      
     ┌┴┐ | + ┌─┼───┐  
     × ⌊     1 ≤ ┌─┼─┐
                 2 | |  <- N goes in here

The tree evaluates from the lower right. Each triple is a fork, N goes into the right prong, the left prong, then the result of both of those goes into the center prong. Unless there's a constant on the left prong, then that's used as-is. So:

|N for the magnitude; 2|(that) for the remainder of dividing by 2; 1<=(that) to find whether to add 0 or 1 for the rounding, (|N)+(that) to re-do the magnitude of N and add the 0 or 1. xN to get the direction +1 or -1, multiplied by the floor of the calculated result.

N enters in several places, which avoids having to store temporary variables and name them, when you aren't actually interested in new variables.

And this works on a single number, a vector or multi-dimensional matrix of numbers.


In case anyone else is curious, but having trouble parsing that, "1≥⍵:⍵" means "return the input ⍵, for ⍵≤1", "⋄" is a statement separator, and "(∇⍵-1)+∇⍵-2" effectively means "otherwise, return fib(⍵-1)+fib(⍵-2)". ("∇⍵-2" means "∇(⍵-2)", because, as mentioned in the video, APL expressions are evaluated right-to-left.)

The {} indicate that this is a function definition: https://www.gnu.org/software/apl/apl.html#Section-3_002e7

My initial take on this, at least, is that it seems pretty reader-hostile. Maybe you get used to it, though.


It's as reader hostile as mathematics symbol is reader hostile. It's not English, it's not meant to be read like a novel but to be read and understood like mathematics. One of the frustrating thing about programming in English like languages is that words might not mean what they claim to mean. Written language is very ambiguous. Nothing from stoping me from writing something as not_true = true; if (not_true) { ... }. or calling something manager or a process when it's neither. Easy to read and pronounce but harder to digest. The idea of APL is that once you understand the symbol like maths, it's unambiguous, if you can parse it, you can understand. There's no crazy naming to confuse you.


Programming is generally not mathematics, though. Usually, you're doing something considerably more complex, though also more concrete.


That is truly beautiful. I already loved APL, from a distance as it were, but this gives me renewed respect for it.


That's an example of what I call "stupid terse".

When comparing program length, we should count tokens, not characters.

We should give a penalty to excessively long tokens, of course, like pointlessly_long_variable_or_function_names. Say, any token longer than 8 characters counts as two tokens. Any longer than 16 counts as 4 and so on, exponentially. Pairing tokens like { } and ( ) can count as one for the pair. In syntax like f(x, y), the () are essentialy one operator denoting function application, spread out to enclose the arguments.

Your APL

  fib←{1≥⍵:⍵⋄(∇⍵-1)+∇⍵-2}
has 19 tokens. jodrelllblank's Python:

    def fib(n):
        if (1 >= n):
            return n
        else:
            return fib(n-1) + fib(n - 2)
has 27. There is a bit of verbiage with the colons, the else punctuation and the explicit return commands.

TXR Lisp:

  (defun fib (n) (if (plusp n) n (fib (+ (pred n) (ppred n)))))
has 21 tokens (or, if you will, nodes in the syntax tree). Only two more over APL, and provides a clear function definition from which we know it takes exactly one required argument, which is given a name. It exhibits no ambiguities.


Now try to type both and see how much time it takes. Or even better, choose a math problem that makes you experiment and try to express it in both. If you try it long enough, you'll see why it's not stupid.

APL is often described as a calculator, and at least to me it's exactly that. If you are programming math and even more so arrays, you can't beat array languages.

I say that as a scheme fanboy, and I'm currently implementing J in Racket. Incidentally, Aaron Hsu is not only an APL expert but also a seasoned schemer, and it's not for nothing he came up with the "disposable code" idea.

So very much not stupid. But as with LISP, you have put up a good try to see the value.

Also,

  fib=:(-&2+&$:-&1)^:(1&<)M.
Removing memoization and defining 'plusp', 'pred' and 'ppred' as you do, we can get it down to 10 tokens without any golfing, which is less than half your example for the same trivial program.

And furthermore,

  fib"0 >: i. 4 5

  1    1    2    3    5

  8   13   21   34   55

 89  144  233  377  610

 987 1597 2584 4181 6765


Re typing, I bet both Python and Lisp will be faster to type. I am typing English-like text all day already - in form of emails, papers, comments and so on, so I am proficient. Switching to exotic APL syntax will make me type much slower.


Well, you should try it and see if you are right, then. APL really excels at quick experimentation for mathematically-oriented programs. Tacit J even more so (which is what I used above).


I got fast at CTRL-m /frac /partial bla bla in LyX, how are APLers entering that unicode?... digging into the vids...


It varies; they tend to follow to the keys on the early IBM dedicated keyboards with APL keys, styled like this: https://www.dyalog.com/uploads/images/Business/products/us_r...

Nars2000 on Windows has its own application-specific key combos, with Alt+Key for the "lowercase" one and Alt+Shift+Key for the "uppercase" one.

Dyalog APL, one of the popular commercial ones on Windows, installs a Windows system-wide input method which uses Ctrl+Key and Ctrl+shift+key; you can switch to it in any program.

TryAPL.org uses a preceeding backtick so `w becomes omega, and there's a JavaScript bookmarklet which can add that to any text input on any browser page, here: https://abrudz.github.io/lb/apl

(No idea what people are doing on *nix keyboard entry).


You can see Aaron Hsu typing out his APL compiler during a talk here: https://youtu.be/ABG5eSCZPrE?t=686 (readable if you increase the video quality setting)

And you can see both that he is fluent at typing it, and that typing is not the bottleneck when few-character blocks of code are doing large chunks of tree manipulation; thinking or talking is.


It might interest you to read the thoughts of the author of APL, search for "notation as a tool of thought", it's the paper he won the Turing award for. Give it a read and see if it softens your resolve on this.


> When comparing program length, we should count tokens, not characters.

GP said APL had "concision" and you then replied about measuring tokens, ended up using some personal ƒ(tokens,nodes,symbols) for reasons I can't fathom.

> It exhibits no ambiguities

What ambiguities are present in the APL function? If you answer, please write clearly, because I know nothing about APL (I was watching the video for my own didactic purposes).

> That's an example of what I call "stupid terse".

That seems needlessly provocative to me.

Edits: clarity. I think I would like the function definition to be written a little more clearly, but I would need more interaction with APL authors to understand their communication norms for their language.


> What ambiguities are present in the APL function?

For instance:

  ∇⍵-2
is that ∇(⍵-2) or (∇⍵)-2?

If something similar to ∇⍵-2 appears but with another symbol in place of ∇, does it parse the same way?

(I'm not even certain that the ∇⍵-2 excerpt is a syntactic sub-unit that is valid on its own.)


Interestingly, you are right in that APL grammar is ambiguous, but not in the way you describe above.

Programming in APL has been described as the "classical Chinese" of programming, and that's a good analogy. APL requires you to know both its primitives and implicit execution model off by heart to be efficient. You must learn to read it like a natural language, both vocabulary and grammar.

By comparison, LISP has an explicit execution model in that it's explicitly reflected in the syntax by parentheses. Relations between APL primitives are implicit, and that's actually very nice and useful too!


Funnily enough, APL is as unambigous as possible when it comes to order of execution. It's _always_ right-to-left

So ∇⍵-2 does ⍵-2 the applies ∇ to it.

Simmilarly (and initially confusing), 2+45-2 evaluates to: (2+(4(5-2))) = 14

From experience, the simplicity of not having operator precendence is liberating (why do we even need operator precendence in the first place, really?).


Why we need operator precedence is so that math notation can properly serve as a "tool of thought".

The correspondence between arithmetic laws and an algebraic notation which remains sane when not fully parenthesized requires precedence levels. Higher "power" operations must be placed at higher precedence levels. For instance, given

  A(x + y + z)
we can distribute A into the trinomial factor:

  Ax + Ay + Az
That doesn't work if that result has some wacky interpretation like:

  A(x + A(y + (Az)))
And under this rule, although you don't have precedence, you still have an implicit rule of associativity: multiplication and addition share the same precedence level, and associate right to left. You have not been been liberated from ambiguity that requires invisible rules.

Why the higher-powered multiplication must have precedence over lower-powered addition is that at the most rudimentary level, it represents repeated addition. 3y means y + y + y arithmetically. If we want that equivalence to be mirrored in notation, we need the precedence. This allows us to take an expression like x + y + y + y + z and confidently rewrite y + y + y as 3y. The properties of addition and multiplication assure us that it's mathematically correct, and the properties of the syntax, assure us that the symbolic manipulation is semantics-preserving: replacing multiple terms of a sum by a single product has no interaction with adjacent terms due to precedence. That's the gist of it: correspondence between syntax rewrite rules and the semantics of algebraic rules.


I'd argue that having

  A*x + A*y + A*z
evaluate to

  A*(x+(A*(y+(A*Z))))
seems wacky because we've been instilled the idea of operator precedence from early school years. So now, when you see

  A*x + A*y + A*z
you instantly do the grouping in your head.

That's all convention really. (And, sure, conventions are powerful).

The point i was making about simplicity was at a mechanical level. The rule "everything is evaluated right-to-left" is mechanically much easier than having, say, 15 different levels of operator precedence[0].

Another point to make here is that + and * are only 2 operators in a much larger list of operators present in a programming language. So even if you have a strong bias towards following the convention and having * have precedence before +, you still need to figure out evaluation order when new operators are included.

There's a spectrum between spartan simplicity and baroque APIs with lots of convenience features and I find that having a sparse set of simple rules is less mentally taxing and more effective than big API surfaces.

[0] https://en.cppreference.com/w/c/language/operator_precedence


The reason we group

  A*x + A*y + A*z
in our heads somehow is that it has a repeating pattern. Either the grouping is

  (A*x) + (A*y) + (A*z)
Or else it must be

  A*(x + A)*(y + A)*z.
It's not obvious that the

  A*(x+(A*(y+(A*z))))
interpretation of

  A*x + A*y + A*z
lends itself to manipulations of syntactic units that corresponds to arithmetic in the same way.

We can't factor out A. If we divide by A, it just goes to

  x + A*y + A*z.
Then if we divide by A again, we get

  (x/A) + y + A*z.
We don't have the concept of commutative laws being reflected in the ability to simply exchange syntactic elements that are siblings at the same level in the tree.

For instance, these are commutative pairs:

  A*x + A*y + A*z <==>  A*x + A*(A*z) + y.
We can't lose the parentheses around (A* z) because the interpretation of the syntax doesn't respect the boundaries required to express commutativity.

The proper way to eliminate nuisance N-level is Lisp syntax, not some silly version of associativity without precedence. Then we still have those algebraic properties:

  (+ (* A x) (* A y) (* A z)) --> exchange nodes to commute -->  (+ (* A z) (* A y) (A x))

  --> factor A:  (* A (+ x y z)).
It can get cumbersome, but we have a sane way to do algebra by manipulating sanely delimited subtrees in regular ways. People have developed (more importantly, debugged) computer algebra systems in this representation.


The preceding discussion is a good example of "whoosh!" because it fails to understand that APL is an array language. It eschews scalar representation like "A1x1 + A2x2 ...". In APL, you would write this as something like "A +.× x".

The same lack of understanding of the array context also underlies the earlier comment favoring "standard" order of operation that works well for maybe five functions with three levels of precedence but quickly becomes unwieldy for more functions.

Someone else already alluded to this lack of scalability but the power of a strictly positional precedence - not "no precedence" - shows up in array operations. For example, consider reduction by a non-commutative function like "-/A".

In the "standard" order of precedence, since all the subtractions are at the same level, this can be restated as the first item in A minus the sum of the remaining items. This is not particularly interesting or useful.

However, positional precedence interprets this expression as an alternating sum: (2-3) + (5-9) (for the "A" above). Alternating sum is a common, useful construction in mathematics.


I'm sure in the numeric example the forum engine made the result wrong.

Let me try. 2 + 4 * 5 - 2 should be 14...


In HN, asterisks denote italics, which is troublesome for citing pieces of code in-line. Look at your rendered comment!

Reliable code formatting is achieved by putting code in its own paragraph that has at least two leading spaces on each line.


Oops, you're right. Unfortunately, it's too late to edit the post now (can't find an edit button).


> "stupid terse"

Please don't break the HN guideline about calling names. Especially not with the shallowest of all programming dismissals (the old canard about the APLs, tied for first place with "but those parens" about the Lisps).

https://news.ycombinator.com/newsguidelines.html


It's all very well to say your lisp is clear and unambiguous, how long did it take you to realise it's wrong? It took me a good 20-30 minutes of writing a reply about character count, before I tried to format your lisp onto three lines and then noticed.

I think the Python code is very clearly a correct Fibonacci, it looks at a glance like the layout of a procedural language doing a recursive call with a base case and a recursive case. The APL is not as clear in that sense, but knowing the diamond is a statement separator it breaks down into small pieces and is fairly easy to read because there's so little of it to read. The Lisp with 8 pairs of parens, 5 levels of nesting, and use of function names instead of "n - 2", while still being a long run of code where there are almost no symbols except alphabet letters and parens to make anything stand out, makes it difficult for me to parse by eye to see what the intended nesting is, and to make sure the parens are nested as intended.

You have it returning fib ((n-1) + (n-2)) instead of ((fib (n-1))+(fib (n-2))), and I think you might have the condition the wrong way around, if N is positive then the recursive calls should happen. That's not to say you or Lisp can't write a correct Fib, it's to say that I think I would spot errors in the Python much more easily, thus making it "more clear".

Which his another way of saying that I'm not convinced that counting characters is automatically worse than counting tokens - characters are what we type, what we have to read, they are what take up screen space and push other code off screen, they push related bits of code further apart even when it's all on-screen, and they are places bugs can hide. If they're not helping, what are they doing?

Characters are the reason the APL could fit 3x in a single 80 character wide line, and the Lisp code 1.3x in the same line, and the Python code 0.2x in the same line. Not that I desperately want to read 80 characters of uncommented APL, but that adds up quickly. The APL tends to be high level and more declarative, which doesn't show up in Fibonacci, but makes a difference in character count.

----

APL functions in the dfn style (curly braces) can only take an argument on the left and an argument on the right, both optional. The left one becomes alpha ⍺ and the right one becomes omega ⍵. That is to say, because you see omega in the code, you know it too expects exactly one argument, that argument goes on the right, and it has a name. The name is no less meaningful than "n", although no more meaningful either.

The steep learning curve about APL code isn't "what does the code say, those symbols are unreadable", they stop being scary quite quickly and just stay dense, instead it's "how on Earth does that manipulation of arrays solve the problem??"


> I think you might have the condition the wrong way around,

Yes I do! I did this just minutes before rushing out the door, and didn't even bother to run it.

Now, the funny thing is, I was focused on paraphrasing your Python expression for expression, so that it calculates Fibonacci in exactly the same way with the same conventions, so any size comparisons are fair. (Though obviously I took advantage of the pred and ppred idioms.) I wasn't thinking "is this correct Fib", but "does it match whatever the Python is doing".

So now I'm looking at: why did I transcribe the simple conditional backwards? Taking a second look at the Python, I now clearly see the reversed convention in 1 >= n that I read as n >= 1.


> "how on Earth does that manipulation of arrays solve the problem??"

Exactly. The visual cortex easily takes care of the readability question, given time and proper exposure for training.

Thinking of how to solve problems using matrices and indices is a whole 'nother thing. It's quite fun if you're bent that way.


I have a feeling that apl (or j) based calculator app (with buttons for all fancy symbols) could be something. All those nice powerful one liners seem to be suited for such app. I wonder if something like that is available. I know there is j for Android, but having a nice closed interface focused on short clever code would be fun.


https://tryapl.org/ gets 70% of the way to what you're talking about (though it's obviously missing the closed-interface-focused-on-calculator-functionality part)


I have J and Retro (it’s a Forth but they’re similar in spirit) on the iPhone. Doing programming puzzles with those on the train is fun and you don’t have to type much.


I grew up with a HP48g and for some reason I never considered a handheld APL. That would be the only thing to match HP RPL :)


dzaima/APL is implemented in Java and can be built as an Android app; steps in the readme here: https://github.com/dzaima/APL and downloadable APK and screenshots here: https://github.com/dzaima/APL/tree/master/AndroidIDE

(I think; I've never used it)


hehe, pretty nice


I've mentioned to others this already, but I'd pay good money for real APL pocket calculator.


You could probably make this happen by compiling a portable APL to one of the programmable graphing calculators with a working C compiler.


Except that what GP really wants is an APL keyboard to go with that.



I find that his mentioning of "monadic" and "dyadic" instead of "unary" and "binary" interesting, why did he mention those?


As far as I know, this is a peculiarity of APL resulting from Iverson's stance on the language as a "tool for thought"[0]. The best explanation I've heard is that "binary" is most apt to number systems (or matrices!) - but a "verb" in APL is not binary, it is dyadic. The language of verbs and nouns is a similar reflection of the ideas that APL is primarily a _language_ (like English, more so than Fortran) to express mathematics succinctly (with a minimum of ambiguity or syntax).

This is (to me) even more apparent in his next language, J; where he sought to "correct" deficiencies in APL and further expanded the "language" to include things like "gerunds"[1] more explicitly, which are nominally (as defined in an English dictionary):

> a noun formed from a verb, denoting an action or state

Which may be summarized somewhat unsatisfactorily as: "Because Ken said so".

[0]: https://www.jsoftware.com/papers/tot.htm

[1]: https://code.jsoftware.com/wiki/Vocabulary/GerundsAndAtomicR...


It's not totally APL-specific terminology, although it's uncommon in other areas of computer science. You can find it (predating APL) in areas like mathematical logic, where a predicate over two terms is sometimes called a "dyadic predicate". I'm guessing Iverson was familiar with that usage and extended it to APL. I wouldn't read too much into it though. It's mostly just Latin vs. Greek roots (unary, binary, ternary are Latin; monadic, dyadic, triadic are Greek).


It's also likely that his usage preceded the more common one prevalent now.


APL made a lot more sense in the era of 110 baud teletypes.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: