Of course this is just my opinion, I also may have forgotten something I do sometimes, just can't think of right now.
1. short(/mathematical) functions
I usually try to keep my functions extremely short (but that isn't always practical), while having most of my hints through naming that aid in code readability in the function names rather than local variable names. So in these very short functions I treat variables more like short math variables like r1
, r2
, a
, b
, c
, etc..
Although those short variables names sometimes have a meaning too, here is an example:
(define (part:cuboid/pos scale)
(define (s% base-vector)
(vec3-non-uniform-scale base-vector
scale))
(define +x (s% (pos 1 0 0)))
(define +y (s% (pos 0 1 0)))
(define +z (s% (pos 0 0 1)))
(define o (pos 0 0 0))
(define m (pos-add +x (pos-add +y +z)))
(define ox (edge o +x))
(define oy (edge o +y))
(define oz (edge o +z))
(define mx (edge m
(pos-add +x +y)))
(define my (edge m
(pos-add +y +z)))
(define mz (edge m
(pos-add +z +x)))
(define oxy (face ox oy))
(define oxz (face oz ox))
(define oyz (face oy oz))
(define mxy (face my mx))
(define mxz (face mx mz))
(define myz (face mz my))
(list
(face-coords oxy)
(face-coords oxz)
(face-coords oyz)
(face-coords mxy)
(face-coords mxz)
(face-coords myz)))
This function creates non-optimized geometry for a cube/cuboid (it gets converted to opengl buffers somewhere else) this is just for illustration.
+x
is an an axis. o
is the origin. m
either I don't remember, but I think I didn't come up with a meaning (m is basically the point at the other end of the main diagonal).
ox
mx
etc. define edges from o
to x
etc..
oxy
is then the face that is defined by the two vectors ox
and oy
sharing the common point o
defining a triangle that is extended to a rhomboid which is used as a opengl quad.
So sometimes my variable names have a domain specific logic to them and sometimes they are just short identifiers used similar to math variables.
2. mostly minimal scopes / internal functions
When my function is less short/mathematical I sometimes write functions with internal functions,
this has the benefit that it turns a long function into lots of little functions (where each can be given a descriptive name) turning a long scope that accumulates more and more names, into n short scopes and it also makes the steps/groups of operations explicit.
Also this allows you to use closures to avoid repetition where it doesn't make things more understandable, for example %s
above makes it way more readable and skip-able than having its body 3 times in my opinion.
Why internal functions?
I find it makes code more readable when all scopes are pretty close to being as short as possible, if an internal function is only used by the surrounding function, why make it accessible to other functions?
Only makes the code harder to edit later because you don't know whether somebody else uses it (in the module, or have to check whether its provided).
(If your module only does one thing and is short it may be fine to use "sibling-functions" rather than "child-functions", because then the module provides clarity)
3. variable names describing data
I think 1. and 2. above are my priority, but somewhat orthogonal to them I also try to come up with short names that try to convey use/meaning/domain-knowledge (for less mathematical code) rather than naming what the function does that produced the value for that variable.
For example if I am processing lists I could do this (define first-result (first lst))
but this doesn't tell me anything new, instead I may write (define f (first lst)
which still isn't better but at least more concise and kind of forces me to use short scopes because else it quickly becomes a mess.
What I actually want to write instead is (define user-name (first lst))
or something similar, that tells the reader what kind of data is expected.
So instead of having a name clash with (define remainder (remainder ... 60))
use (define minutes (remainder p1 60))
Example mixing mathematical and named:
(define-values (p1 seconds) (quotient/remainder p0 60))
(define-values (p2 minutes) (quotient/remainder p1 60))
(define-values (p3 hours) (quotient/remainder p2 60))
4. bringing the language closer
Switching from language user to library author / language designer.
I think in languages like python this is often restricted to questions like "How can I use (and abuse) the existing syntax and semantics to somehow represent my problem in a more straight forward way".
But in racket we have more flexibility there.
The example above is basically my function local naming convention that may be bad in other situations. Instead of it you also could think, maybe I could create some kind of abstract geometry algebra, implement that as a macro and then write some other description, that in the end generates the same or similar data. (I didn't do that because I currently have reasons to focus my time on other stuff and stay more low level)
example
But here is an example of a utility macro I use that turns a function local convention into something more formal, generating what would be repetitive:
(define (matrix4-from-rotation-axes x y z)
(define-attributes (x y z) vec3- (x y z))
(matrix4 xx yx zx 0
xy yy zy 0
xz yz zz 0
0 0 0 1))
Here define-attributes
is a custom macro that gets a set of inputs (x y z)
a prefix vec3-
and a set of suffixes (in this case also (x y z)
) it then generates local bindings (via define-values). It also has optional renaming not shown here.
This expands to something like this:
(define-values (xx xy ... zz) (values (vec3-x x) (vec3-y x) ... (vec3-z z)))
Basically it creates all the permutations of the inputs and the suffixes. (If somebody is interested in this macro I may create another post with it or even a package, but I might need to add syntax/loc
for proper source locations.)