The point of Whitney's array languages is to allow your solutions to be so small that they fit in your head. Key chunks should even fit on one screen. A few years ago, Whitney reportedly started building an OS using these ideas (https://aplwiki.com/wiki/KOS).
I'm aware of the idea. I'm also aware that I can read and understand a whole lot of pages of code in the amount of time it takes me to decipher a few lines of K for example, and the less dense codes sticks far better in my head.
I appreciate brevity, but I feel there's a fundamental disconnect between people who want to carefully read code symbol by symbol, who often seem to love languages like J or K, or at least he better able to fully appreciate them, and people like me who want to skim code and look at the shape of it (literally; I remember code best by its layout and often navigate code by appearance without reading it at all, and so dense dumps of symbols are a nightmare to me)
I sometimes think it reflects a difference between people who prefer maths vs languages. I'm not suggesting one is better than the other, but I do believe the former is a smaller group than the latter.
For my part I want grammar that makes for a light, casual read, not having to decipher. I want to be able to get a rough understanding with a glance, and gradually fill in details, not read things start to finish (yes, I'm impatient)
A favourite example of mije is the infamous J interpreter fragment, where I'd frankly be inclined to prefer a disassembly over the source code. But I also find the ability to sketch out such compact code amazing.
I think Wirths designs very much fit in the languages that are skimmable and recognisable by shape and structure category. I can remember parts of several of Wirths students PhD theses from the 1990s by the shape of procedures in their Oberon code to this day.
That's not to diminish Whitney's work, and I find that disconnect in how we process code endlessly fascinating, and regularly look at languages in that family because there is absolutely a lot to learn from them, but they fit very different personalities and learning styles.
I sometimes think it reflects a difference between people who prefer maths vs languages. I'm not suggesting one is better than the other, but I do believe the former is a smaller group than the latter.
This dichotomy exists in mathematics as well. Some mathematicians prefer to flood the page with symbols. Others prefer to use English words as much as possible and sprinkle equations here and there (on their own line) between paragraphs of text.
The worst are those that love symbols and paragraphs, writing these dense walls of symbols and text intermixed. I’ve had a few professors who write like that and it’s such a chore to parse through.
i keep hoping that one day i'll understand j or k well enough that it won't take me hours to decipher a few lines of it; but today i am less optimistic about this, because earlier tonight, i had a hard time figuring out what these array-oriented lines of code did in order to explain them to someone else
textb = 'What hath the Flying Spaghetti Monster wrought?'
bits = (right_shift.outer(array([ord(c) for c in textb]),
arange(8))).ravel() & 1
and i wrote them myself three months ago, with reasonably descriptive variable names, in a language i know well, with a library i've been using in some form for over 20 years, and their output was displayed immediately below, in https://nbviewer.org/url/canonical.org/~kragen/sw/dev3/rando...
i had every advantage you could conceivably have! but i still guessed wrong at first and had to correct myself after several seconds of examination
i suspect that in j or k this would be something like (,(@textb)*.$i.8)&1 though i don't know the actual symbols. perhaps that additional brevity would have helped. but i suspect that, if anything, it would have made it worse
by contrast, i suspect that i would have not had the same trouble with this
bits = [(ord(c) >> i) & 1 for c in textb for i in range(8)]
however, as with rpn, i suspect that j or k syntax is superior for typing when what you're immediately evaluating expressions rather than writing a program to maintain later, because the amount of finger typing is so much less. but maybe i just have a hard time with point-free style? or maybe, like you say, it's different types of people. or maybe i just haven't spent nearly enough time writing array code during those years
> If you go to the link and press 'help' you'll see some docs for ngn/k
Almost as impenetrable as the code unless you already know the language. But that's ok - I guess that's the main audience...
E.g. trying to figure out what "\" means, in that help is only easier now because you gave me the whole line, as there are 64 occurrences of "\" in that doc and I wouldn't have known what pattern to search for to limit it...
It's back to the philosophical disconnect of expecting people to read start to finish/know far more detail inside out rather than skimming and relying on easy keyword lookups... (yes, we're lazy)
> 'reduce with concat'
So "flatten" in Ruby-speak, I take it (though "flatten" without an argument in Ruby will do this recursively, so I guess probably flatten(1) would be a more direct match).
> you don't need to do any mapping, it's automatic.
After these pointers (thanks!), here's - mostly for my own learning, what I ended up with not in an attempt to get closer to the linenoise (we can do that with a horrific level of operator overloading that'd break most of the standard library, though we can't match k precisely). Please don't feel obliged to go through this unless you're morbidly curious; I just had to, but I'm sure you'd suffer going through my attempt at figuring out what the hell k is doing...:
textb="What hath the Flying Spaghetti Monster wrought?"
# Firstly, I finally realised after a bunch of testing that 1) "(8#2)" does something like this.
# That is, the result of 8#2 is (2 2 2 2 2 2 2 2), which was totally not what I expected.
def reshape(len,items) = [].fill(0...x)
class Integer
# For the special case of (x#y) where x is a positive integer, which is frankly the only one
# I've looked at, we can do:
# So now 4.reshape(2) returns [2 2 2 2] just like (4#2) in ngn/k
def reshape(items) = Array(items)*self
# Now we can do something somewhat like what I think "encode" is
# actually doing - this can be golfed down, but anyway:
# With this, "a".ord.encode(8.reshape(2)) returns [0,1,1,0,0,0,0,1],
# equivalent to (8#2)\ "a" in ngn\k
def encode(shape)
rem = self
Array(shape).reverse.map do |v|
val = rem % v
rem = rem / v
val
end.reverse
end
end
# Now we can break Array too.
class Array
# First a minor concession to how Ruby methods even on the Array
# class sees the focal point as the Array rather than the elements.
# E.g. `self` in #map is the Array. If the focus is to be on applying the
# same operation to each element, then it might be more convenient
# if `self` was the element. With this, we can do ary.amap{reverse}
# instead of ary.map{|e| e.reverse} or ary.map{ _1.reverse}.
# To get closer to k, we'd have needed a postfix operator that we could
# override to take a block, but unfortunately there are no overridable
# postfix operators in Ruby. E.g. we can hackily make
# ary.>>(some_dummy_value) {a block} work, but not even
# ary >> (some_dummy_value) { a block} and certainly not
# ary >> { a block }
#
def amap(&block) = map { _1.instance_eval(&block) }
# If we could do a "nice" operator based map, we'd just have left it
# at that. But to smooth over the lack of one, we can forward some
# methods to amap:
def encode(...) = amap{encode(...)}
# ... with the caveat that I realised afterwards that this is almost certainly
# horribly wrong, in that I think the k "encode" applies each step of the
# above to each element of the array and returns a list of *columns*.
# I haven't tried to replicate that, as it breaks my mind to think about
# operating on it that way. That is, [65,70].encode(2.reshape(10))
# really ought to return [[6,7],[5,0]] to match the k, but it returns
# [[6,5],[7,0]]. Maybe the k result will make more sense to me if I
# take a look at how encode is implemented...
def mreverse = amap{reverse}
end
# Now we can finally get back to the original, with the caveat that due to
# the encode() difference, the "mreverse.flatten(1)" step is in actuality
# working quite differently, in that for starters it's not transposing the arrays.
#
p textb.bytes.encode(8.reshape(2)).mreverse.flatten(1)
# So to sum up:
#
# textb -> textb.bytes since strings and byte arrays are distinct in Ruby
# (8#2) -> 8.reshape(2)
# x\y -> y.encode(x) ... but transposed.
# |x -> x.mreverse
# ,/+x -> x.flatten(1) .. but really should be x.transpose.flatten(1)
#
# Of course with a hell of a lot of type combinations and other cases the k
# verbs supports that I haven't tried to copy.
i can't decode it either but i think you're right. note that this probably gives the bits in big-endian order rather than little-endian order like my original, but for my purposes in that notebook either one would be fine as long as i'm consistent encoding and decoding
The big caveat being it clicked too late that 1) "encode" is not "change base and format", but "for each element in this array, apply the module to the entire other array, and pass the quotient forward", and 2) encode returns a list of columns of the remainders rather than rows (the output format really does not make this clear...).
So you can turn a list of seconds into hour, minute, seconds with e.g.: (24 60 60)\(86399 0 60), but what you get out is [hour, minute, second] where hour, minute, second each are arrays.
If you want them in the kind of format that doesn't break the minds of non-array-thinking people like us because the order actually matches the input, you'd then transpose them
by prepending "+", because why not overload unary plus to change the structure of
the data?
+(24 60 60)\(86399 0 60)
Returns
(23 59 59
0 0 0
0 1 0)
Or [[23,59,59], [0,0,0], [0,1,0]] in a saner output format that makes it clear to casuals like me which structure is contained in what.
Now, if you then want to also flatten them, you prepend ",/"
I feel much better now. Until the next time I spend hours figuring out a single line of k.
I think the K would likely be both simpler and harder than your first example by reading very straightforwardly in a single direction but with operators reading like line noise. In your case, my Numpy is rusty, but I think this is the Ruby equivalent of what you were doing?
textb = 'What hath the Flying Spaghetti Monster wrought?'
p textb.bytes.product((0...8).to_a).map{_1>>_2}.map{_1 & 1}
Or with some abominable monkey patching:
class Array
def outer(r) = product(r.to_a)
def right_shift = map{_1>>_2}
end
p textb.bytes.outer(0...8).right_shift.map{_1 & 1}
I think this latter is likely to be a closer match to what you'd expect in an array language in terms of being able to read in a single direction and having a richer set of operations. We could take it one step further and break the built in Array#&:
class Array
def &(r) = map{_1 & r}
end
p textb.bytes.outer(0...8).right_shift & 1
Which is to say that I don't think the operator-style line-noise nature of K is what gives it its power. Rather that it has a standard library that is fashioned around this specific set of array operations. With Ruby at least, I think you can bend it towards the same Array nature-ish. E.g. a step up from the above that at least contains the operator overloading and instead coerces into a custom class:
textb = 'What hath the Flying Spaghetti Monster wrought?'
class Object
def k = KArray[*self.to_a]
end
class String
def k = bytes.k
end
class KArray < Array
def outer(r) = product(r.to_a).k
def right_shift = map{_1>>_2}.k
def &(r) = map{_1 & r}.k
end
p textb.k.outer(0...8).right_shift & 1
With some care, I think you could probably replicate a fair amount of K's "verbs" and "adverbs" (I so hate their naming) in a way that'd still be very concise but not line-noise concise.
that all seems correct; the issue i had was not that python is less flexible than ruby (though it is!) but that it required a lot of mental effort to map back from the set of point-free array operations to my original intent. this makes me think that my trouble with j and k is not the syntax at all. but conceivably if i study the apl idiom list or something i could get better at that kind of thinking?
I think you could twist Python into getting something similarly concise one way or other ;) It might not be the Python way, though. I agree it often is painful to map. I think in particular the issue for me is visualizing the effects once you're working with a multi-dimensional set of arrays. E.g. I know what outer/product does logically, but I have to think through the effects in a way I don't need to do with a straightforward linear map(). I think I'd have been more likely to have ended up with something like this if I started from scratch even if it's not as elegant.
p textb.bytes.map{|b| (0...8).map{|i| (b>>i) & 1} }.flatten
EDIT: This is kind of embarrassing, but we can of course do just this:
textb.bytes.flat_map{_1.digits(2)}
But I think the general discussion still applies, and it's quite interesting how many twists and turns it took to arrive at that