When you go down certain rabbit holes you develop a fascination with obscure forms of programming and start to realize it has some powerful benefits of which you can never take advantage because it isn't widely adopted.
To free yourself from this curse write ten regular for-loops in C and say a prayer to K&R while tighly holding your copy of The C Programming Language.
I think still potentially very interesting if you're keen on expressing the most computation using the smallest number of characters though!
On the other hand, there might be a limit. I do know how it is to use the Haskell lens package only occasionally, and then having to lookup again what the operators read out loud as "percent-percent-at-squiggly" and "hat-dot-dot" stand for (withIndex and toListOf respectively).
But arguably, the ones that are long and seldom used need looking up anyway, and their name is not that much more useful maybe?
[1] Also compare "2+2*8" with "sum 2 to the product of 2 and 8", or even just "sum(2,prod(2,8))".
- Operators containing `%` apply a function to the target (mnemonic: % is "mod" in many languages).
- Operators containing `=` have a `MonadState` constraint and operate on the monadic state.
- Operators containing `~` operate directly on values.
- Operators starting with `<` pass through the new value.
- Operators starting with `<<` pass through the old value.
- Operators containing `@` operate on indexed optics (mnemonic: indices tell you where you are "at" in the structure you're traversing).
So an operator like `(<<<>=)` means "semigroup append something in the monadic state and return the old result". This is the power of a well-chosen symbol language, and I don't know how you'd do this ergonomically and compactly with only named operators.
Instead, in the kdb team I joined recently (a one-letter, symbol heavy language used by quants), nobody can remember what things do, nobody has time to teach you the insanity of code older than 2 months, and you re instead spending weeks "learning" under the heavy sighs of everyone suffering from the new burden guy.
Why do you want to do things compactly rather than with clarity?
I wish google could search for one-letter language specific symbol so I could ask "what does -9! do on type J vs type C" (nonsensical exemple, I cant remember types without looking at the type table, but I think -9! transforms a bit array into an object)
The only other thing can I find can be an issue with the heavy use of symbols in a language is that it can lead to a certain degree of inflexibility in the language. There are only so many symbols that can be used, and once they're all used up your options are either use normal variable names (and reduce the number of ideas that can be expressed concisely), or make symbols context dependent.
The designers of k went to some lengths to avoid the use of variable names, so some symbols are better than others in terms of clarity. Some mean the same thing everywhere, but others are heavily context dependent in an effort to reuse the limited amount of symbols they have at their disposal.
k is a great domain-specific language, but struggles at being a good general purpose language for a variety of reasons. I'd love to be able to use it for data processing inside of other languages. Let the other less expressive but more robust languages handle the control flow, library interactions, etc., and then run k code to work with data as vectors and tables. If only k supported n-dimensional arrays, then it'd be very interesting to see what it could do if integrated with something like Numpy, but I could spend all day wishing k was better than it is. I'm very happy with what it's able to do, and generally groan when I find myself having to use other less expressive data processing tools (which is practically everything).
I think the "if you are deeply familiar with them" is important. Using non-standard symbols makes comprehension more binary: either you've memorized them and understand or you don't; there's no muddling through, relying on common concepts and terms to make up for incomplete memorization.
That probably also results in a much more binary user base: true-believers who dedicated a bunch of time to become proficient, and non-users who were unable or unwilling to, and not a whole lot in between.
In languages like K or APL, the combination of symbols is the actual definition.
But I'm not strongly in either camp. A few common branches of mathematics taken together have quite a large alphabet of symbols, too, and we work well with it. It feels like symbolic notations can seem obtuse at first and be hard to get into, but once you're used to it the alternative may appear worse.
{x#x{x,+/-2#x}/0 1}
I'm sure if you used K for a year or so, that would be obvious and understandable at a single glance. But all I can think of is that I used to see that in my terminal session right after my modem got disconnected.I think the same problem applies. If you don't remember the specific of a symbol it becomes difficult to understand what the symbol might be without context.
kvPairs.reduce((acc, [k,v]) => ({...acc, [k]: v}), {})
(Which obviously has nothing to do with the K-Snippet but is the first thing that came to my mind that's equally as symbol-heavy)Alternatively:
mut acc = {}
for k, v in kvPairs {
acc[[k]] = v;
}
acc
Am I much wrong? fib = 0 fby ( 1 fby fib + next fib );
That's how this would look in a data-flow language."fby" means "flowed by". It constructs a stream.
So you can be quite terse without loosing the clarity just by using the right abstraction.
Example taken form:
And you don't get to waste time scrolling, your typical module's code fits on your single screen.
Anything about the L1 cache and K is just wrong, usually. At 600KB the K4 database engine is much too large to fit in L1 (K9 from Shakti is somewhat smaller but still a few times too large). And L1 instruction cache misses aren't a bottleneck for other languages, so there's little benefit in reducing them even to the extent K does it.
The long version: https://mlochbaum.github.io/BQN/implementation/kclaims.html
Is that really the bottleneck? I've done quite a lot of profiling on high performance code and I've almost never hit a bottleneck in the instruction cache. Data access bottlenecks or branching hit performance harder and sooner than instruction fetching.
> And you don't get to waste time scrolling, your typical module's code fits on your single screen.
How much of the time you save scrolling is spent on decoding an array of symbols and remembering what those symbols are?
There's a balancing act around that. It's called "abstraction".
At some point, you cannot afford to know what's actually happening. If you try, the entire problem won't fit in your head. So you cut the thing you're working with to a name and an interface, and forget what's "actually happening". You do that right in the K code, because you e.g. cut your understanding of `/` to "fold the array with the preceding operation", and totally don't think about the way it's implemented in the machine code.
This, of course, does have a cost; the simplest case is inefficiency, worse are circuitous ways to arrive to logically the same result, when a much simpler way exists.
I'd argue that `/` or `+` are very much names, of the same nature as `fold` or `add`, just expressed shorter. So if you prefer point-free style, you can likely do a very similar thing in, say, Haskell (and some people do).
I'd hazard to say that APL and J are DSLs for array / matrix numeric code. They allow to express certain things in a very succinct way. What APL traditionally had, mmm, less than ideal experience was I/O, because the language is not optimized for the branchy logic required to handle it robustly. K is a next step that allows to escape into "normal-looking" language with long names, explicit argument passing, etc when you feel like it.
Also, I love this idea: «APL has flourished, as a DSL embedded in a language with excellent I/O. It's just got weird syntax and is called Numpy.» (https://news.ycombinator.com/item?id=17176147) The ideas behind APL / J / K are more interesting than the syntax, and haven't been lost.
Ironically, points-free programming in Haskell has a lot of '.' in it.
Python and Numpy would benefit a lot from having something like k to express vector/matrix operations elegantly.
One common sight is this:
thing
.stuff()
.other()
.whatevs()
Each of the calls returns a different type. Rust-analyzer displays the return type of each call to the right of it.I imagine something similar could reconcile the benefits of terseness with readability and discoverabilty.
The blog post already has the prototype:
+ / ! 100
plus reduce range 100
Imagine the second line being added in by your ide in a light gray.
(0 to 99).sum
That's not much longer, and quite readable even for the uninitiated. (It's a Scala expression, so not made up).or (0 ..= 99).sum() for range including the 99
which makes me think that i don't know how to read (0 to 99) unless i learn whether it's exclusive or inclusive (but at least it's googlable, unlike a random looking operator)
So what would be the benefit compared to just writing `plus reduce range 100`?
Of course once you've come up with clear names for each symbol you could do the opposite, let the IDE turn `plus reduce range 100` into `+/!100`. But as long as IDEs are still glorified text editors and devs care about the representation that gets stored on disk I would argue making the terse notation the default is the right choice.
Naming things is hard. It's one of the 2 most difficult problems in CS, along with cache invalidation and off-by-one errors. Of course, in this context I mean "CS" as "Computer Science", not "CouchSurfing".
For the K programming language, meanings are specifically defined. In the article:
"The word “ordinal” can mean anything, but the composition << has exactly one meaning."
That's fine for the K compiler, but not for Google Search, or grep. I use "<<" to mean a bit shift to the left, presumably because I was taught C in university.
Unique names are more useful for addressing (e.g. IPV6) but common names are more memorable (e.g. a URL. Translators (e.g. DNS) can't be perfect when there's a one-to-many or many-to-one correlation, but they try their best.
K does well to enforce structure, but in the process, makes it very hard for the programmer to find examples and other documentation. I guess that's why the language hasn't become as popular as other languages, whose syntax is sufficiently familiar to be legible and memorable but unique enough to be searchable.
Author builds this up by showing how long is writing "max" function with other approaches (iteration, reduce).
Can be wrong though
...FWIW my understanding of the language is such that I would have suspected the last line of the article to read "|/ list". I thought Max-Over needed an argument.
[0] pg36, http://web.archive.org/web/20050504070651/http://www.kx.com/...
Math.max(...list)e.g.,
Math.max(...Array(100_0000).fill(0))
results in: Uncaught RangeError: Maximum call stack size exceededIt's not at all clear to me why this would be a necessary limit to exist, as functions can't reasonably have more than a few hundred formal parameters so passing 100,000 would always imply using unspread or arguments on the receiver, which could surely trivially handle arbitrarily sized arrays.
> ... wincing slightly at the lambda notation you need to avoid running afoul of JavaScript’s variadic Math.max()...
+/!100
feels like the opposite way you'd write it in some kind of RPN concatenative language 100 ! [ + ] /I believe the reasoning is something like Iverson (or maybe Whitney) didn’t like the complexity of PEMDAS in maths so decided on this rule.
In APL family languages there are infix operators and prefix operators.
Prefix operators take as their operand the result of everything to their right. Infix operators take as one operand everything to their right and take as their other operand the first thing on their left.
Polish notation would be lisp (or the notion most people have for lisp, special forms and macros break it up a bit).
So in some K flavoured LISP I think it would be
((/ +) (! 100))I want to learn things that shape my thinking in a way that makes me more efficient, and in a way that makes it easier to express ideas.
If your language isn't giving me that then it's bye bye.
I get the appeal but squinting your eyes at a string of single-character symbols is taking it too far. The whole thing has to be somewhat ergonomic as well.
When you want to review your code, you just go into normal mode, concealing all lengthy but frequent names into appealing symbols. When you want to write code, you switch to editing mode and all symbols on the editing line turn back into names
I use this feature mostly with python code. One liners making proficient use of lambdas, maps, reduces, and other functional treats are satisfyingly condensed, reducing the amount of attention required to parse each line.
No need for custom keyboards. People don't have to parse arbitrary symbols to understand what I wrote because the underlying code remains vanilla.
A drawback is that the apparent conciseness tricks me into writing very big one-liners difficult to parse when all terms are expanded.
[A] a = A
[A] b = [[A]]
[A] [B] c = [A B]
[A] d = [A] [A]
[A] e =
[A] [B] f = [B] [A]
This the minimal Joy. [[db]fbcabc[da]cfc]da
This program is one I most like.Vg vf gur cnenqbkvpny pbzovangbe.
Stages of denial in encountering K, March 9, 2020, 422 comments https://news.ycombinator.com/item?id=22504106
Wonder how many people did not skip to the end and then skim back
Honestly, while I like the clarity, I would still dislike it in JavaScript for performance reasons, because in JavaScript reduce is not optimized at all. And yes, I do work on a code-base where that difference is significant.
Also, that loop can still be cleaned up a bit in modern JS:
let max = list[0];
for (const v of list) {
if (v > max) max = v;
}If 'reduce' and 'map' had widely used symbols, which were taught in school / university as part of the standard curriculum, how different would coding look today?
> "The term "Kafkaesque" is used to describe concepts and situations reminiscent of Kafka's work, particularly Der Process (The Trial) and Die Verwandlung (The Metamorphosis). Examples include instances in which bureaucracies overpower people, often in a surreal, nightmarish milieu that evokes feelings of senselessness, disorientation, and helplessness. Characters in a Kafkaesque setting often lack a clear course of action to escape a labyrinthine situation. Kafkaesque elements often appear in existential works, but the term has transcended the literary realm to apply to real-life occurrences and situations that are incomprehensibly complex, bizarre, or illogical."
~ https://en.wikipedia.org/wiki/Franz_Kafka#%22Kafkaesque%22
So yeah, if you like it, I get it, go enjoy it. But this smugness of "it's actually better and I'm better for knowing it", it irks me. One of the most important aspects of programming languages is that they need to be used, and K is the single hardest language that I've tried to understand, even with documentation on the other screen and reading just short snippets. A language that people will ignore because they don't even know where to start reading is not that good of a language.
If you ever feel like trying it again, and get stuck on something, https://chat.stackexchange.com/rooms/90748/the-k-tree is the place to ask :)
I start to feel uncomfortable when I see an explicit loop.
But that's actually very weird as I write Scala a lot where you use "for" (instead of "do-notation") for monadic computations.
My brain starts smoking when someone uses a "for" as an regular loop! That causes usually a few seconds of complete confusion. :-D
I think the article tries to make it sound more mysterious and groundbreaking than it really is.
That's...uhh...one way to do it. But it's a lot shittier to read than sum(range(100)).
Since the arity of all the primitives are known there's less parens than lisp. That seems like the clear sweet spot to me.
And I think this is born out with K the product. One of the things they added with Q is a more text oriented syntax.
The big a-ha in APL style languages is shifting from thinking about loops and iterating over single elements to transforming tensor like objects. It's a powerful approach no matter what language and syntax you use.
Maybe K can run those programs faster than Lisp/C/$FAV_LANG, but it's ultimately up to a much smarter programmer to implement that bit.
Also, the primary goal is not always speed... runtime speed or speed of development. There are other things we trade off for all the time.