J语言中的手写程序
Handwriting Programs in J (2017)

原始链接: https://www.hillelwayne.com/handwriting-j/

## J:一种用于手写代码的语言 作者发现,手写代码时,传统的编码方式令人沮丧,因为代码没有像初稿那样为这种方式优化。这促使他们探索J,一种源自APL的数组编程语言,旨在实现简洁和速度。J使用“动词”(单数和双数)代替函数,并从右到左评估表达式,这带来了一个独特的学习曲线。 虽然功能强大,但J的语法以其密度高和难以阅读而闻名。这正是作者感兴趣的地方,他们假设手写可以释放其潜力。J的结构适合于可视化为二叉树,而高度压缩的代码——隐式动词,可以被解释器优化以提高速度和空间效率。 通过构建复利模拟器的例子,作者展示了如何使用副词和连接词将显式动词转换为更高效的隐式动词,并用注释图表可视化了这个过程。最终,J的复杂性虽然具有挑战性,但提供了一种独特而引人入胜的编程体验,感觉几乎就像“书写咒语”,使付出努力的价值。尽管在协作工作方面存在实际限制,但作者重视J带来的智力刺激和纯粹的乐趣。

Hacker News 新闻 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 J 语言的手写程序 (hillelwayne.com) 7 分,Bogdanp 1 小时前 | 隐藏 | 过去 | 收藏 | 4 条评论 gnabgib 1 小时前 | 下一个 [–] (2017) 当时 (100 分,29 条评论) https://news.ycombinator.com/item?id=15449073 回复 veqq 1 小时前 | 上一个 [–] 在纸上编码通常很有帮助。 顺便说一下,我刚刚发布了这个!回复 gnabgib 1 小时前 | 父评论 [–] 机器人总是会打败你 turtleyacht 24 分钟前 | 根评论 | 父评论 [–] 猜是发布文章的机器人,而不是编码机器人。回复 考虑申请 YC 的 2026 年冬季批次!申请截止日期为 11 月 10 日 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系方式 搜索:
相关文章

原文

I think by hand. It’s easier for me to write my first drafts on a tablet and type them up afterwards. I can’t do this with code, though. Here’s me scrawling out a python function as fast as possible:

That took three times longer to write than type. Something about code being optimized for legibility and IDE usage and lame stuff like that. I still like the idea of writing code, though, so I looked for a language that wasn’t just easy to write, but benefited from being hand-written.

And that brought me down the rabbit hole of J.

J Orientation

J is an array programming language, designed to process data as fast and as concisely as possible. It’s a descendant of APL and shares the same design philosophy. Since it has a different family tree than most mainstream programming languages, it uses different terminology than you’re probably used to. Instead of functions, we have verbs. Unary verbs are called monads, while binary verbs are called dyads. There are no verbs with more than two arguments. All J programs are evaluated right to left.

   6 - 1 = 5 NB. This is a comment
6
   (6 - 1) = 5
1

In the first case, J evaluates 1 = 5, which is false, or 0. Then 6 - 0 is 6. We can also define our own verbs:

   mean1 =: verb define
(+/ y) % (# y)
)
   mean1 i. 100 NB. 0 2 3 ... 99
49.5

When defining verbs, we always use y for the monadic argument and x for the (optional) dyadic argument. # y is the length of the array y. +/ y is “reduce y with +”, aka the sum of the elements of y. x % y divides x by y. This calculates the mean.

mean1 is an explicit verb. J programmers often prefer to write tacit verbs, which don’t reference their operands. This is how we’d write mean as a tacit verb:

   mean2 =: +/ % #
   mean2 i.100
49.5

mean2 has a fork, where three independent verbs are in sequence. (f g h) y expands to (f y) g (h y), turning g into a dyad. In many cases, the J interpreter can do optimizations on the tacit verbs that it can’t on explicit verbs. Our tacit mean is faster and uses less space for arrays of a thousand elements, although our explicit mean pulls ahead for arrays of ten thousand.

J is full of ways of compacting your scripts. x f@:g y expands to f (x g y). (f g) y expands to y f (g y). f \ y applies f to all prefixes of y, such that f /\ y becomes accumulate f over y:

   +/\ i.10
0 1 3 6 10 15 21 28 36 45

J Disorientation

You can probably see the problem here: J is fast and concise but also unreadable. It’s not just that the commands look like line noise, it’s that J programs are actually compressions of hideously-complicated call trees. This is what drew me to the language: its complexity is poorly served by monospaced characters. I wondered if freehand would be better.

The key insight to tacit J is that since all verbs are either monads or dyads, every primitive operation can be expressed as a binary tree. And while explicit verbs can have intermediate state, primitives cannot. This means that all tacit verbs expand to binary trees.

boolean tree

