first of all, racket is probably my favorite language ever. it's by far the easiest and cutest language to write anything in, but particularly excels at being a scheme that's as batteries included as a scheme could possibly be, which makes writing algorithms really fast and easy (especially compared to verbose Business Languages like Javaβ’). racket is the first language on my resume, and my go-to language when program speed isn't the top concern
(on the other hand i'm an Imposter Racket Programmer because i still suck at writing macros. i'm sorry. i know it's the whole point of racket. i get by without needing a lot of custom macros most of the time)
i'm not really going to touch on the obvious issues with racket. yes, it's an interpreted language, which means it can be quite slow compared to systems languages, and it tends to use a lot of RAM since everything is heap allocated. but in the world of interpreted languages it's not actually the worst, and i'm hoping that the port to chez scheme improves the performance situation a bit (it's planned to be the default implementation next release!!). this is mostly to complain about one thing but i will add some other stuff as well
preemptive green threads
this is my top complaint. i don't like it and i wish racket had been designed with preemption being optional
racket execution is single-OS-threaded -- in order to use multiple OS threads you create places and communicate between them using place channels, because places aren't allowed to access each other's memory. within places, threads are racket execution sequences that the racket interpreter switches between in the context of a single OS thread, for example when a thread sleeps or is waiting on I/O. this enables effective async programming in racket, where you can have multiple racket threads performing I/O and the racket interpreter will use OS-level polling (select
, poll
, epoll
, kqueue
, perhaps some day racket will have io_uring
because that would be cool,)
so, fearless concurrency! this eliminates the standard kind of race conditions in mutable shared memory right?
no, it doesn't: https://gist.github.com/iitalics/8bf2aa8e62b03045ad023287f9a7f6b2
by spec, racket green threads are preemptive, which means the interpreter is allowed to arbitrarily switch between them at any time. so racket took the main advantage of not allowing shared memory between OS threads and threw it away because shared memory between racket threads is actually also unsafe
in practice (with 3m racket) it turns out it's really hard to actually make a PoC for this because the optimizer will screw you over and turn your fearlessly unconcurrent code into fearlessly concurrent code for you (!!) but you can do it (thanks to iitalics for being smart enough to actually make a fairly small PoC). so while it's probably rare to see a race condition like this show up in your own production code, especially if you minimize having unnecessary mutable data, it can theoretically happen at any time to anything, just like if you were actually using OS threads, and to put it bluntly this annoys the heck out of me
racket provides an atomic mode that suspends thread switching, but it suspends all thread switching, even the kind you actually want. there doesn't seem to be a mode that suspends only preemption, which makes me sad
fundamentally it seems like one of the design goals of racket is to avoid pulling a sneaky computers are bad moment on you at all costs. that's why, for example, numbers are arbitrary precision rationals by default1. so it seems really weird that racket also explicitly re-introduces fearful concurrency into the language
you could argue that the ways around this issue encourage better coding practices, which may be true. one solution if you need shared access to mutable data is to serialize mutation operations over a thread mailbox which i suppose is decent
lack of concurrency primitives
specifically, a single-producer/multi-consumer event (think asyncio.Event
) and multi-producer/multi-consumer queue in the stdlib would be really useful. you can manually implement these but it's annoying that like, stuff that python provides standard in asyncio
isn't present in racket. there is a full list of everything in racket that is a "synchronizable event" (you can have a green thread wait on the type until a condition is met, or of multiple events at the same time using sync
)
in general coding concurrency stuff is a bit obtuse. for example, this is my solution to "wait for event with timeout" -- sync
has no builtin timeout so instead you need to provide an additional alarm event and check if that ended up being the result of sync, indicating the timeout was hit
;; get thread mail event
(define recv-evt (thread-receive-evt))
;; create a wakeup event if wakeup is needed sometime
(define wakeup-evt (if (>= wakeup now)
(alarm-evt (* 1000 wakeup))
always-evt))
;; wait for either a message or a wakeup
;; if both events are ready, one will be pseudorandomly chosen so it should be fair
(define sync-result (sync recv-evt wakeup-evt))
(cond
[(equal? sync-result wakeup-evt)
;; run dequeue
(let dequeue-loop ()
(when (try-dequeue)
(dequeue-loop)))
(loop)]
[else
;; handle message
(match-define (qmsg from type data) (thread-receive))
.......
format
ok so, format
sucks and there are no good facilities in racket for generic string formatting of different data types
format
is along the lines of C's snprintf
except there are exactly 3 specifiers, ~a
, ~s
, and ~v
which accept any data type and output it in different representation forms (approximately raw, human-readable, and racket-source-readable if i'm understanding correctly). and, that's entirely it,
there's no facility for the number of digits a real should print with, for example. you'd have to add a call to real->decimal-string
which is a very long and very obtuse name. there's no support for padding/alignment. that would also have to be manual. and of course, fancy string interpolation like any modern language is out the window because who needs that, right??
the result of this is that any sort of nontrivial string formatting with just the stdlib is really painful to implement. i don't understand why good formatting seems to be entirely missing from the stdlib because it seems like an important thing to have for a very batteries-included language
regex escapes
racket defines a special reader mode for regex, which goes eg #px"content of regex..."
despite having a special reader mode specifically for this regex string you still have to double-backslash in the string
#px"abcd\\efg" ;; matches abcd\efg
this seems unnecessary. the reader mode should have been built to parse the regex as a raw string, like python's r-string mode, given that there is already a custom reader mode specifically for regex.
more stuff !
additional list of (more minor) racket "wats": https://git.lain.faith/iitalics/racket-wat
fin π¦
i guess no language can really be completely perfect, or something
racket is still very good and very cute imo
some have argued this is a bad thing. "i don't like it when simply adding 2 numbers explodes into gigantic complexity for no reason2"
i actually found out when writing this post that as of last year there is now officially an n log n algorithm for integer multiplication which is COOL AS HECK AAAAAAAAAAAAAA
Comments
September 21, 2020 12:53
@haskal Thank you!
Big Racket fan here too. I wasn't aware that green threads are pre-emptive and kind of assumed they weren't. On Fractalide we've been using async channels for communicating between threads and that has been enough for us to avoid any issues.
I have been trying to use futures for parallel processing, but there's just too many things that may bring them back to serial execution. Places suffer from limitations in what data you can put on the channel.
From what I've read it seems Gerbil is the only Scheme with OS threads in the sense that most people would think of them, but I haven't tried it yet.
Will check out your wat list as well!