A Problem
I am working on another post that explains the next step of our Tic-Tac-Toe journey, but first, a brief aside. As I was implementing logic for a Tic-Tac-Toe board (fun!) I began to run into an issue. My board actor had methods, but I needed to be able to call them from each other. Here’s a simple example:
(define (^board bcom)
;; Create a Guile array containing 'unspecified' items in a 3x3 grid
(define arr (make-array *unspecified* 3 3))
;; Go through each item in the grid and spawn a '^mark' actor for each.
(array-map! arr (lambda () (spawn ^mark)))
(methods
;; Get the coordinates from the array.
;; (we flip the x and y for aesthetic reasons but it doesn't matter much here)
[(ref coords)
(match coords ((x y) (array-ref arr y x)))]
;; Make a mark on the grid.
[(choose! coords mark-char)
(let ((mark (...))) ;; how do we call the `ref' method here, so we can get our mark actor?
($ mark 'choose! mark-char))]))
Our actor actually doesn’t know about itself. So we cannot make an invocation using <-
or $
because we have no instance of ourselves to pass to these functions. So… what do we do?
Awkward Workaround?
A workaround I thought of was to make use of our access to the scope, and create “proxy functions” that would do the real work, while our Goblins methods just pass on the responsibility to them. That would look like this:
(define (^board bcom)
(define arr (make-array *unspecified* 3 3))
(array-map! arr (lambda () (spawn ^mark)))
;; The logic has moved to these two defines.
(define (_ref coords)
(match coords ((x y) (array-ref arr y x))))
(define (_choose! coords mark-char)
(let ((mark (_ref coords))) ;; Ah, we can just call the define directly.
($ mark 'choose! mark-char)))
(methods
[(ref coords) (_ref coords)]
[(choose! coords mark-char) (_choose! coords mark-char)]
This works, and it’s okay. But it is clunky. Can we use Goblins' actor library to make this easier?
Hello, selfish-spawn!
The Racket version of Goblins' actor-lib has a neat little module with a function called selfish-spawn
. But it’s only in Racket for now. And we are using Guile. However, I tried my hand at porting the relevant Racket code to Guile, and it seems to work, so let’s pretend it is, and we can see how it works. We won’t actually go into how it’s implemented because it’s weird, Guile-specific, and unimportant for this! But if you are curious, check out the merge request.
All we have to do to use selfish-spawn
is to add a new argument self
to our actor, after bcom
. We can then use it in very much the same way one would use self
in Python to refer to ourself.
(define (^board bcom self) ;; we have added 'self' here!
(define arr (make-array *unspecified* 3 3))
(array-map! arr (lambda () (spawn ^mark)))
(methods
[(ref coords)
(match coords ((x y) (array-ref arr y x)))]
[(choose! coords mark-char)
;; Now, we can `$` the `ref` method on `self` just like any other actor!
(let ((mark ($ self 'ref coords)))
($ mark 'choose! mark-char))]))
The only other thing we have to do is use selfish-spawn
anywhere we would use spawn
to spawn our actor, and it does some magic to automatically inject itself, into itself! Which is pretty powerful! Here’s how we would invoke our choose!
method on a selfish board:
(define a-board (selfish-spawn ^board))
($ a-board 'choose! (0 1) "x")
Can we become selfish?
One drawback is that it would appear that we can’t use the handy bcom
function to become a selfish actor. However, if we start off selfish we can either become a different sort of selfish actor, or reform our ways and decide to no longer be selfish (by not passing in self
to bcom
).
Is there a solution to this? I don’t really know, actually. It might be useful to know, so if you know, give me a shout on Mastodon!
Conclusion
Selfish spawning is pretty neat, but you may want to use it sparingly. Instead of invoking methods on the same object, think about whether it would make more sense to divide your logic up into multiple actors. You can also nest actors if desired, which can be a powerful pattern in some cases. And if you want to invoke on a sub-object, that’s easy to do without using selfish-spawn.
Next up, we will go into more implementation details of this “selfish” Tic-Tac-Toe board, and how we can use it to play Tic-Tac-Toe in a decentralized way!