Conversation
Edited 4 days ago

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

1
0
5

short little update but I was intimidated by Racket’s macro system since it seems very magical and complex, so I hadn’t touched it at all until this point

but out of necessity I made some extremely basic macros in it using define-syntax-rule and you know what? it’s actually super easy. it’s definitely still magic (which I think is a bit of a travesty when Lisp makes macros so simple and intuitive compared to any other language) but it’s simple magic that’s easy to use

also, so far the struct -> OOP refactor is going really well! my code is much smaller, simpler, and easier to read already - though I haven’t completed the full refactor yet

0
1
2