4 ms·
Not knowing APL, I unfortunately find the beginning code completely unreadable: ⍸(⊃⍵)@0⊣¯1⌽0 1⍷⍵ [Edit: My thought process here is: "This syntax is so diffi
by jcrites 3y ago
Not knowing APL, I unfortunately find the beginning code completely unreadable:
⍸(⊃⍵)@0⊣¯1⌽0 1⍷⍵
[Edit: My thought process here is: "This syntax is so difficult that I can't even begin to reason about it even using web search. Can GPT understand it? Yep, it seems to."
"Could this algorithm be more clearly expressed in another language? Yes, it looks like it."]
----
I asked ChatGPT (4) to explain this expression (Prompt: "Explain what this APL code does: ___"). In case others find it helpful, the response is below.
First, let's break down each symbol:
1. ⍵ represents the right argument of a function.
2. ⊃ is the disclose or first function, which picks the first item of its argument.
3. ⍷ is the "where" function, which returns a boolean mask of where its left argument appears in its right argument.
4. ⌽ is the reverse function. With the argument ¯1, it rotates its argument one place to the right.
5. @ is the "at" function, which modifies certain elements of an array. (⊃⍵)@0⊣ replaces zeros in its right argument with the first element of ⍵.
6. ⊣ is the left function, which returns the left argument, ignoring the right one.
7. ⍸ is the "where" function, which returns the indices where its argument is true (or in the case of numeric data, non-zero).
Now, let's combine these to understand the whole expression:
0 1⍷⍵ returns a boolean mask of where the sequence 0 1 appears in ⍵.
¯1⌽ then rotates this mask one place to the right.
(⊃⍵)@0⊣ replaces zeros in the rotated mask with the first element of ⍵.
Finally, ⍸ returns the indices of non-zero elements in the result.
So, this code is finding the indices in the array ⍵ where a 0 is immediately followed by a 1, and replaces these 0s with the first element of ⍵. It then returns the indices of the non-zero elements in the result.
[Edit: I'm not entirely sure but this explanation might contain an off-by-one error. I don't quite follow APL enough to tell. The translated alternative does for sure though.]
----
On that note, I'll remark that I find APL's syntax to have distinct disadvantages. I know a number of programming languages well, having written production code in them for decades. I've come across basically every mainstream programming language and find them generally readable, even though I don't have much domain knowledge, because they share so many concepts that are typically reflected with similar syntax (for loops, assignments, functions, etc.) Even relatively more "out there" languages like Haskell and Perl are somewhat readable without a ton of domain knowledge.
I find it very difficult to get on board with languages that value terseness of this sort over readability (like in K and APL – especially with APL's special symbols). Code is read so much more than it's written, in general, that transparency is valuable and important – as well as the ability to search for code fragments to understand their meaning.
I asked ChatGPT4 to translate this code into Rust, for example, to see what it would come up with. Here was its literal translation, that relies on mutating the input (analogous to the APL code). This is similar to what you'd get transliterating the code into C/C++/Java:
fn find_zero_one_and_replace(input: &mut Vec<i32>) -> Vec<usize> {
let first_element = *input.first().unwrap_or(&0);
let mut indices = vec![];
let mut prev_zero_index = None;
for (i, &item) in input.iter().enumerate() {
if item == 0 {
prev_zero_index = Some(i);
} else if item == 1 {
if let Some(index) = prev_zero_index {
input[index] = first_element;
indices.push(index);
prev_zero_index = None;
}
}
}
indices
}
[Edit: I haven't checked whether this is correct]
After some back-and-forth, asking it to write a function that doesn't mutate the input, and uses iterators, it comes up with some code that I think is probably readable even if you don't know the language well (barring some particulars of the syntax):
fn find_zero_one_indices<'a>(input: &'a [i32]) -> impl
Iterator<Item = usize> + 'a {
input.windows(2)
.enumerate()
.filter_map(move |(i, window)| {
if window == &[0, 1] {
Some(i+1)
} else {
None
}
})
}
[Edit: This code fails to detect the leading group of `1s` in the input and return a response beginning with `0`. Corrected code might be:]
fn find_zero_one_indices<'a>(input: &'a [i32]) -> impl Iterator<Item = usize> + 'a {
let mut previous = 0;
input.iter().enumerate().filter_map(move |(i, &x)| {
let result = if previous == 0 && x == 1 { Some(i) } else { None };
previous = x;
result
})
}
[Edit: The corrected form is arguably less readable than the original due to the need to detect a leading window that doesn't start with `0`. This version somewhat weakens my argument about the clarity of the alternative, but this is still readable. Perhaps someone else with more Rust experience can suggest a simplification.]
I find this readable since "windows" here is a direct reference to the concept of windowing, which is collecting sequences of items from a stream into groups. "Enumerate" iterates over a pair of sequence elements and their indexes: `(i, window)`. These concepts exist in a number of languages.
But if you didn't know what these code elements meant, you could search for them: "rust windows", "rust enumerate", "rust filter_map" and get useful results. The stdlib documentation will be the first or second link. For example, if you don't know what "filter_map" does: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter_map https://doc.rust-lang.org/std/iter/trait.Iterator.html#metho... (And the same would be true for Java, Python, etc.)
Each of these elements (windows, enumerate, filter) is syntactically joined into a conceptual pipeline, providing a framework for reasoning about how they work, in a paradigm used by many languages.
Finally we have the detection logic: `if window == &[0, 1]`. Hopefully that's readable, and I expect makes this solution easier to understand than any of the other variants.
I may have somewhat missed the point of the article: it goes on to describe how the solution can be generalized in various ways. But this is also likely true of the canonical solutions in many languages: iterators (in particular), anonymous functions, etc. enable code snippets to compose and generalize in many ways as well.
- rak1507 3y agoWhy has this thing of posting unchecked GPT4 output become popular? It's infuriating. The explanation is wrong, but who cares right? Oh great I see now they've added yet another APL-on-HN trope: the 'equivalent' (note: it's wrong too!) code in some trendy language (and saying APL/K are unreadable). It's like someone crafted the perfect message to piss me off.
- jcrites 3y agoI checked it insofar as I understand APL and what the algorithm was attempting to do: that the algorithm was attempting to detect and return the index of the first element of every consecutive "[0 1]" pair. [Edit: OK, I see the mistake is that it should return the index of the `1`, not the `0`. Yes, that's the kind of mistake I'd catch during unit testing, or after more carefully reviewing the code.] In what way is its explanation wrong? Its summary of the algorithm is: > So, this code is finding the indices in the array ⍵ where a 0 is immediately followed by a 1, and replaces these 0s with the first element of ⍵. It then returns the indices of the non-zero elements in the result. That summary seemed to correctly describe the algorithm, and seemed consistent with the article, so I assumed that its breakdown was correct. I don't understand the individual symbols or operators of APL and can't quickly fact-check the answer, but given that the summary was consistent with the article, it seemed to indicate that GPT had deduced (with no context from the article) what the algorithm was doing in human terms – in other words, whether it was able to "read" the APL code. Whether it's actually mutating the input or not, I don't know. Its description of the transformation seemed to check out, though. Regardless of whether its breakdown is right or wrong, my true intended commentary is about the opacity of the syntax and the difficulty in reading it (which I elaborated on via an edit, after your comment was posted but before seeing it).
- rak1507 3y ago<mean message edited out - despite me really wanting to be mean to someone who's been programming for longer than I've been alive but seems to lack reading comprehension> You don't have to understand APL to know what it's attempting. From the article it clearly is meant to return [0,5,9,11,16], your rust returns [4, 8, 10, 15] (at least the second one, the first one doesn't even compile!).