To illustrate, here's a little immutable struct and a function to update it:
(struct timeseries (variable observations) #:transparent)
(define (add-observation ts t v)
(struct-copy timeseries ts
[observations (conj (timeseries-observations ts) `(,t ,v))]))
My question is: If I make a struct that inherits from timeseries
, then add-observation
will return a timeseries
struct rather than a struct of the type that it was passed. How do you update a struct and retain its type?
By the way, if the above code is just not how things are done in Racket, please let me know the conventional way. The fact that I haven't found a function in the Racket libraries like struct-copy
but that retains the type of the original struct makes me suspect that I'm going about this the wrong way. Is there some ordinary way to accomplish the same purpose without encountering the problem of returning a struct of a different type than you started with?
Unfortunately this is one of struct-copy
's well-known limitations, most of which stem from it being implemented by what Sam Tobin-Hochstadt has aptly described as "unhygenically pasting bits of structs together" (rather than a low-level notion of copying structs), and is part of the reason that "struct-copy
is hopeless and can't be fixed without major changes to how structs work." Matthias Felleisen described this as "an Achilles' heel in our world." There is definitely a desire in the Racket community to improve this situation, but for a number of reasons it seems daunting. I'm not aware of anyone actively working on it, and what a principled solution would look like seems to be an open question.
Structs are in many ways very fundamental to Racket. Conceptually, every value in Racket could be an instance of some struct type, though in reality the runtime system has specialized representations for certain built-ins. In fact, I think the ongoing work of replacing C with Chez Scheme in the Racket implementation may use structs for some things that are built-ins in the legacy Racket VM. This is possible because structs offer strong encapsulation capabilities, especially through inspectors. Improving the way structs work would touch essentially all of Racket and involve many disparate considerations, especially around backwards-compatibility.
Here are a few pointers for further reading about the issues:
struct
could start providing more static information while preserving backwards compatibility.This thread points out a different limitation of struct-copy
and includes a good summary from Alexis:
…
struct-copy
is irreparably broken and cannot be fixed without fundamental changes to Racket’s struct system. Namely, it has the “slicing” problem familiar to C++ programmers when using struct inheritance, and the way it synthesizes field accessors from the provided field names is unhygienic and can be easily thwarted.
The good news is that, while figuring out The Right Thing to do the general case is hard, Racket's languages-as-libraries approach makes it possible for all programmers and library-writers to try different approaches in their own code. There are various Racket packages to help with functional update and other features. Alexis' struct-update
provides a macro to synthesize functions like timeseries-observations-update
. Jay McCarthy has also experimented with enhancements to struct in library code. You can also implement a solution tailored to your specific use-case, ranging from implementing a consistent copy
method (with racket/generic
or racket/class
) to creating a domain-specific language that can more naturally express your problem domain. This mailing-list thread, despite the subject line, covers a lot of approaches to functional update in Racket (including some thoughts from me about DSLs).