This goes both ways: given a binary tree of verbs and nouns, we can (almost always) condense it to a single tacit verb. We know we’re done when our binary tree has a tacit verb as the root node, y as its right child, and x as its optional left child.

Let’s do an example. I want to write a compound interest simulator, where the annual rate per year can vary with the market. As one step in the process, I need N weighted random numbers to represent return rates per year. I also want to run M simulations at once. That means I want a verb that takes a shape and an array of rates and return a suitable M x N matrix:

   3 4 random_matrix 'aaabbc'
aacb
acbb
baba

Let’s start with the verbs we’ll need.

  • $ is the “shape” verb. If x is an array of dimensions and y is a number, then x is a matrix of that shape filled with the number. If y is an array, it will cycle the array to fit.
   x =: (2 3) $ 3 9
   x
3 9 3
9 3 9
  • ? is the “random” verb. ? y is an integer in [0, y). If y is a matrix then it applies the verb to each cell.
   x { 'abcdefghijkl'
djd
jdj

With all of these pieces, we can express the problem as “find the length of y, initialize a matrix of shape x filled with len, randomize every cell to an integer between 0 and len, then use the random numbers as indices against y.” The binary tree for our random matrix verb looks like this:

random1

   explicit =: dyad define
(? x $ (# y)) { y
)
   (3 4) explicit 'aaab'
aaba
aaab
abba

This gives us an explicit verb. However, we don’t yet have a tacit verb: our binary tree is four levels deep. The next step is to use adverbs and conjunctions to transform the tree into a tacit verb.

First, x $ (# y) can be turned into a dyadic hook. While (f g) y is y f (g y), as a dyad x (f g) y is x f (g y).

hook random2

Next, we can use the @: conjugation. As mentioned before, x f@:g y is f (x g y).

at random3

Getting closer! This almost looks like a dyadic fork: x (f g h) y is (x f y) g (x h y).

fork

However, we only have that on the left side. We could get the right side to match if there was some noop f such that x f y = y. It wouldn’t affect the computation, but we’d get the right side in the form for a fork. Fortunately, we do have a noop: ], or the “right side” verb. x ] y = y. Let’s add that.

right random4

And now we can fork!

random5

Now we have a single verb root with x and y as child nodes. This is a valid tacit verb.

   tacit =: (?@:$ #) { ]
   (3 4) tacit 'aaab'
abab
aaaa
baab

We can compare relative performance of the two by using the special ts phrase. J code is so concise it’s often easier to copy and paste utility phrases than import entire libraries for them. ts will take a sentence and an iteration count and compute the average of the runtime and bytes taken up for space. We’ll average 100 iterations of randomizing a 1000 x 1000 matrix with one of 500 random values:

   ts=: 6!:2 , 7!:2@]       NB. Time and space
   shape =: 1000 1000
   vals =: i. 500
   100 ts 'shape explicit vals'
0.0192691 2.51706e7
   100 ts 'shape tacit vals'
0.0171166 1.67793e7

Our tacit verb is about 10% faster and uses only two-thirds the total space. Surprisingly, though, we can make both much faster by increasing the size of the value array!

   vals =: i. 512
   100 ts 'shape explicit vals'
0.00761634 2.51706e7
   100 ts 'shape tacit vals'
0.00512317 1.67793e7

Our new explicit is twice as fast as before, because J has special code for picking random numbers in [0, 2^n). However, our new tacit is over three times as fast as before! Special codes often synergize in ways that make tacit verbs extremely efficient.

J Reorientation

We saw being able to sketch our call trees helped us write a tacit verb. But can we also use drawings to read them? I think so. (?@:$#){] Is monospaced, using symbols that don’t carry a lot of semantic meaning. We can use annotations, markups, and different colors to make it more legible. Step one, let’s remove the parenthesis and annotate which verbs form a hook and which form a fork.

draw1

@: is a conjugation; let’s call that out by making it green. We added ] as a noop, let’s mark that in red:

draw2

Finally, let’s lower the hook verbs to make the order of evaluation clearer:

draw3

We can also use another color as comments, use highlights, etc. I’m still experimenting with what annotations add the most useful information when reading a verb. Here’s an older experiment I tried before settling on marking noops:

comments

Thoughts

J feels like the perfect language for crunching huge amounts of data. Unfortunately I never have to crunch huge amounts of data and even if I did the syntax would probably scare my coworkers away. Not everybody is as enthusiastic about handwriting everything as I am. So J was a great language to learn from a self-improvement perspective but I don’t see myself doing very much else with it. The financial simulator is coming along pretty well, though!

The real value of J only hit me when I wrote this essay. Sitting on the bus drawing binary tree transformations, marking glyphs in a rainbow of colors and annotating them with lines and curves, I suddenly felt like I was scribing a spell. I got to imagine I was a crazy old wizard. That more than anything else is why I love J. Because if we can’t have fun with programming, why bother?

联系我们 contact @ memedata.com