KlongPy: High-Performance Array Programming in Python
github.com
github.com
KlongPy: Vectorized port of Klong array language - https://news.ycombinator.com/item?id=35400742 - April 2023 (8 comments)
Klong: a Simple Array Language - https://news.ycombinator.com/item?id=21854793 - Dec 2019 (73 comments)
Statistics with the array language Klong - https://news.ycombinator.com/item?id=15579024 - Oct 2017 (12 comments)
Klong – a simple array language - https://news.ycombinator.com/item?id=10586872 - Nov 2015 (21 comments)
If you've already seen this, and aren't interested in another look then just move on.
> (Reposts are fine after a year or so; links to past threads are just to satisfy extra-curious readers)
but that's too tedious to do routinely.
Nice.
The syntax has the same problem as perl in that you have to learn too many symbols that are hard to look up. And this combined with the tacit style makes it difficult to parse what the code is doing.
I think ruby went in the radically opposite direction in that it provides a similar feature set to perl with similar functional features but everything is human readable text. I feel like there’s room for a human readable version of J that relies less on syntactical sugar.
I do think the direction Klong has gone with the syntax is an improvement: https://t3x.org/klong/ambiguity.html
I’m curious if anyone has a go-to article/example of why an array language would be a good choice for a particular problem over other languages?
I know the second chapter of J for C programmers is kinda like that but I didn’t really find it convincing: https://www.jsoftware.com/help/jforc/culture_shock.htm#_Toc1...
Thinking of something as `each | map | filter | sum` is waaay less buggy than writing bespoke procedural code to do the same thing. No doubt there is a "cost" to it as well, but the _abstraction_ is valuable.
Now, if there were a "compiler" which could optimize that whole pipeline down and squeeze out the inefficiencies between steps because it could "see" the whole program at the same time. (oh, I don't know, something like `SELECT * FROM foo WHERE a > 100 AND b < 9000 LEFT JOIN bar ON ...etc...`)
...perhaps you could get both an expressivity gain (by using higher level concepts than "for" and "while"), a reduction in bugs (because you're not re-implementing basic work-a-day procedures), and an improvement in efficiency (because the "compiler" can let you express things "verbosely", while it sorts out the details of efficiency gains that would be tenuous to express and keep up to date by hand).
I also think there is some merit to “high syntactical density” clearly if you can see the entire code in one place instead of having to navigate through many files or sections that’s beneficial. (Heavily discussed in the last big HN thread: https://news.ycombinator.com/item?id=38981639)
I also think JQ has proven the merit of tacit functional languages in that you can concisely write arbitrary transforms/queries on json that can be more expressive than SQL (many SQL engines have added JSONPath anyway). And I also think postfix is great for processing pipelines.
But I am not totally convinced in the approach of APL/J/Q/KDB for the combination of terse style + prefix + tacit because it makes the code so difficult to read. I think if you took an approach similar to JQ where instead of relying on symbols operators were just human readable words it would be easier to get us half way there to trying out the verb, adverbs etc. approach of the APL family. The problem with making it human readable text is that you lose the conciseness which is part of the draw of the APL family as they want to have a high syntax density and analogous code to mathematical expressions.
For Python, support `prql-to-sqlite3` (in memory?) "out of the box"
For JavaScript, support "array-of-dicts/objects" with no ceremony.
Basically, in python I could justify the depends if I can say:
sql = prql.parse( """select * ...""" )
results = db.execute(sql)
In JS, similarly: all_my_records = [ { ... }, ... ]
function oh_god_why_nevermind(...) { ... }
subset = prql.execute(
`select * from all_my_records ...`
)
(butchering all syntax, of course)...basically "nobody wants to learn sql nowadays", and in javascript "my kingdom for a left-join!"
Pitch it as "graphql for data" (/zing!)
I've recently been updating my stack in other areas and have been utilising "Getting Started" or "Quick Start" sections a lot and thought that we're actually not making that easy enough or easy at all.
I'll look into incorporating your suggestion soon.
If you're up to it, you can open an issue in our repo and then I can ping you when that's done.
> In qSQL this is: aj[`sym`time; t; q], which means perform an asof-join on t, looking up the nearest match from table q based on the sym and time column.
> In standard SQL, again you’ll have difficulty: sql nearest date, sql closest date even just the closest lesser date isn’t elegant. One solution would be:
> WITH cte AS (SELECT t.sym, t.time, q.bid, ROW_NUMBER() OVER (PARTITION BY t.ID, t.time ORDER BY ABS(DATEDIFF(dd, t.time, p.time))) AS rowNum FROM t LEFT JOIN q ON t.sym = q.sym) SELECT sym,time,bid FROM cte WHERE rowNum = 1
> It’s worth pointing out this is one of the queries that is typically extremely slow (minutes) on row-oriented databases compared to column-oriented databases (at most a few seconds).
This is a really nice example but I think it’s more about this as of join being a really useful operation, it appears both pandas https://pandas.pydata.org/docs/reference/api/pandas.merge_as... And duckdb https://duckdb.org/docs/guides/sql_features/asof_join.html
Pandas:
> pd.merge_asof(t, q, on='time', by='sym', direction='backward')
Duckdb:
> SELECT t.sym, t.time, q.value FROM t ASOF JOIN q ON t.sym = q.sym AND t.time >= q.time;
So it seems more like this is benefitting from a useful feature of time series databases rather than the features of an APL-family language.
Personally I find the pandas syntax to be the most straightforward here.
It's just a regular join with the `asof` keyword in front.
The pandas version assumes both fields have the same name, it separates the on and by conditions in a way that's not necessary in SQL version, and imo the "backwards" keyword is a bit ugly and error prone.
https://github.com/briangu/klongpy/blob/main/docs/quick-star... links to an "API Reference" and a "REPL Reference", but the links are broken.
It also simplifies things as no need to implement BIDMAS
foo(bar(baz(42)))
and then remove the superfluous parens foo bar baz 42
The expression is evaluated from right to left.Now, let's make two of the functions into object members:
A.foo(bar(B.baz(42)))
Remove the parens, extracting the methods from their objects, instead feeding each object as a left argument to its former member function: A foo bar B baz 42
This is normal APL-style call syntax; right-to-left if you want.In '1 + 1', + is a dyadic operator, while in 'exp 5', exp is monadic.
In J, and in APL I guess, left arg is usually understood as 'control data', while right arg is the data upon which calculation is done. Left argument is usually left unchanged after calculations.
In this way, is it possible to create multi-arguments verbs, by placing boxed args on the left of the verb.
There's also a subset - possibly even a silent majority - of problems where you're doing fairly intensive calculation on data that fairly comfortably fits into a modern CPU cache. For those, I'd still expect SIMD to be a great choice from a performance perspective.
I don't really think of it as a "losing" performance problem. It's all about tradeoffs. SIMD tooling often offers an easier programming model than SPMD tooling, and spending an extra 60 minutes of development time to save myself 60 seconds of run time is generally not a worthwhile trade in my problem space. Especially if I also need to run it on more expensive compute instances to realize that speedup.
If someone says "this isn't really high performance" and you say "I don't personally care, my stuff is small and doesn't take long time to run" that isn't a coherent reply.
I find it to be much less coherent, nearly to the point of knocking down straw men, when people get lost in arguing semantics over a term that we should all know by now is perennially ill-defined.
High performance means you show that its speed is competitive against the other fastest programming methods.
You labeling something "high performance" because you wrote something slow and made it faster has nothing to do with releasing something and calling it "high performance" publicly.
I think it's an oversimplification to say SPMD is a better model full stop. Array programming has the advantage that the implementer optimizes a specific function, and its interaction with the rest of the program is much simpler: all the data is available at the start, and the result can be produced in any order. So things like multi-pass algorithms and temporary lookup tables are possible, particularly valuable if the program isn't spending that much time on arithmetic but rather "heavy" searching and sorting operations. Not that I claim NumPy or CuPy do a good job of this. Sections "Fusion versus fission" and "Dynamic versus static" are relevant here, I think: https://mlochbaum.github.io/BQN/implementation/versusc.html#...
Without getting into any discussion of the array paradigm itself, the reason commercial programming is such a winner-take-all system now is hiring and organizational difficulties, not that popular languages are so much more productive than unpopular ones. Building a working application is the easy part of running a business.
A few years ago there was an article about K's use in high-frequency trading. I'm not sure about usage of APL and J, though. BQN is still fairly new, so it will take a while to see much production usage.
If you've ever written code using NumPy, Tensorflow, or PyTorch, you're doing array programming. Those libraries are heavily influenced by the array languages, including taking a lot of terminology (rank, etc.). I've personally found that playing with J and BQN helped me understand Tensorflow, and vice versa.
AFAIK that's a linear algebra term for matrices, not something array programming or Pytorch or TF invented.
- array languages: rank is the dimensionality of an array, i.e. a vector is rank-1, a matrix is rank-2, a N-D array is rank-N
- linear algebra: rank is the number of linearly-independent columns (and also rows)
So for example, if you have a 5x5 matrix where 4 of the columns are linearly independent, it would be rank-4 in the linear algebra sense, and rank-2 in the array language sense.
I guess (though I've never really thought of it before) that you could say that the array-language definition is the rank (in the linear algebra sense) of the index space. Not sure if that's intentional.
?> sum::{+/x} :" sum + over / the array x
:monad
?> sum([1 2 3])
6
?> count::{#x}
:monad
?> count([1 2 3])
3
What?sum::{} is the creation of a monadic function (which I just learned about because I don't have a CS background)
:" is a comment
sum([...]) is calling the function with a list, and the function will iterate over the list adding each of the elements
count::{#x} is another monadic function that returns the number of elements, which then gets called
Embedding KlongPy in a Python block would look more like this (also from the docs):
from klongpy import KlongInterpreter
import numpy as np
data = np.array([1, 2, 3, 4, 5])
klong = KlongInterpreter()
# make the data NumPy array available to KlongPy code by passing it into the interpreter
# we are creating a symbol in KlongPy called 'data' and assigning the external NumPy array value
klong['data'] = data
# define the average function in KlongPY
klong('avg::{(+/x)%#x}')
# call the average function with the external data and return the result.
r = klong('avg(data)')
print(r) # expected value: 3
Note the calls to "klong('<some-str-of-klong-syntax')". name::{body} :" function declaration
This isn't necessarily niladic or monadic or dyadic on it's own, it depends on the body. The bodies from the example all just happened to be monadic. +/x :" monadic : sum over argument x
#x :" monadic : count of the argument x
+ :" dyadic : sum
!10 :" niladic : an array from 0 to 9
I had to look up how calling a dyadic function looks in Klong: add::{+}
add(2;3)> count::{#x}
These two expressions are not valid in R or Python. They are implicitly declaring a function without declaring the arguments, it is implicitly iterating over the array and returning the result (all features of array languages to make it more concise).
Python’s + and / are a function of two objects (dyadic) and are syntactic sugar (they can’t be treated as function values, thats what the operator module is for): https://docs.python.org/3/reference/datamodel.html#object.__...
The array languages optimize for concise code so a lot of things are done implicitly that are done explicitly in Python.
The python equivalent:
> _sum = lambda x : functools.reduce(operator.add, x)
> _count = lambda x: functools.reduce(lambda acc, _: acc + 1, x, 0)
Of course in python and R there are built ins for these already (sum(x) and len(x)) but this is just to show what array languages are doing for you.
Notice how they're _defining_ the "sum" operation there. Instead of being something builtin, they defined "sum" as {+/x}.
{+/x} is the interesting part. That's Klong. It's not being able to define a "sum" operation, it's that "sum" can be expressed as {+/x}. That's very different than both R and python.