lisp:land-of-lisp:ch7
This commit is contained in:
		
							
								
								
									
										210
									
								
								lisp/land-of-lisp/ch7/graphlib.lisp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								lisp/land-of-lisp/ch7/graphlib.lisp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,210 @@ | ||||
| (defun dot-name (exp) | ||||
| 	(substitute-if #\_ (complement #'alphanumericp) (prin1-to-string exp))) | ||||
|  | ||||
| ;; A node in DOT format can only contain letters, digits, and the underscore | ||||
| ;; So here we are replacing any forbidden characters to underscores | ||||
|  | ||||
| ;; example  | ||||
| ;; (substitute-if #\e #'digit-char-p "I'm a l33t hack3r!") | ||||
| ;; >> "I'm a leet hacker!" | ||||
| ;; This says, if a char of the string is also a number, sub | ||||
|  | ||||
| ;; By passing (complement) above, we are basically saying: | ||||
| ;; If char is not alphanumeric, replace with "_" | ||||
|  | ||||
| (defparameter *max-label-length* 30) | ||||
|  | ||||
| (defun dot-label (exp) | ||||
| 	(if exp | ||||
| 		(let ((s (write-to-string exp :pretty nil))) | ||||
| 			(if (> (length s) *max-label-length*) | ||||
| 				(concatenate 'string (subseq s 0 (- *max-label-length* 3)) "...") | ||||
| 				s)) | ||||
| 		"")) | ||||
|  | ||||
| ;; max-label-length is our max chars for our label | ||||
| ;; if node label is larger than the limit, it gets cropped and an ... is added | ||||
| ;; (write-to_string) is similar to (prin1-to-string) as it writes an expression | ||||
| ;; to a string. | ||||
|  | ||||
| ;; :pretty parameter is a keyword parameter that tells lisp not to alter the | ||||
| ;; string to make it pretty. Without this, lisp would place new lines or tabs | ||||
| ;; into our converted string to make it look more pleasing to the eye | ||||
| ;; by setting :pretty to nil, we tell lisp to output the expression without  | ||||
| ;; any decorations (having new lines in a label can confuse Graphviz) | ||||
|  | ||||
| (defun nodes->dot (nodes) | ||||
| 	(mapc (lambda (node) | ||||
| 					(fresh-line) | ||||
| 					(princ (dot-name (car node))) | ||||
| 					(princ "[label=\"") | ||||
| 					(princ (dot-label node)) | ||||
| 					(princ "\"];")) | ||||
| 				nodes)) | ||||
|  | ||||
| ;; here we use mapc to go hrough eveyr node in the list of nodes | ||||
| ;; princ prints each node in the DOT format directly to screen | ||||
| ;; mapc is a slightly more efficient variant of mapcar; | ||||
| ;; The difference is that it does not returne the transformed list | ||||
| ;; The nodes->dot function uses the (dot-name) and (dot-label) to convert data | ||||
|  | ||||
| (defun edges->dot (edges) | ||||
| 	(mapc (lambda (node) | ||||
| 					(mapc (lambda (edge) | ||||
| 											 (fresh-line) | ||||
| 											 (princ (dot-name (car node))) | ||||
| 											 (princ "->") | ||||
| 											 (princ (dot-name (car edge))) | ||||
| 											 (princ "[label=\"") | ||||
| 											 (princ (dot-label (cdr edge))) | ||||
| 											 (princ "\"];")) | ||||
| 								(cdr node))) | ||||
| 				edges)) | ||||
|  | ||||
| (defun graph->dot (nodes edges) | ||||
| 	(princ "digraph{") | ||||
| 	(nodes->dot nodes) | ||||
| 	(edges->dot edges) | ||||
| 	(princ "}")) | ||||
|  | ||||
| (defun dot->png (fname thunk) | ||||
| 	(with-open-file (*standard-output* | ||||
| 										fname | ||||
| 										:direction :output | ||||
| 										:if-exists :supersede) | ||||
| 		(funcall thunk)) | ||||
| 	(ext:shell (concatenate 'string "dot -Tpng -O " fname))) | ||||
|  | ||||
| ;; To keep this dot->png function reusale, the graph->dot function isn't | ||||
| ;; called directly, instead we write it to accept a thunk | ||||
|  | ||||
| ;; In this scenairo, a function without arguments is commonly called a thunk | ||||
| ;; or suspension. In this caes, the thunk our function needs would be a | ||||
| ;; function that, when called, prints a DOT file to the console. | ||||
|  | ||||
| ;; Why is a thunk useful? | ||||
| ;; the easiest way for us to write and debug graph->dot and other DOT | ||||
| ;; file functions is to have them print their results directly to | ||||
| ;; the console.  When we call graph->dot, it doesn't return | ||||
| ;; its results as a value, but instead, prints them at the console | ||||
| ;; as a side effect. Therefore we can't just pass the value | ||||
| ;; of graph->dot to dot->png. | ||||
| ;; instead we pass in graph->dot as a thunk. then dot->png is responsible for  | ||||
| ;; calling graph->dot, caputring the results, and sending them to a file | ||||
|  | ||||
| ;; this technique is used a lot | ||||
| ;; First we print stuff to the console; next we wrap it in a thunk; | ||||
| ;; Finally we redirect the results to some other location | ||||
|  | ||||
| ;; Writing to a file | ||||
|  | ||||
| ;; The function with-open-file enables dot->png to write information to a file | ||||
| ;; for clarity, here's an example | ||||
| ;;(with-open-file (my-stream | ||||
| ;;									"testfile.txt" | ||||
| ;;									:direction :output | ||||
| ;;									:if-exists :supersede) | ||||
| ;;	(princ "Hello File!" my-stream)) | ||||
|  | ||||
| ;; :direction is set to :output (we are only writing, no reading) | ||||
| ;; :if-exists :supersede (if a file with that name exists, overwrite) | ||||
|  | ||||
| ;; putting a colon in front of a symbol means that symbol always means itself | ||||
| ;; so :cigar can only be a constant named :CIGAR | ||||
|  | ||||
| ;; Basicaly, this just says that a symbol has its own meaning. | ||||
|  | ||||
| ;; so back to (dot->png) How does it save to file, and not just go to the console? | ||||
|  | ||||
| ;; Aparently with-open-file is analogous to using (let) to create a variable.  | ||||
| ;; Hence, it usually leads to the creation of a lexical (local) stream variable. | ||||
|  | ||||
| ;; However, if a dynamic variable already existsw with the same name, | ||||
| ;; (let) will temporarily override the value of the dynamic variable to the | ||||
| ;; The new value. | ||||
|  | ||||
| ;; *standard-output* is such a dynamic variable | ||||
| ;; This means that we can temporarily override the value of *standard-output* | ||||
| ;; to a new value by passing it into our (with-open-file) command | ||||
|  | ||||
| ;; In the body, where we call our thunk, any values printed to the console, | ||||
| ;; will now be automagically routed to our file instead. | ||||
|  | ||||
| ;; Well, that is interesting. Everything that normally goes to | ||||
| ;; *standard-output*, will instead go to our file within this function | ||||
|  | ||||
| ;; Alright, let's wrap this up | ||||
|  | ||||
| (defun graph->png (fname nodes edges) | ||||
| 	(dot->png fname | ||||
| 						(lambda () | ||||
| 							(graph->dot nodes edges)))) | ||||
|  | ||||
| ;; This function takes the name of a DOT file (as the variable fname) | ||||
| ;; as well as the graph's nodes and edges and uses them to | ||||
| ;; generate the graph | ||||
|  | ||||
| ;; It calls dot->png and creates the appropriate thunk, a lambda function | ||||
| ;; as is usual for a thunk, it takes no parameters. | ||||
|  | ||||
| ;; The graph->dot function is called inside the thunk as a | ||||
| ;; delayed computation. Specifically if we called graph->dot directly, | ||||
| ;; its output would just show up in the console. However, when inside the thunk | ||||
| ;; it will be called at the leisure of the dot->png function, and the output | ||||
| ;; will generate the dot file with the filename passed in as the first param | ||||
| ;; to graph->png | ||||
|  | ||||
| ;; Cool, now we can make a directional graph with this: | ||||
| ;; (graph->png "wizard.dot" *nodes* *edges*) | ||||
|  | ||||
| ;; Now let's make a graph that's non-directional. | ||||
|  | ||||
| (defun uedges->dot (edges) | ||||
| 	(maplist (lambda (lst) | ||||
| 						 (mapc (lambda (edge) | ||||
| 										 (unless (assoc (car edge) (cdr lst)) | ||||
| 											 (fresh-line) | ||||
| 											 (princ (dot-name (caar lst))) | ||||
| 											 (princ "--") | ||||
| 											 (princ (dot-name (car edge))) | ||||
| 											 (princ "[label=\"") | ||||
| 											 (princ (dot-label (cdr edge))) | ||||
| 											 (princ "\"];"))) | ||||
| 									 (cdar lst))) | ||||
| 					 edges)) | ||||
|  | ||||
| (defun ugraph->dot (nodes edges) | ||||
| 	(princ "graph{") | ||||
| 	(nodes->dot nodes) | ||||
| 	(uedges->dot edges) | ||||
| 	(princ "}")) | ||||
|  | ||||
| (defun ugraph->png (fname nodes edges) | ||||
| 	(dot->png fname | ||||
| 						(lambda () | ||||
| 							(ugraph->dot nodes edges)))) | ||||
|  | ||||
| ;; (maplist) is like (mapcar) except that the function inside it | ||||
| ;; receives the entire remainder of the list, not just the | ||||
| ;; current item in the list | ||||
|  | ||||
| ;; (maplist) sends the print function everything in the list from  | ||||
| ;; the current item until the end | ||||
|  | ||||
| ;; uedges->dot then uses the information about future nodes it gets | ||||
| ;; from maplist to check whether the destination of the node appears | ||||
| ;; later in the edge list | ||||
|  | ||||
| ;; The actual checking is done with (assoc) looking for the current edge | ||||
| ;; in the list of remaining edges calculated as (cdr lst) | ||||
| ;; In this case it skips the edge so that only one of any pair of edgs will be | ||||
| ;; printed | ||||
|  | ||||
| ;; (ugraph->dot) is similar to (graph->dot) except that it describes | ||||
| ;; the graph as just a graph when making DOT data instead of digraph | ||||
|  | ||||
| ;; (ugraph->png) is almost the same as (graph->png) except that it calls | ||||
| ;; (ugraph->dot) instead of (graph->dot) | ||||
|  | ||||
| ;; Test (ugraph->png "uwizard.dot" *nodes* *edges*) | ||||
|  | ||||
		Reference in New Issue
	
	Block a user