Writing malloc wrong, for fun
jvns.ca
jvns.ca
Looking at the code I remember why I hate C. It's because of the lack of memory management, obviously, but also because of the crappy naming conventions. At a glance, disregarding your prior C programming experience, what is "mut"? What is "u8"? What is "putc"?
Why does a language designed in this day and age try to cater to old fogeys pining for C naming conventions if it wants to supercede C? We have consoles with widths >80 characters, we have autocompletion and even Intellisense in any decent programming editor know to man!
Compare and contrast:
putc with WriteCharacter or write_character or write_char
uint with UnsignedInteger or unsigned_integer or unsigned_int
Don't go overboard Java Enterprise edition-style, but stop saving on the "cheap" stuff, sheesh!
I'm also spoiled by Python at this, the epitome of "pseudocode" programming language. What's up with "~"? Couldn't they have found a 1-2-3-4 letter word for the random tilde?
I do understand why they did it - to make it easier to draw the C crowds in. But really, isn't there a lower level (no memory management/optional memory management) language with decent syntax? Why is everybody creating lower level languages trying to copy C? C was created in the age of teletypes and it became popular because it was portable assembly.
It's syntax was not a forte.
And, personally, `uint` is far better than `UnsignedInteger`, and `u8` is far far better than `Unsigned8BitInteger`, the relevant information is much easier to extract in the former (i.e. u[nsigned]8[bits], rather than trying read the number hidden within the text).
I'm not saying this in defence of cryptic shorthand but in defence of sane line lengths and margins.
Also, unsigned_int is not a great name. It's too long and it doesn't make sense--if it can't be negative, it doesn't make sense to think of it as an integer by analogy! I much prefer something like Haskell's Word types: Word, Word32, Word8 and so on. Short, easy on the eyes and clear.
Ultimately, really verbose names like UnsignedInteger and WriteCharacter just add noise to your line and obscure the structure of the code. While it's nice to be clear, chances are you will be seeing these words all over the place so you want them to be short and get out of your way. C goes too far in that direction with incredibly cryptic function names[1], but I think you go too far in the opposite.
Similarly, I think completely eschewing symbols is a bad idea. Symbols give your code structure that plain words lack. With a mix of symbols and names, code is neatly delimited between the parts that specify "stuff" (variable and function names) with the parts that specify "structure" or "operations" (control flow, auxiliary things like variable assignments and pointers and so on).
I think a good example here is bitwise operations. I think C's operators like <<, >>, &, ^ and | make for significantly more readable code than their named versions (shiftLeft, shiftRight, and, xor, or). Compare a simple snippet from Hacker's Delight:
(x & y) + ((x ^ y) >> 1)
(x and y) plus ((x xor y) shiftRight 1)
Sure, the first version is cryptic to someone unfamiliar with bitwise manipulations, but I think it's ultimately much clearer. If you're going to be doing more than a handful of bitwise operations in the future, the notation is well worth learning.Code is read more often than it's written, but it's skimmed even more often than it's read. With a healthy mix of symbolic notation and names, I can easily get the gist of code at a glance and mentally skip around. With just words in the Python style, I have to actually read each line of code--a serious drag on going through a codebase. I really don't want to have to read code unless I really have to!
Words may be clearer to the uninitiated--people without experience in the relevant language or libraries. But for common tasks, I feel this is really optimizing for the short term (learning) and against the long term (working with code). It's worth learning some special notation if it comes up often because that notation can be more succinct, convey information more efficiently and--the most important part, to me--can be easily read at a glance.
I certainly don't think C has a good syntax, for several reasons including the fact that it often goes too far in trying to be concise. But at the same time, I hardly believe in the traditions of Java which embraces verbosity and uniformity at the expense of clarity and expressiveness (just take a look at BigInteger!) or Python and Ruby which try too hard to read like English and end up forcing me to read too much.
[1]: Functions like: ioctl, atoi, fputc, strxfrm. Life gets even worse when you get into intrinsics! I wrote more about this in another HN post: https://news.ycombinator.com/item?id=6717209
80-90-100 characters lines are perfectly readable and also easily diffable/viewable side by side on almost any screen (I hope you're not doing 3-way merges merges on a phone :p).
And thankfully new lines don't cost anything so we can just break up long lines like in this very good example: http://stackoverflow.com/a/903767, and we can afford better identifiers.
C/C++ people - break the cycle! Make new, human readable, 2000+ style APIs for C. Death to the fputcs of the world!
(IMO symbol versus word is a personal preference thing and has no actual impact on code readability unless I need to re-read the language spec after 6 months of not using the language).
(x .&. y) + ((x `xor` y) `shiftR` 1) applykl =: 2 : '(((u&.>) @: (n&{)@])`(n"_)`] }) ^: ({. uy_vx e.) "_ 1'(Further historical note: the choice of ~ as the operator to create owned pointers is hardly "random": it's a line, and owned pointers are linear types. Contrast the obsolete @ operator for creating managed pointers, which is a circle because managed pointers were primarily used to facilitate circular references. The removal of @ means that this conceptual mapping is no longer evident, hence the transition to the `box` keyword.)
void* malloc(size_t sz) {
return rand() << 32 | rand();
}
Or in rustopia: fn extern "C" malloc(len: uint) -> *mut u8 {
unsafe {
std::rand::random::<uint>() as *mut u8;
}
}
Insert more so that it actually maps to memory.256 bit addresses, anyone? Well, maybe 1024... :-)
As the author points out, malloc is very easy if you don't worry about free'ing anything. :)
Tl;dr - it's worth it, IMO :)
static mut base: uint = 0x200000;
pub extern "C" fn malloc(len: uint) -> *mut u8 {
base as *mut u8
}
(I'll admit the second can be reduced less, since it's actually doing real work: basically just dropping the `: uint` on the `let ret`, dropping the `return` and associated `;`, and removing the () on the if condition.)I guess the major source of verbosity is the `extern "C"`, which, fortunately, is only necessary when one requires a function uses the C ABI/calling-convention; in the overwhelmingly common case one writes `pub fn foo(...)` which will use the Rust ABI (and it gets reduced to just `fn foo(...)` if the function can be private to a module).
let ret = base;
...was instead written as: let ret: uint = base;
...with all the additional verbosity that entails.