lens
lens copied to clipboard
Needs to be easier to define nested lenses when using struct/lens a lot
This is some example code from oflatt/space-orbs:
(define game-orbs-enemys-lens (lens-thrush game-orbs-lens orbs-enemys-lens))
(define game-orbs-player-lens (lens-thrush game-orbs-lens orbs-player-lens))
(define game-orbs-player-pos-lens (lens-thrush game-orbs-player-lens orb-pos-lens))
(define game-orbs-player-time-lens (lens-thrush game-orbs-player-lens orb-time-lens))
(define game-orbs-player-shots-lens (lens-thrush game-orbs-player-lens orb-shots-lens))
(define game-orbs-player-deaths-lens (lens-thrush game-orbs-player-lens orb-deaths-lens))
(define game-orbs-player-kills-lens (lens-thrush game-orbs-player-lens orb-kills-lens))
This is part of the project's code for defining lenses for manipulating its state which consists of a lot of different structs, all defined with struct/lens. While struct-nested-lens is useful for defining a single lens into a nested group of structs, we currently have no easy way to eliminate the boilerplate of connecting lenses produced by struct/lens to nested structs automatically. Something like this might be useful:
(define-nested-lenses (top-id top-lens-expr)
[sub-id sub-lens-expr] ...)
Which defines one lens for each sub-id named {top-id}-{sub-id}-lens equal to (lens-thrush {top-lens-expr} {sub-lens-expr}. Used with the above example we get:
(define-nested-lenses (orbs-player orbs-player-lens)
[pos orb-pos-lens]
[time orb-time-lens]
[shots orb-shots-lens]
[deaths orb-deaths-lens]
[kills orb-kills-lens])
(define-nested-lenses (game-orbs game-orbs-lens)
[enemys orbs-enemys-lens]
[player orbs-player-lens]
[player-pos orbs-player-pos-lens]
[player-time orbs-player-time-lens]
[player-shots orbs-player-shots-lens]
[player-deaths orbs-player-deaths-lens]
[player-kills orbs-player-kills-lens])
I'm not sure this is better, but this is a pain point that needs addressing. Something more radical might even be a struct-tree/lens macro that defines several structs at once and lenses hooking them all up together.
A struct-tree/lens-like form could work if you could specify that certain fields of a struct should be instances of another struct, sort of like a type:
(struct/lens/nested position (x y) #:transparent)
;; position-x-lens and position-y-lens
(struct/lens/nested velocity (x y) #:transparent)
;; velocity-x-lens and velocity-y-lens
(struct/lens/nested player ([pos : position] [vel : velocity]) #:transparent)
;; player-pos-lens, player-vel-lens,
;; player-pos-x-lens, player-pos-y-lens,
;; player-vel-x-lens, and player-vel-y-lens
(struct/lens/nested world ([p1 : player] [p2 : player]) #:transparent)
;; world-p1-lens, world-p2-lens,
;; world-p1-pos-lens, world-p1-vel-lens, world-p2-pos-lens, world-p2-vel-lens,
;; world-p1-pos-x-lens, world-p1-pos-y-lens, world-p1-vel-x-lens, etc.
Then it has enough information to do that. It would have to store the information in a define-syntax binding, which the other struct/lens/nested forms would get via syntax-local-value when they see a clause like [p1 : player].
That would work, but unhygienically relies on assumptions about lens names which I don't like. Maybe we need something similar to struct-info instances for lenses so we can do this more safely? Some people may want to rename the lenses produced by struct/lens, @lexi-lambda mentioned wanting this for a macro that made structs where the accessors are applicable lenses.
No, the struct-info-like thing was what I was talking about. The struct/lens/nested form would define a syntax binding with a struct-info-like thing that would preserve hygiene just as much as struct would.
Should struct/lens define that as well? Then the "leaf" structs don't need to be defined with struct/lens/nested, or even struct/lens (as someone could define it with define-struct-lenses).
(struct position (x y) #:transparent)
(define-struct-lenses position)
(struct/lens velocity (x y) #:transparent)
(struct/lens/nested player ([pos : position] [vel : velocity]) #:transparent)
;; player-pos-lens, player-vel-lens,
;; player-pos-x-lens, player-pos-y-lens,
;; player-vel-x-lens, and player-vel-y-lens
So a primitive like (define-struct-nested-lenses player [pos position] [vel velocity]) could be what struct/lens/nested expands to
(struct/lens/nested player ([pos : position] [vel : velocity]) #:transparent)
=>
(struct player (pos vel) #:transparent)
(define-struct-lenses player)
(define-struct-nested-lenses player [pos position] [vel velocity])
This could now build off of #243 to handle the actual lens definitions. struct/lens and struct/lens/nested (thinking that struct/nested-lens might be a better name for consistency with *-lens and *-nested-lens functions) just need to make transformer bindings listing the lens ids, similar to how (struct foo (...) ...) creates a struct:foo binding.
Note: struct/nested-lens needs to create a separate binding holding nested lenses so two struct/nested-lens uses can hook up together properly.
Thought: I don't like that this adds another struct/* form. Maybe struct/lens should just support it on its own:
(struct/lens player ([posn position] health attack defense))
If we went this route, a keyword might be better syntax than just a bracketed pair:
(struct/lens player ([posn #:nested position] health attack defense))
What about a generalized define-nested-lenses that could work like this:
(define-nested-lenses [game-orbs game-orbs-lens]
[enemys orbs-enemys-lens]
[player orbs-player-lens
[pos orb-pos-lens]
[time orb-time-lens]
[shots orb-shots-lens]
[deaths orb-deaths-lens]
[kills orb-kills-lens]])
Which would expand to something like:
(define game-orbs-enemys-lens (lens-thrush game-orbs-lens orbs-enemys-lens))
(define game-orbs-player-lens (lens-thrush game-orbs-lens orbs-player-lens))
(define game-orbs-player-pos-lens (lens-thrush game-orbs-player-lens orb-pos-lens))
(define game-orbs-player-time-lens (lens-thrush game-orbs-player-lens orb-time-lens))
(define game-orbs-player-shots-lens (lens-thrush game-orbs-player-lens orb-shots-lens))
(define game-orbs-player-deaths-lens (lens-thrush game-orbs-player-lens orb-deaths-lens))
(define game-orbs-player-kills-lens (lens-thrush game-orbs-player-lens orb-kills-lens))
That brings the tree structure out a lot more.
That could also work, but I don't think we need it yet. The nested lenses created by struct/lens would only add one layer at a time, so it wouldn't use that feature. I'd like to get nested struct lenses working before generalizing.
I mean as an alternative to that.
(I'm starting to not like the modified struct/lens as much because it would require people to write structs in the opposite order to the way I and other people normally write them.)
I see what you mean. Unfortunately though it doesn't work quite as smoothly when a struct has multiple fields of the same type, for instance a game with two player fields, player1 and player2, would have to duplicate the nesting parts. Additionally the field names are repeated a lot.
This might work to solve the multiple fields of the same type issue:
(define-nested-lenses [game-orbs game-orbs-lens]
[enemys orbs-enemys-lens]
[(player1 player2) (orbs-player1-lens orbs-player2-lens)
[pos orb-pos-lens]
[time orb-time-lens]
[shots orb-shots-lens]
[deaths orb-deaths-lens]
[kills orb-kills-lens]])
But I'm not sure I like that syntax much, it seems kinda clunky and like the fields should be paired with their lenses.
I'm not sure that having to duplicate the nesting parts for two fields of the same type is all that bad. It would be good to be able to avoid the repetition, but in those cases I think it's still useful to show the tree structure.
@oflatt I wonder what you think of this?
This is really cool. I think all of these ideas are great, it is just a matter of the syntax. I think adding another struct/* form would be fine, or make it support it on it's own like in this example that Jack had.
(struct/lens player ([posn position] health attack defense))
It would make it easy and I think that is what both of you are going for, as well as making it clear that posn is a position, using a nice type-like system. Sometimes it is easy to forget what structures are nested where. As for the duplicate nesting parts for the two fields, I agree that it is not bad, but may become a problem if there are many duplicate fields. It would be good to include a way to avoid the duplicates if needed, similar or the same as the example above. Thank you for working on this and taking an interest in my project! This lens package is exactly what I have been looking for since I first started making little games.