The Kitten Programming Language
kittenlang.org
kittenlang.org
Quite fun and surprisingly readable to this old FORTH programmer. Though List<Char> for strings had me coughing up a fur ball.
Edit: it looks like as of 2018, the author was taking time off of working in tech (https://github.com/evincarofautumn/kitten/issues/201#issueco...). Maybe lack of activity means they got another job, or maybe they left the programming/tech world entirely.
Kitten Programming Language - https://news.ycombinator.com/item?id=17681202 - Aug 2018 (13 comments)
Kitten – A statically typed, stack-based functional programming language - https://news.ycombinator.com/item?id=13345832 - Jan 2017 (49 comments)
Kitten: a statically-typed stack-based functional programming language - https://news.ycombinator.com/item?id=11753145 - May 2016 (1 comment)
Kitten: compile to C, stack-based functional programming language - https://news.ycombinator.com/item?id=11506603 - April 2016 (3 comments)
Kitten - high-performance statically typed concatenative programming language - https://news.ycombinator.com/item?id=4993640 - Jan 2013 (1 comment)
Perhaps not the best choice. I believe this is frequently considered a design mistake in Haskell.
I think the two biggest reasons to not use a list of characters to represent a string are memory efficiency (not storing a linked list pointer per character) and memory locality (keeping the characters of the string near each other in memory since they are frequently used as a unit).
This also lets you implement compressed small strings (eg in tagged pointers) which is a good memory optimization.
As far as UTF-8 goes, it does add a whole layer of complexity that I had overlooked: although, I suspect intelligent data structure choices could mitigate the random access cost (something like an array of bytes plus an array of grapheme cluster offsets, or similar).
I personally have never found the memory locality concerns relevant to the vast majority of programs I write: when using a language like Python or Ruby or Haskell, you already have a lot of pointer chasing going on because of your non-string data structures such that the memory locality of strings is nearly irrelevant.
For the type of string processing code I find myself writing, linked lists are the wrong data structure because they tend to turn O(n) algorithms into O(n^2) ones: this can be mitigated by things like skip-lists, and is complicated by things like Unicode. But, personally, I tend to think that the biggest issue is not the performance impact of non-local storage of the string’s contents (which can be mitigated by techniques like cdr-coding). The problem is that string processing code, in my experience, tends to assume getting a predetermined offset into the string is relatively low cost.
I do think this was a mistake in Haskell, but also it's silly that Haskell and Lisp are based on lists which are a bad inefficient data structure. It's doubly silly that lazy languages like Haskell use linked lists (which are bad) as a metaphor to represent generator functions (which are good).
But why? It seems like having a `Digit` type, and having a `NatNum` type that's `List<Digit>`.
(OK, to nerdsnipe myself, that runs into trouble with leading 0's, but hopefully the point is clear. `0 | NonZeroDigit:List<Digit>` is maybe more accurate but less punchy.)
BigInt types are in fact more or less List<Digit>, normal integer types just aren't implemented like that for performance reasons.
A char is the primitive type that makes up a string if chained together. It's more or less a practical naming convention, just like we can call a tuple of floats a vector. Maybe you could get by without chars, but then String would be the only type in your language that doesn't consist of any scalar element but always has an arbitrary length in every situation.
But that's almost never what a language is giving you with their 'characters'.
Right. Because you only use BigInts, you don't use isolated BigInt digits.
> but then String would be the only type in your language that doesn't consist of any scalar element but always has an arbitrary length in every situation.
Sure, why not? Every type has something special about it. Integers have overflow, floats have precision, infinities, and NaN, and booleans don't map cleanly to bytes.
There's enough different ways to iterate over a string; you don't need a canonical way to decompose them, which is basically what a char type is. I wouldn't go as far as "mistake", but it's definitely fine to go without it.
> A char is the primitive type that makes up a string if chained together. It's more or less a practical naming convention, just like we can call a tuple of floats a vector.
For chars more complex than a byte, there are multiple practical conventions and it's hard to pick one.
For char=byte there's a very natural convention, and an array of bytes reflects what's actually in memory, but char=byte is something that should be avoided.
So it's either "none of these stand out" or "don't do this". So you might as well keep the language slightly simpler and not have a char type.
Internally it can be a list or array or whatever.
Traversal could yield Character objects, which would correspond to Unicode code points (the easy but less-good option, like in Python) or extended grapheme clusters (the actually useful option, like in Swift [0]).
It should also be easy to convert to/from a list or array of raw bytes.
[0]: https://docs.swift.org/swift-book/LanguageGuide/StringsAndCh...
Though possibly they just meant a string shouldn't be stored as a linked-list; I'm not familiar enough with Haskell to know whether List implies that underlying representation
> Primitive (unboxed) types cannot be defined in Haskell, and are therefore built into the language and compiler. Primitive types are always unlifted; that is, a value of a primitive type cannot be bottom. We use the convention (but it is only a convention) that primitive types, values, and operations have a # suffix (see Section 7.3.2, “The magic hash”). For some primitive types we have special syntax for literals, also described in the same section.
So with respect to 'primitives', Strings are not less special in Haskell-land than, say, Integers (which are arbitrary-size and assignable as literals).
Anyway, Strings as Lists of Chars perform poorly both memory-wise and speed-wise, which is why the linked-list implementation is generally considered a mistake.
If the string is stored in a contiguous array of memory, you can maintain a pointer to the start of the string and then do some pointer arithmetic to get char n. This is an O(1) operation.
If you store the string as a linked list of characters, then to access the nth character you have to follow a bunch of pointers until you get to that char. This is an O(n) operation, and it's going to take up more memory (4/8bytes per pointer, plus a byte for each character). Also it can make your memory fragmented. The upside is that you can grow and shrink the string. Adding chars to the front or end will be O(1). Adding chars in the middle will be O(n).
In the array case, if you want to modify the string you'd likely have to allocate a new block of memory for the larger string and copy all chars over. That will be an O(n) operation.
Null terminated strings are annoying because you don't know how big the string is without reading it. So getting the length of the string is an O(n) operation. The plus side is you don't have to carry around an extra byte(s) to hold the string length.
So it really depends on what you're doing with the string. Are you reading the string and accessing random characters often? Then an array is better. Are you mutating the string often? Then probably a linked list is better. Do you need to know how large your string is? Then keeping a byte around to indicate that is useful. But if you often read the entire string until the NULL character, then that extra memory dedicated to its length is wasted.
As for linked lists, I believe the times where they're more performant are fairly rare, and unlikely to occur at the level of a data structure for individual characters, which is reflected in String's poor performance in practice. Pretty much the only advantage you get from using string-as-a-list-of-chars is that you get to use List's Functor and Monad instances, but that probably should be avoided in any case.
That's the sort of thinking that gave us instant messaging applications which take gigabytes of memory and can saturate several year-old CPU cores trying to render some text...
That said, null termination + length is usually a good string representation. Instant length retrieval with the ability to ignore it when your algorithm needs to read to the end anyway.
This is the problem that language designers need to solve for.
Pretty sure that only works 100% when in pure ascii. In UTF there is the possibility of characters being more than 1/2/4 bytes.
1. Primarily, the Java String class stores an array of characters as instance data. The underlying format of arrays in Java is JVM dependent, but most JVMs store arrays (which are immutable) as a contiguous block of memory, with a 4-byte length field. This allows for very fast access for a number of different operations, at the cost of storing those 4 bytes for each array.
2. String instances themselves are also immutable, which has a number of very nice features.
Back in the day when C was created there was essentially a trade-off between storage (storing the length as an int value for each array) and ease of use. These days I think most people would argue that the teeny amount of storage used for Java arrays is mostly a moot point. Also, since Strings are immutable Java can do some optimizations like representing the same string value as just a single underlying instance even if multiple instances of that String are created in code.
Interestingly, code points beyond the BMP used to be quite uncommon (Unicode was originally designed for just 64k code points), but now emoji are in there, which of course are used a lot.
Cocoa/ObjC has the same problem - actually Java is based on Cocoa so that's probably where it came from.
For many cases, strings won't use any characters outside of UCS-2. In these cases, programmers gain the performance advantages of fixed sized characters without worrying about the downsides of surrogate pairs. It doesn't have the small memory footprint of UTF-8, but one could argue that if memory size is really important and fixed size characters are not, you're better off using in-memory compression for your strings, which will beat UTF-8.
If you are using characters outside of UCS-2, then Java's string methods are a potential source of confusion and bugs. There's always a trade-off, right?
https://dzone.com/articles/going-beyond-java-8-compact-strin...
I thought the JVM stopped doing that many years ago?
What you may be referring to is that interned Strings are stored in the head, while they used to be stored in the Permanent Generation:
See the commit for yourself! They removed the sharing between string values when you created a substring a while ago.
https://github.com/openjdk/jdk/commit/f55750d05a04484b719220...
I was referring to String interning, where literals defined in code are only created once even if they are duplicated, and the API provides String.intern() to get the "canonical" instance of a String.
The best internals (imho) is just a array (see, on rust):
https://doc.rust-lang.org/src/alloc/string.rs.html#278-280
This allow a lot of efficient operations and the possibility of use views (for substrings).
Where thing get nasty is all the unicode stuff. So even if the internal are simply, provide a good API that allow to use unicode correctly is what is more complicated.
I think here "proper implementation of strings" must take in account if is proper unicode, ascii or raw. Rust and other modern languages choose UTF-8 and I think this is the best posible option.
More accurately, there's an impedance mismatch with how many people use strings. Text is a better fit for them.
The Haskell implementation is not purely functional. It is however well tested, and wicked fast. While it is immensely appealing to view Haskell as a "there is a God!" abstraction, one does well to often consider and exploit what is happening through the looking glass.
One sees this for example using `type ShowS = String -> String` to concatenate many small strings to later output all at once. A project I've been meaning to complete is to translate a fast parser of mine to Text, to see if I can get it to run as fast. I'm sure the response to that blog post will be amusing.
It's amazing how many languages are designed to be simple and fast. You'd think that at some point the design would converge...
The author answers some questions.
> In addition, stack-based resource management has predictable runtime performance behavior: when you drop a value from the stack, it gets deallocated, no garbage collector required.
This cannot possibly work for this language. Stack-based memory management only works as long as all references go from "newer" values (higher on the stack) to "older" ones (deeper on the stack). But this language allows you to write a function that rearranges the stack, for example by swapping the two topmost elements.
Consider a scenario where some value x is on top of the stack:
x
Now we build some sort of heap value that has a reference to x and push it onto the stack: box(x)
x
Now swap these: x
box(x)
Then drop x from the stack, for example by printing it. This deallocates x. The stack is now: box(x)
But this value is undefined, since x has been deallocated. You have a use-after-free error.I wish people stopped approaching language design from the point of view that the people who have been working on garbage collection for the last 60 years are all idiots who don't see how simple it all is.
Looking at https://github.com/evincarofautumn/kitten/issues/193: "Does kitten require garbage collection?" -- "Nope. [...] The plan is that boxed lists ([…], List<T>) and closures ({…}) are reference-counted" and https://github.com/evincarofautumn/kitten/issues/131: "Explore GC strategies" -- "The C backend currently uses naïve reference counting."
A quick skim of kitten.c confirms that objects have a reference count field and are only really released when it drops to zero. (I didn't find code that increments the reference count, but I find it hard to care.)
The project's website claims:
> Automatic management of memory and resources with no garbage collector.
One can weakly argue that many people mean tracing garbage collection when they speak of garbage collection, and that reference counting is not garbage collection in this narrow sense. This is unhelpful at best.
The FAQ has the claim I quoted above, of which the "when you drop a value from the stack, it gets deallocated" appears to be simply false.
I should add that I'm aware that this project is incomplete, but that doesn't excuse making wildly implausible claims.
The language's developers themselves seem to agree that postfix syntax isn't always best: The syntax for the type "list of characters" is "List<Char>", which is not postfix. Compare ML-family languages, where this would use postfix syntax, writing this type as "char list".
That may be the case, but taking a core language feature and commenting "I don't like it, change it" isn't a very effective way to encourage discussion, in my opinion.
Anyway, here's a rough idea. Introduce some special token, like : for example, and add a special syntax rule in the parser that "a: b" is parsed as if it had been written "b a".
Then people who find it more readable can write
say: "hello"
while others can continue to write "hello" say
and surely everyone would be happy!We can have discussions about the convenience or readability of different syntaxes, but if the arguments are going to be of the level of "I don't like it", then a downvote is a valid response.
So it's always: data => process => process => process.
Left to right, no surprise, always the same thing first.
I kinda like the idea, although it's weird to get used to due to unfamiliarity.