Have I Missed Something ?

Hi,

I have been learning Lisp (SBCL 1.0.1 for intel Mac) for a couple of months as time permits. I have installed and tweaked cl-opengl nnd I am now starting a 'project' as a learning experience, a simple Lisp editor using OpenGL. I have a package for it and all is well on the general code creating front.

However, in the course of my fiddlings, I have created a structure called 'editor-env':

(defstruct editor-env
  buf                   ; buffer for the loaded file                                                                                                 
  top                   ; the top-edge [y] coordinate of the window                                                                                  
  left                  ; the left-edge [x] coordinate of the window                                                                                 
  row                   ; cursor row value                                                                                                           
  col                   ; cursor column value                                                                                                        
  row-max               ; maximum height of full columns plus one                                                                                    
  col-max               ; maximum width of full columns plus one                                                                                     
  csr-char              ; character for the cursor position                                                                                          
  paper                 ; background color                                                                                                           
  ink                   ; text foreground color                                                                                                      
  font font-dx font-dy  ; font face and cell size                                                                                                    
)

As my code is growing, I am finding that I am typing things like this:

(defun rowcol->screen (row col)
  (let (x y)
    (setq x (+ (editor-env-left *env*) (* col (editor-env-font-dx *env*))))
    (setq y (+ (editor-env-top *env*) (* row (editor-env-font-dy *env*))))
    (return-from rowcol->screen (values x y))))

(Any suggested style / Lisp idiomatic improvements always welcome.)

I mean of course the (editor-env-FFFF *env*) idiom for setf/getf usage, over and over and over again. Dare I say that I almost miss the ability of the Java/C++ '.' operator (or -> etc). I have used Smalltalk for eight years and that even seems better than the above!

So, my question is this...have I missed something fundamental in the way that I could be acessing fields in my structure. I know that I could declare a macro but

  1. is that the 'Lisp' way (I am still learning remember)
  2. isn't that just creating my own syntactic sugar ?

If it is a case of (1) using macros then I guess I am still climbing the learning curve. After 21 years in software, you still have to climb. Daily.... otherwise you slide back down.

What I really want to know is, how does everybody else do it / deal with a high volume of field access. Is it with multiple-value-bind type things, is there a PASCAL 'with' or something that I have yet to find. The HyperSpec is truly huge and I think that the Hitch-Hikers Guide pales in comparison.

Many thanks,
Sean Charles.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

This seems off topic for

This seems off topic for LtU. A Lisp discussion group would be more appropriate.

Snippets

I agree that a Lisp forum would be more appropriate but there is something "meta" about the verbosity that tries to creep into Lisp programs that use structures and classes. Having to read and write prefixes like editor-env- several times in every small function is just silly.

I don't know a really good and canonical solution but here are some code snippets that might be stimulate some ideas.

First just a basic tweak:

(defun rowcol->screen (row col)
  "Return the X and Y coordinates of ROW and COL as values."
  (values (+ (editor-env-left *env*) (* col (editor-env-font-dx *env*))))
          (+ (editor-env-top *env*) (* row (editor-env-font-dy *env*))))

Then one option would be to unpack the fields of *env* into their own special variables:

(defun rowcol->screen (row col)
  (values (+ *left* (* col *font-dx*))
          (+ *top* (* row *font-dy*))))

Or to use WITH-SLOTS which SBCL supports on structures:

(defun rowcol->screen (row col)
  (with-slots (left top font-dx font-dy) *env*
      (values (+ left (* col font-dx))
              (+ top (* row font-dy)))))

Or use CLOS'ish short-named generic functions for accessors as you get from DEFCLASS instead of DEFSTRUCT:

(defun rowcol->screen (row col)
  (values (+ (left *env*) (* col (font-dx *env*))))
          (+ (top *env*) (* row (font-dy *env*))))

The Tutorial on Good Lisp Programming Style may provide inspiration too. Otherwise there's a lot of Lisp code on the 'net where you can see how other people write code in practice (e.g. common-lisp.net) but don't expect to find only one answer with Lisp. :-)

-of

In CLOS I am using the convention slot-of for accessors, e.g. left-of, which was suggested on comp.lang.lisp some time ago.

I'd love to have the dot notation, too.

dot notation

You could have a dot'ish syntax like this:

(print @*env*.top)
(setf @myenv.top 42)

with this read macro:

