Iâve decided that I want to give #Racket another chance, and see if I can work out solutions to the problems that I ran into when trying to use it. itâs definitely the #Lisp that feels most practical to me out of everything that Iâve tried, and I still love the contract system and its specific take on threading macros
I remember that the error tracebacks were often missing important info, so I want to wait until that happens and see if I can figure out what the specific problem is/was
I was also trying to get some kind of static function signature checking, to make sure I was calling things right, but Iâve decided that Iâm okay with relying on runtime errors (from contract violations) instead, as long as I can read the error tracebacks easily enough
another problem that I ran into while using it is that it feels clumsy and awkward to work with complex data structures
with Python Iâm used to being able to nest objects pretty much arbitrarily deep, and have my LSP autocomplete all of the fields/methods at each step so I know exactly what type of data I have, and what I can do with it. with Racket that isnât really the case - I need to flip through my source code in another split to remember what data I have at each step and how to transform it. I donât think thereâs a solution for this - I think Iâll just have to deal with this unfortunately
another aspect of Racket handling complex data structures clumsily is just that the syntax for drilling into a data structure is pretty noisy and long:
(~> some-data-structure
(get-field foo _)
(hash-ref "some-key")
(list-ref 0)
some-struct$-field)
compare that to most other languages, which would let you just write:
some_data_structure.foo["some-key"][0].field
this problem gets so much worse when you need to actually mutate the data, and even worse than that when itâs immutable. hereâs a real function that I wrote in Racket: (itâs for a really basic clicker game)
(define/contract (game-state$-buy-autoclicker state ac-name)
(-> game-state$? string? game-state$?)
(define-struct-lenses game-state$)
(define-struct-lenses ac-slot$)
(define &ac
(lens-compose (&hash-ref ac-name) &game-state$-autoclickers))
(define ac-cost
(~> (&ac state)
ac-slot$-cost))
(printf "buying autoclicker ~a for cost ~a\n" (&ac state) ac-cost)
(define &ac-num-owned
(lens-compose &ac-slot$-num-owned &ac))
(define new-state
(~> state
(game-state$-clicks-update (-= ac-cost))
(lens-update &ac-num-owned _ (+= 1))))
(if (negative? (game-state$-clicks state))
(error "not enough clicks to buy this autoclicker")
new-state))
pretty much all itâs doing is this:
class GameState:
def buy_autoclicker(self, ac_name):
ac = self.autoclickers[ac_name]
cost = ac.get_cost()
print(f"buying autoclicker {ac} for cost {cost}")
if cost > self.clicks:
raise Exception("not enough clicks to buy this autoclicker")
ac.num_owned += 1
self.clicks -= cost
but the combination of nested data and immutability make it a huge mess in Racket
Iâm definitely open to suggestions for how I can make this type of code shorter and easier to read, but for the moment hereâs what Iâm going to try:
Iâm going to stop using structs completely in favor of classes, because classes are a great way to have mutability (which fixes the immutability problem) which is neatly contained in a way that can be easily unit-tested, and the syntax and semantics for dealing with classes are often shorter, simpler, and more consistent too
Iâm also going to see if I can make a series of general-purpose âget/set/update the data in this nested data structureâ forms:
(get-in some-data '(0 3 field-name "some-key"))
; returns some_data[0][3].field_name["some-key"]
(set-in some-data '(0 3 field-name "some-key") "new-value")
; returns a version of some_data with the specific data changed
; etc.:
(set!-in some-data '(0 3 field-name "some-key") "new-value")
(update-in some-data '(0 3 field-name "some-key") (+= 1))
(update!-in some-data '(0 3 field-name "some-key") (+= 1))
that way, my very first example would look like this instead:
(get-in some-data-structure '(foo "some-key" 0 field))
and I could refactor my function into a method that looks like this:
(define (buy-autoclicker ac-name)
(define ac (hash-ref autoclickers ac-name))
(define cost (send ac get-cost))
(printf "buying autoclicker ~a for cost ~a\n" ac cost)
(when (> cost clicks)
(error "not enough clicks to buy this autoclicker"))
(set! clicks (- clicks cost))
(update-field! num-owned ac add1))
thatâs dramatically shorter and nicer