Why can't code in #:fallbacks refer to the generic methods?

Viewed 82

This code:

(require racket/generic)

;; A holder that assigns ids to the things it holds. Some callers want to know the
;; the id that was assigned when adding a thing to the holder, and others don't.
(define-generics holder
  (add-new-thing+id holder new-thing) ;returns: holder id  (two values)
  (add-new-thing holder new-thing)    ;returns: holder
  #:fallbacks
  [(define (add-new-thing holder new-thing)  ;probably same code for all holder structs
     (let-values ([(holder _) (add-new-thing+id holder new-thing)])
       holder))])

produces this error message:

add-new-thing+id: method not implemented in: (add-new-thing+id holder new-thing)

I'm able to fix it by adding a define/generic inside the fallbacks, like this:

  #:fallbacks
  [(define/generic add add-new-thing+id)
   (define (add-new-thing holder new-thing)
     (let-values ([(holder _) (add holder new-thing)])
       holder))])

but this seems to add complexity without adding value, and I don't understand why one works and the other doesn't.

As I understand #:fallbacks, the idea is that the generic definition can build methods out of the most primitive methods, so structs that implement the generic interface don't always need to reimplement the same big set of methods that usually just call the core methods with identical code—but they can override those "derived" methods if needed. say, for optimization. That's a very useful thing*—but have I misunderstood fallbacks?

It seems strange that fallbacks code couldn't refer to the generic methods. Isn't the main value of fallbacks to call them? The documentation for define/generic says that it's a syntax error to invoke it outside a #:methods clause in a struct definition, so I'm probably misusing it. Anyway, can someone explain what are the rules for code in a #:fallbacks clause? How are you supposed to write it?

The Clojure world has something similar, in the potemkin library's def-abstract-type and deftype+, but not as well integrated into the language. potemkin/def-map-type illustrates very nicely why fallbacks—as I understand them, anyway—are such a valuable feature.

1 Answers

The second version of your code is correct.

The first version of your code would work if you had a fallback definition of add-new-thing+id, but because you are referring to any possible definition of that method outside of the fallback scope, you need to import it.


It effectively feels a bit repetitive to have to define the generic again inside the fallback clause. It's because #:fallbacks works the same way as #:methods, and therefore has the same behavior of overriding generics with its own definitions.

To make it explicit that you are overriding a method, you need to "import" it inside your clause, using define/generic (which is not really defining anything, it is just importing the generic into the context).

As the documentation for define/generic says:

When used inside the method definitions associated with the #:methods keyword, binds local-id to the generic for method-id. This form is useful for method specializations to use generic methods (as opposed to the local specialization) on other values.

Then in define-generics:

The syntax of the fallback-impls is the same as the methods provided for the #:methods keyword for struct.

Which means #:fallbacks has the same behavior as using #:methods in a struct.

Why?

The logic behind that behavior is that method definition blocks, like #:methods and #:fallbacks have access to their own definitions of all the generics, so that it's easy to refer to your own context. To explicitly use a generic from outside this context, you need to use define/generic.

Related