(defun read-slot-reference (stream &optional c n)
  "Read @foo.bar as (SLOT-VALUE foo 'bar)"
  (declare (ignore c n))
  (let ((*readtable* (copy-readtable)))
    (set-syntax-from-char #\. #\Space)
    (let ((object (read stream t nil t))
          (slot (read stream t nil t)))
      `(SLOT-VALUE ,object ',slot))))

(set-macro-character #\@ 'read-slot-reference)

but I'm not sure if you can peel off the leading @ in a portable way.

RE: Off topic...

My mistake... I don't think I have quite appreciated the 'intent' of this forum.

Thanks for the quality replies, I have already been enlightened by what is possible.

So, where can I find a good lisp forum... comp.lang.lisp... I don't seem to be able to find a Usenet system... any ideas ?

comp.lang.lisp,

PLT style

Luke Gorrie has pretty much covered this, but the simple answer is that you could define a macro to execute some code within an editing environment:

(defmacro with-editor-env ((env) &body body)
  `(with-slots (buf top left row col row-max col-max csr-char paper ink font font-dx font-dy) ,env
     ,@body))

You could then write code like this:

(with-editor-env ((make-editor-env :buf 4 :ink :foo))
  (setf col 42)
  (values buf col ink))

Brace yourself, because I'm going to make this relevant to programming language design. This with-editor-env macro is leading toward something important: the notion of the current environment. If you have multiple environments -- say, multiple files open -- then I think it's worthwhile to structure your code as switching between environments rather than always specifying which environment your code is acting upon in every bit of code that performs an action.

This is very similar to the way you actually use a text editor, so there's a lot less mismatch between what you expect to program and what you actually end up programming. Check out this example, some emacs lisp code I wrote yesterday:

(defun picflow-show-in-c-buffer (args)
  (save-excursion
    (pop-to-buffer "*picflow-output*")
    (delete-region (point-min) (point-max))
    (insert (read (remove 13 (cadr args)))) ; insert c code
    (goto-char (point-min))
    (c-mode)))

This switches to a new buffer (creating it if it doesn't exist), deletes any current contents, inserts some text, goes to the beginning of the buffer, and then turns on C mode for that buffer so it gets proper syntax highlighting. Importantly, it then restores the context in which the user was working -- this is handled by the save-excursion special form.

This code would have been longer if emacs made me pass around environment variables all the time, but more crucially it would have been less obvious how to write it. The language's editing constructs very closely approximate the commands available to the user.

I think this is interesting as an example of a situation in which imperative, very stateful programming is actually the Right Thing to do.

Comonads?

Brace yourself, because I'm going to make this relevant to programming language design. This with-editor-env macro is leading toward something important: the notion of the current environment. If you have multiple environments -- say, multiple files open -- then I think it's worthwhile to structure your code as switching between environments...

...I think this is interesting as an example of a situation in which imperative, very stateful programming is actually the Right Thing to do.

I wonder if this isn't what comonads are for.

I think this is interesting

I think this is interesting as an example of a situation in which imperative, very stateful programming is actually the Right Thing to do.

Indeed. And buffer-local variables make Emacs buffers into my favourite object-oriented programming construct. Emacs Lisp is outrageously underappreciated in this world.

'Applicative' programming?

Rather than comonads, how about Applicative programming?
import Control.Applicative

data EditorEnv = EditorEnv {
    buf::String,
    top::Int,
    left::Int,
    row::Int,
    col::Int
    -- etc.
}

env = EditorEnv { buf = "",top = 1,left = 2,row = 3,col = 4 }
-- We'd like to write: example = (top+row,col+left) but we have to use:
example = pure (,) <*> (pure (+) <*> top <*> row) <*> (pure (+) <*> col <*> left)
Completely eliminates explicit references to env in example. We can just compute example env. Needs a bit of sugaring - the paper suggests a novel syntax using brackets but there's probably a nice way without extending the syntax. Otherwise I'd use a state monad to make the references to env implicit.

with-edit-env macro

Luke, I really had a good 'Eureka' moment with your version about using 'values' and dropping the return-from. It's hard to remember nuances(?) when you don't yet know them! I guess I am still thinking like a C/C++/Java person and not remembering that the return value is the value of the last executed form.

The Tutorial on Good Lisp Programming Style was a good read, forgotten most of it already but it reminds me a lot of my Kent Beck Smalltalk Best Practices book. I am always dipping into that one and I will make a point of reading this one as well.

Peter, I have already written *that* macro, I eventually decided that it was 'the thing to do' in a Lisp world and I need to learn more about macros. Having suffered with C for 20 years, I know a good thing when I see(!) it. Groan. :-)

My only personal criticism of doing it that way (with-editor-env ...), from my still-a-noob-at-Lisp status is that AFAICR, 'with-slots' is only for use on CLOS objects but SBCL allows it to work for structures. To my mind that is not portable, a small point but nonetheless an important one to me.

I think that I will now alter my macro so that it instead creates some local bindings in a 'let', long winded maybe but portable.

As for the "Comonadic Functional Attribute Evaluation"; I am still reading that. All grist for the mill. Now, all I have to do is convert it into plain English...

Peter, regarding this code...

(defun picflow-show-in-c-buffer (args)
  (save-excursion
    (pop-to-buffer "*picflow-output*")
    (delete-region (point-min) (point-max))
    (insert (read (remove 13 (cadr args)))) ; insert c code
    (goto-char (point-min))
    (c-mode)))

...from your p-o-v *your* code is cleaner but somewhere in the implementation isn't the same issue going to crop up with accessing the current buffers properties for modification ?

Well, having said all that I still don't think that I have yet found the 'ideal' solution to my question / problem, indeed there may not be an ideal solution. It never ceases to amaze me that you can get completely sidetracked from the main thrust of effort by some seemingly innocent little detail. That's the beauty of software. LOL.

Thanks, this is a very informative group around here. I hope to be learning a lot more from you guys and maybe chipping in when I feel more competent with Lisp.

Sean Charles

Portability

My only personal criticism of doing it that way (with-editor-env ...), from my still-a-noob-at-Lisp status is that AFAICR, 'with-slots' is only for use on CLOS objects but SBCL allows it to work for structures. To my mind that is not portable, a small point but nonetheless an important one to me.

More Lisp sins are committed in the name of portability (without necessarily achieving it) than for any other single reason.

Sins, eh?

I'm pretty sure this counts as a sin:

(defmacro make-with-struct-macro (struct-name slots)
  `(defmacro ,(intern (format nil "WITH-~A" struct-name)) ((env) &body body)
     (let ((env-sym (gensym)))
       `(let ((,env-sym ,env))
	  (symbol-macrolet (,@(loop for slot in ',slots
				 collect (list slot `(,(intern (format nil "~A-~A" ',struct-name slot))
						       ,env-sym))))
	    ,@body)))))

(make-with-struct-macro editor-env (buf top left row col row-max col-max csr-char paper ink font font-dx font-dy))

This has the same effect as my with-editor-env macro, but portably. And it can be extended to work with other structs as well, just by adding more calls to make-with-struct-macro. Incidentally, seancharles, putting a bunch of local bindings in a LET form probably isn't exactly what you want. You want to be able to modify struct values with setf, and Lisp isn't like C in this respect: you're not dealing with pointers, you're dealing with setf expansions. What my utterly incomprehensible abomination of a macro does it use symbol-macrolet to expand symbols like "ink" to setf-compatible forms like (editor-env-ink env).

This is really only interesting as an example of where macros start to break down into incomprehensibility and bugs. Notice that there are no less than three nested layers of macros: a macro that generates a macro that generates (among other things) some macro code.

It also has the potential for the two classic macro pitfalls: variable capture and multiple evaluation, both narrowly averted at the cost of making the macro even less readable. This could easily have turned into a horrible, bug-ridden mess. Is that indicative of the fundamental limitations of Lisp-style macros, or am I just doing something inherently perverse?

I suspect that there are some common parts of Common Lisp macro writing which are inescapably mind-bending, and the best you can do is encapsulate them. For example, some of the creepy mind-bending nature of the code above can be eliminated with the classic once-only macro from cl-utilities:

(defmacro make-with-struct-macro (struct-name slots)
  `(defmacro ,(intern (format nil "WITH-~A" struct-name)) ((env) &body body)
     (once-only (env)
       `(symbol-macrolet (,@(loop for slot in ',slots
			       collect (list slot `(,(intern (format nil "~A-~A" ',struct-name slot))
						     ,env))))
	  ,@body))))

That eliminates a couple layers of LETs and gensyms -- at the cost of putting them into a macro that is so truly beyond mortal understanding that it makes all who gaze upon it go mad. See for yourself.

ONCE-ONLY

Funny that ONCE-ONLY is regarded as magic that you needn't try to understand. It's only a couple of months ago that I had to debug an incorrect implementation of ONCE-ONLY in a program I'd downloaded. :-)

I guess the author couldn't convince himself that the standard version is correct and decided to make some changes..

SLIME: WITH-STRUCT Elisp/CL

BTW in SLIME we have a simpler portable WITH-STRUCT macro, written by Helmut Eller if I recall correctly:

(defmacro with-struct ((conc-name &rest names) obj &body body)
  "Like with-slots but works only for structs."
  (flet ((reader (slot) (intern (concatenate 'string
					     (symbol-name conc-name)
					     (symbol-name slot))
				(symbol-package conc-name))))
    (let ((tmp (gensym "OO-")))
    ` (let ((,tmp ,obj))
        (symbol-macrolet
            ,(loop for name in names collect 
                   (typecase name
                     (symbol `(,name (,(reader name) ,tmp)))
                     (cons `(,(first name) (,(reader (second name)) ,tmp)))
                     (t (error "Malformed syntax in WITH-STRUCT: ~A" name))))
          ,@body)))))

It's late so I haven't checked against your versions for subtle pitfalls and I assume they're specific to macro-writing-macroness. :-)