screwlisp proposes kittens

Lispy gopher rfc1436 without tcp

| link | Last. 20260216T084334179Z |

Reading someodd’s manifesto convinced me that like how Alan Kay considers lisp a property of this universe the same way Maxwell’s equations are, gopher protocol is a similar platonic solid of the rules our universe has for internetworked communication. If there are aliens, they program in lisp and phlog on the gopher.

Please accept this lispy gopher instead of my promised complicated neural network and condition distributino ones for the time being!

CL-USER> (dolist (s (list 'foo-specify 'dir-specify))
  (handler-bind ((retrieve #'handle-directory)
		 (retrieve #'handle-file)
		 (t #'error))
    (handler-bind
	((got-request-for #'handle-got-request))
      (as-gopher () () (signal 'got-request-for
			       :item-specifier
			       s))))
  (terpri) (terpri))
BAR

0FOO-USER	FOO-SPECIFY	NIL	0
1DIR-USER	DIR-SPECIFY	NIL	0
.

NIL

On the other hand, I do not think TCP is a fundamental property of this universe, like how C++ and rust are clearly not fundamental properties of this universe in the same way.

I have implied (well, someodd.zip has) that gopher is a fundamental general view on communication. Taking this view I would like to view gopher as a general, run-time specified view on communication like lisp is for computing.

Macro that provides file and directory gopher itemtypes

RFC1436 encourages adding itemtypes to an implementation gradually, with these two being the fundamental starting ones so I cooked their restarts into an as-gopher macro.

Here I am using file and directory in the RFC1436 sense of textual thing and gophermap.

(defmacro
    as-gopher
    ((&optional
	(output-stream *standard-output*))
     (&body more-restarts)
     &body body)
  `(restart-case
       (progn ,@body)
     (file
	 (thing)
       (princ thing ,output-stream))
     (directory
	 (direntity-list)
       (loop
	 :for r :in direntity-list :do
	   (format ,output-stream
		   "~:[.~;~{~c~@{~a~^	~}~}~%~]"
		   r r)
	 :finally (princ ".")))
     ,@more-restarts))

Two conditions for requests and retrievals

(define-condition
      got-request-for
    ()
  ((item-specifier :initarg :item-specifier
		   :reader item-specifier)))

(define-condition
      retrieve
    ()
  ((item :initarg :item
	 :reader item)))

An item alist and handlers

So that we have a thing that we are talking about, I decided looking up a request would be an assoc in an association list, and that a gophermap (that is directory itemtype 1) would be a :gophermap tagged list, and anything else would fall back to a file itemtype 0 (show as text).

It then seems natural for the gophermap to be a simple filter of the alist itself.

(setq *print-circle* t)

(defparameter *specifier.items*
  '#1=(((#\0 foo-user foo-specify nil 0) . bar)
       ((#\1 dir-user dir-specify nil 0) :gophermap #1#)))

(defun handle-got-request (c)
  (unless
      (and (typep c 'got-request-for)
	   (slot-boundp c 'item-specifier))
    (return-from handle-got-request))

  (let ((item (cdr (assoc (item-specifier c)
			  *specifier.items*
			  :key 'caddr))))
    (signal 'retrieve
	    :item (cond ((and (listp item)
			      (eq :gophermap (first item)))
			 (mapcar 'car (second item)))
			(t item)))))

(defun handle-directory (c)
  (unless
      (and (typep c 'retrieve)
	   (slot-boundp c 'item)
	   (listp (item c)))
    (return-from handle-directory))
  
  (let ((r (find-restart 'directory c)))
    (when
	r
      (invoke-restart r (item c)))))

(defun handle-file (c)
  (unless
      (and (typep c 'retrieve)
	   (slot-boundp c 'item))
    (return-from handle-file))

  (let ((r (find-restart 'file c)))
    (when
	r
      (invoke-restart r (item c)))))

And so we can

(dolist (s (list 'foo-specify 'dir-specify))
  (handler-bind ((retrieve #'handle-directory)
		 (retrieve #'handle-file)
		 (t #'error))
    (handler-bind
	((got-request-for #'handle-got-request))
      (as-gopher () () (signal 'got-request-for
			       :item-specifier
			       s))))
  (terpri) (terpri))

(see the top).

Conclusions

I am pretty happy with this. It shows I can straightforwardly view anything kind of like an association list as being an expression of gopher protocol, then navigate and browse it according to rfc1436 in a runtime-computed way using the common lisp condition system in this non-TCP realisation.

Some obvious realisation targets are signalling the literal view that lisp pathname stuff on my computer’s directories and files is part of the gopher, lisp code as data in general is part of the gopher, the gopher (I will use someodd.zip’s admittedly Haskellian server stuff) is part of the gopher, and if we imagine separating links-on-page and just-text-on-page in lynx, the web is part of the gopher.

Similar to how I gave the great solderpunk of ROOPHLOCH and gemini protocol a hard time because I did not think transport layer security should be specified inside the communication protocol, here I have implemented RFC1436 without transmission control protocol neither.

Addendum about what new this can do

Gamedev of the ages and lisp schemer mdhughes.tech said this was a pretty clear example of conditions but asks what can they do that function calls can not.

This is a point Kent Pitman also made about my simple examples, that if all you are doing is basically making a function call, the correct thing to do would be to instead just make a function call.

An impossible-example

Edit: Forgot example running

IMPOSSIBLE-EXAMPLE
CL-USER> (handler-bind ((retrieve #'handle-directory)
	       (retrieve #'opposite-day)
	       (retrieve #'handle-file)
	       (t #'error))
  (impossible-example))
rab
oof
"rab
oof"

We introduce an opposite-day handler in which a file’s text is reversed iff *opposite-day* is true and the reversed text is given to a restart named file if it is available on the condition being handled.

Then we define a function impossible-example that signals to retrieve an item.

We then call impossible-example inside a handler-bind where opposite-day can accept a retrieve signal.

This is impossible lexically, because opposite-day does not have and has not had lexical access to the file restart which it accesses by computing restarts (in this case by find-restart) available on the retrieve condition it handles.

(defparameter *opposite-day* t)

(defun opposite-day (c)
  (unless (and *opposite-day*
	       (typep c 'retrieve)
	       (slot-boundp c 'item))
    (return-from opposite-day))

  (let* ((meti (reverse (format nil "~a" (item c))))
	 (r (find-restart 'file c)))
    (when
	r
      (invoke-restart r meti))))

(defun impossible-example ()
  (handler-bind
      ((got-request-for #'handle-got-request))
    (as-gopher () ()
      (signal 'retrieve :item "foo
bar"))))

(handler-bind ((retrieve #'handle-directory)
	       (retrieve #'opposite-day)
	       (retrieve #'handle-file)
	       (t #'error))
  (impossible-example))

Hopefully I got that right!

Mastodon thread: https://gamerplus.org/@mdhughes@appdot.net/116079791969629546

screwlisp proposes kittens