structracket

How do you get struct-copy to create a struct of the same type as the original?


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?


Solution

  • 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:

    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).