five days of advent of code in J
I have no solid plans for doing Advent of Code, but while hanging out in a Discord call, I did the first two days in J in a whim. Then I kept going, and now I'm five days in. (I don't know how much longer I'll last though.)
In the same spirit and to give this blog a solid spin, here's some random observations about J. I may incorporate these into a more polished post later (probably elsewhere, but who knows).
For context, I researched J a bunch while writing about APL-like code golf languages and maybe wrote a toy program or two, and have separately in the past spent some time reading some of the online tutorials and taking notes, but I can't recall having written any "full programs" in J before yesterday, so I would consider myself a beginner. At least, I started Advent of Code this year with a weird subset of J knowledge that might randomly go very deep in some technical detail, but was woefully inadequate for translating ideas to code in practice due to being patchy.
- J is quite hard to learn because it's nigh impossible to search for online.
- Try a query like "reverse array in J". All the results are in Java.1 If not that, they'll all be about JavaScript or Julia or something. Presumably, those other J-prefixed languages are so dominant that there are more people Googling them and accidentally hitting enter way too early than there are people actually trying to learn J. (The answer is to use monadic
|.
.) - The results might be even worse for less obviously programming-related queries; you might pick up something like a random forum thread that contains a username with a J or a reference to the J keybind in some video game. Here's a weird thing I wanted to look into: when I start J (on Windows), I get a "Waiting for printer connection..." dialog with a single "Cancel" button. I don't understand what this is. I don't need or want J to print anything. I can't find any information online linking this error message to J.
- Not even writing "JLang" helps, as a lot of J resources don't include that keyword and there does exist a JLang that's apparently a Java LLVM backend?
- Maybe the best option is just
site:code.jsoftware.com
. - I've asked Claude about J, and it's more helpful, but still hallucinates a lot. I can live with that, but it becomes considerably less useful when sometimes it hallucinates a wrong answer and then gives up when I push back, even when there really is a correct answer. I wanted to build a 2D array from two 1D arrays, just having the inputs as the two rows, and Claude told me to use
,.
, which gives me the transpose of what I want. But actually,,:
works!
- Try a query like "reverse array in J". All the results are in Java.1 If not that, they'll all be about JavaScript or Julia or something. Presumably, those other J-prefixed languages are so dominant that there are more people Googling them and accidentally hitting enter way too early than there are people actually trying to learn J. (The answer is to use monadic
- I put off understanding verb ranks for longer than I should have. Controlling verb ranks is how you apply/map functions in the ways and across the dimensions you want to. Misunderstood verb ranks can manifest as any kind of bug.
- J terminology is... unusual. I feel like it evolved in its own isolated universe, sprouting unique definitions for all kinds of unusual words. I don't mind it that much when the words are distinctively J (e.g. nouns, verbs, adverbs, conjunctions; forks and trains), but it makes it hard to find operations I know from other programming languages because I have no idea what to search for. Examples include:
- "type" is a somewhat vague word in J that doesn't seem to be used consistently unambiguously, but whatever it means, it's not what I want it to mean, which is the category that two atoms share if and only if they can be directly placed in the same array. The word for that category is "precision". On the other hand, (the built-in alias for) examining the precision of a noun is called
datatype
. - The "tail" of a list (monadic
{.
) is its last element, unlike most other functional programming languages where "tail" means all but the first element. (In J, all-but-first is "behead", all-but-last is "curtail".) - The "insert" adverb (monadic
/
) actually performs a fold/reduce. (There are also separate "fold" operations.) - The "do" verb (monadic
".
) actually evaluates a string as code. Also see below... - A "direct definition" is basically J's version of a lambda or an anonymous function. I would have liked to know about them much sooner. Naturally, zero of those words appear on its wiki page.
- "type" is a somewhat vague word in J that doesn't seem to be used consistently unambiguously, but whatever it means, it's not what I want it to mean, which is the category that two atoms share if and only if they can be directly placed in the same array. The word for that category is "precision". On the other hand, (the built-in alias for) examining the precision of a noun is called
- The operator symbols are also likewise quite hard to get into my head. Some aspects of J are very unusual compared to other programming languages, like using
%
for division, and using|
("residue") for the "modulus" operation but with the left and right operands swapped! Those aside, though, I think J's symbolic vocabulary is fairly internally consistent, with strong patterns between most symbols and their.
and:
variants. That's where all the skill points got allocated, I guess. edit: Well, I think the big exception is=
and~:
, which are pretty bad! - It really threw me off to realize that in a function you write
return.
after the expression you want to return, but I understand now that this makes sense in J's world; basically functions return the last expression they evaluate, soreturn.
makes the expression before it the last expression that got evaluated. - Debugging J, and trying to write bug-free J, is quite hard.
- It took me a while to figure out I should often be looking at the shape of something, which implicitly also tells me its rank (how many dimensions it has), but sometimes I still got very confused because a string prints the same as its contents. It took me a long time to find
datatype
to introspect the precision and distinguish them. There was also the time where I typed=:
as:=
for a few solid minutes and couldn't understand why my code was producing weird errors. - Why can't I
assert.
at the top level?- Some J verbs swallow errors in somewhat worrying ways. Many operations that would error in other languages, like taking the head
{.
of an empty list, instead produce an item of "fill atoms": 0 if the list was numeric, the space character if the list was of characters.
- Some J verbs swallow errors in somewhat worrying ways. Many operations that would error in other languages, like taking the head
- It took me a while to figure out I should often be looking at the shape of something, which implicitly also tells me its rank (how many dimensions it has), but sometimes I still got very confused because a string prints the same as its contents. It took me a long time to find
- String manipulation is de-emphasized in J, and it shows. Splitting and slicing strings is kind of a pain. The main way to convert between an ASCII character and its integer value is apparently to index or find the index in
a.
, a built-in array of bytes. There are no escape sequences in string literals, other than two single quotes to escape one of them; if you want a special character, break out of the literal and concatenate a constant onto it. Still, regexes work (and in fact are sometimes easier to write when the backslash doesn't mean anything in the host language's string literals), so it's possible to make do. ".
is quite scary! Firstly, although it's sometimes suggested as a way to convert strings to numbers and I found it in some Advent of Code in J blog posts online, it's literally eval!! Once more with passion: parsing input with monadic".
is remote code execution!!!". 'shell ''cat /etc/passwd'''
my beloved.- The fix is to use
".
dyadically: ifx
is a string,0 ". x
parsesx
as a whitespace-separated sequence of numbers, replacing any malformed numbers with the LHS0
. It also behaves more like a number-parsing function in other ways, like accepting the normal loading minus sign-
. But one worrisome thing about dyadic".
is that, even when called with a single string, the rank of its result depends on the input! If the input is a string consisting of one number, dyadic".
returns an atom (a 0-dimensional value), but if the input consists of zero or more than one number, dyadic".
returns a list (a 1-dimensional value).
- The fix is to use
- Parsing J is weird, or isn't a meaningful concept! When you encounter the J code
a b c
, you actually have no idea how to parse it without knowing whata
,b
, andc
are.- If
a
andb
are verbs andc
is a noun, this is callingb
monadically withc
and then callinga
monadically withb
. - If
a
andc
are nouns, you're callingb
dyadically witha
andc
as arguments. - If all three are verbs (or
a
is a noun and the others are verbs), you have a fork. - If
a
is a verb,b
is an adverb, andc
is a noun, you're modifyinga
withb
and calling the result monadically withc
as an argument. - (And so on...)
- If
- There's an interesting divergence of perspectives between J and other FP languages in terms of what basic constructs are natural or awkward to use, which affects which built-in operations are and aren't higher-order functions. One reason is that the simplest possible product type, the heterogeneous pair, is a very basic component of most FP languages; the "homogeneous list" data type can often be viewed as built on top of it, a la Lisp conses. But in J, homogeneous (and in fact rectangular) arrays are more fundamental and language-native, while heterogeneous pairs are considerably more awkward and need to be built from homogeneous arrays and "boxes". Another reason is something like, when first learning about higher-order functions in a typical FP language, you'd typically start with
map
andfilter
, later expanding intozip
andfold
. But in J, arguably every function call is implicitly a map or zip, which sometimes affects the design philosophy of built-in behaviors.- For example, consider sorting a list in some custom way. Typical built-ins a programming language might provide would be sorting by a custom comparator and/or a custom key projection function (Haskell's
sortBy :: (a -> a -> Ordering) -> [a] -> [a]
andsortOn :: Ord b => (a -> b) -> [a] -> [a]
). This is not what J provides. J's/:
"sort up" operator is a first-order function; it takes a list to sort and a separate list of keys to sort the first list by, i.e. it would be typedsortOn :: Ord b => [a] -> [b] -> [a]
2. I don't think J has a built-in way to sort by a custom comparator. - On the other hand, consider getting all prefixes or suffixes of a list (Haskell's
inits :: [a] -> [[a]]
andtails :: [a] -> [[a]]
). This seems like a pretty reasonable first-order function. The problem is that the returned[[a]]
is unnatural in J, since each item will have a different length and J arrays' items need to be equally long. So, J's prefix and suffix operations\
and\.
are higher-order functions (specifically, adverbs) that immediately map a provided function over the prefixes and suffixes! They'd be typed something like([a] -> b) -> [a] -> [b]
in Haskell. Of course, you can pass the identity function if you don't mind getting your result padded with fill atoms, or pass<
to get a list of boxed lists.
- For example, consider sorting a list in some custom way. Typical built-ins a programming language might provide would be sorting by a custom comparator and/or a custom key projection function (Haskell's
- A lot of cognitive overhead / code commenting goes to tracking the shapes of things. At least this skill is familiar from my day job, where I spend a lot of time working with numpy arrays and their ilk, and am also spending lots of effort track their shapes.
- Right-to-left precedence is another source of friction when writing the code, especially when I mentally compare it to my experiences solving Advent of Code in my own esolang. I am basically always writing code right-to-left, repeatedly moving my cursor to the beginning of the line to prepend the next function.
- On the plus side, this helps make it more intuitive what order the operands of some dyads (e.g. dyadic
#
or/:
) go in. But then this often makes me wish hooks were backwards: that is, if(u v) y
meant(u y) v y
instead ofy u (v y)
, so that filtering by a predicate with#
is more natural. Is it just me?
- On the plus side, this helps make it more intuitive what order the operands of some dyads (e.g. dyadic
I'm a little wary of even mentioning "Java" and all these other programming languages in this blog post, lest I prevent some searchers from finding it because they added
-java
to their search query, as I myself did often when trying to find J posts.↩with the additional requirement that the two arguments are required to be the same length. But in Haskell it would be weird to have a function with this type plus such a requirement outside the type system when the obvious alternative type
[(a, b)] -> [a]
would capture the requirement.↩