src/chancery.lisp @ 11dfd8438119 default tip
Added tag v1.0.1 for changeset acf092c22cdb
| author | Steve Losh <steve@stevelosh.com> |
|---|---|
| date | Thu, 27 Aug 2020 22:17:20 -0400 |
| parents | acf092c22cdb |
| children | (none) |
(in-package :chancery) ;;;; Utils --------------------------------------------------------------------- (deftype non-keyword-symbol () '(and symbol (not keyword))) (defmacro -<> (expr &rest forms) "Thread the given forms, with `<>` as a placeholder." ;; I am going to lose my fucking mind if I have to program lisp without ;; a threading macro, but I don't want to add another dep to this library, so ;; here we are. `(let* ((<> ,expr) ,@(mapcar (lambda (form) (if (symbolp form) `(<> (,form <>)) `(<> ,form))) forms)) <>)) (defmacro assert-nonempty (place message) `(assert (not (emptyp ,place)) (,place) ,message)) (defmacro gimme (n &body body) `(loop :repeat ,n :collect (progn ,@body))) (defun emptyp (string) (zerop (length string))) (defun cat (&rest strings) "Concatenate `strings` into a string." (apply #'concatenate 'string strings)) (defun ch (string index) "Return the character of `string` at `index`. Allows negative indices." (if (emptyp string) nil (aref string (if (minusp index) (+ (length string) index) index)))) (defun chop (string n) "Chop `n` characters off the end of `string`" (subseq string 0 (max 0 (- (length string) n)))) (defun vowelp (character) (ensure-boolean (member character '(#\a #\e #\i #\o #\u #\A #\E #\I #\O #\U)))) (defun prefix-sums (sequence &aux (sum 0.0f0)) (map 'list (lambda (n) (incf sum (float n sum))) sequence)) (defun separate-with-spaces (list) (-<> list (split-sequence :. <>) (mapcar (rcurry #'riffle #\Space) <>) (apply #'append <>))) (defun join-string (&rest parts) (apply #'cat (mapcar #'princ-to-string parts))) ;;;; RNG ---------------------------------------------------------------------- (defparameter *random* #'random "The random number generation function to use (default: `CL:RANDOM`).") (defun chancery-random (n) (funcall *random* n)) ;;;; Weightlists -------------------------------------------------------------- (defstruct (weightlist (:constructor %make-weightlist)) ;; items and weights are the original things passed in. ;; sums and total are coerced to single floats for easier comparison. items weights sums total) (defun make-weightlist (items weights) "Make a weightlist of the given items and weights. Weights can be any `real` numbers. Weights of zero are fine, as long as at least one of the weights is nonzero (otherwise there's nothing to choose). " (%make-weightlist :items items :weights weights :sums (prefix-sums weights) :total (coerce (apply #'+ weights) 'single-float))) (defmethod print-object ((object weightlist) stream) (print-unreadable-object (object stream :type t) (prin1 (mapcar #'list (weightlist-weights object) (weightlist-items object)) stream))) (defmethod make-load-form ((object weightlist) &optional environment) (make-load-form-saving-slots object :slot-names '(weights sums items total) :environment environment)) (defun weightlist-random (weightlist) "Return a random item from the weightlist, taking the weights into account." (loop :with n = (chancery-random (weightlist-total weightlist)) :for item :in (weightlist-items weightlist) :for weight :in (weightlist-sums weightlist) ;; Use <= instead of < here to work around https://github.com/Clozure/ccl/issues/342 :when (<= n weight) :do (return item))) ;;;; Core --------------------------------------------------------------------- (defun special-form-p (form) (ensure-boolean (and (consp form) (member (first form) '(quote eval))))) (deftype special-form () '(satisfies special-form-p)) (defun compile-list (contents) `(list ,@(mapcar #'compile-expression contents))) (defun compile-symbol (symbol) `(,symbol)) (defun compile-special-form (expression) (destructuring-bind (symbol argument) expression (ecase symbol (quote `(quote ,argument)) (eval argument)))) (defun compile-expression (expression) (typecase expression (null expression) (non-keyword-symbol (compile-symbol expression)) (special-form (compile-special-form expression)) (string expression) (list (compile-list expression)) ; todo: vectors? (t expression))) (defun build-weightlist-weighted (size weights) (make-weightlist (range 0 size) weights)) (defun build-weightlist-zipf (size exponent) (loop :with denominator = (loop :for n :from 1.0 :to size :sum (/ (expt n exponent))) :repeat size :for rank :from 1 :for item :from 0 :for weight = (/ (/ (expt rank exponent)) denominator) :collect item :into items :collect weight :into weights :finally (return (make-weightlist items weights)))) (defun compile-selector-uniform (expressions) (values `(chancery-random ,(length expressions)) expressions)) (defun compile-selector-weighted (expressions) (values `(weightlist-random ,(build-weightlist-weighted (length expressions) (mapcar #'first expressions))) (mapcar #'second expressions))) (defun compile-selector-zipf (expressions &key (exponent 1.0)) (values `(weightlist-random ,(build-weightlist-zipf (length expressions) exponent)) expressions)) (defun compile-selector (distribution-and-options expressions) (destructuring-bind (distribution &rest distribution-options) (ensure-list distribution-and-options) (apply (ecase distribution (:uniform #'compile-selector-uniform) (:weighted #'compile-selector-weighted) (:zipf #'compile-selector-zipf)) expressions distribution-options))) (defun compile-rule-body (expression-compiler expressions distribution) (if (= 1 (length expressions)) (funcall expression-compiler (first expressions)) (multiple-value-bind (selector expressions) (compile-selector distribution expressions) `(case ,selector ,@(loop :for i :from 0 :for expression :in expressions :collect `(,i ,(funcall expression-compiler expression))))))) (defun compile-define-rule (expression-compiler name-and-options expressions) (destructuring-bind (name &key documentation (distribution :uniform) (arguments '())) (ensure-list name-and-options) `(defun ,name ,arguments ,@(ensure-list documentation) ,(compile-rule-body expression-compiler expressions distribution)))) (defun compile-create-rule (expression-compiler options expressions) (destructuring-bind (&key documentation (distribution :uniform) (arguments '())) options (compile nil `(lambda ,arguments ,@(ensure-list documentation) ,(compile-rule-body expression-compiler expressions distribution))))) (defmacro define-rule (name-and-options &rest expressions) "Define a function that will return random elements of `expressions`. `name-and-options` should be of the form: (name &key documentation (distribution :uniform) (arguments '())) If no options are needed a bare symbol can be given. `name` is the symbol under which the resulting function will be defined. `documentation` will be used as a docstring for the resulting function. `distribution` denotes the distribution of elements returned. `arguments` is the arglist of the resulting function. Examples: (define-rule color :blue :green :red) (define-rule (metal :documentation \"Return a random metal.\" :distribution :zipf) :copper :silver :gold :platinum) See the full documentation for more information. " (compile-define-rule #'compile-expression name-and-options expressions)) (defun create-rule (expressions &rest options) "Return a function that will return random elements of `expressions`. `options` should be of the form: (&key documentation (distribution :uniform) (arguments '())) `documentation` will be used as a docstring for the resulting function. `distribution` denotes the distribution of elements returned. `arguments` is the arglist of the resulting function. Examples: (create-rule (list :blue :red :green)) (create-rule (list :copper :silver :gold :platinum) :documentation \"Return a random metal.\" :distribution :zipf) See the full documentation for more information. " (compile-create-rule #'compile-expression options expressions)) (defmacro generate (expression) "Generate a single Chancery expression. Example: (define-rule x 1 2 3) (generate (x x x)) ; => (1 3 1) " (compile-expression expression)) (defun invoke-generate (expression) "Generate a single Chancery expression. THIS FUNCTION IS EXPERIMENTAL AND SUBJECT TO CHANGE IN THE FUTURE. Because this is a function, not a macro, you'll need to do the quoting yourself: (define-rule x 1 2 3) (generate (x x x)) ; => (1 3 3) (invoke-generate '(x x x)) ; => (2 1 2) " (eval (compile-expression expression))) ;;;; Strings ------------------------------------------------------------------ (defun compile-string-combination (list) `(join-string ,@(-<> list (separate-with-spaces <>) (mapcar #'compile-string-expression <>)))) (defun compile-string-modifiers (vector) ; #("foo" a b c) => (c (b (a "foo"))) `(princ-to-string ,(reduce (flip #'list) vector :start 1 :initial-value (compile-string-expression (aref vector 0))))) (defun compile-string-other (expr) `(princ-to-string ,expr)) (defun compile-string-expression (expression) (typecase expression (string expression) (null "") (non-keyword-symbol (compile-symbol expression)) (special-form (compile-special-form expression)) (vector (compile-string-modifiers expression)) (cons (compile-string-combination expression)) (t (compile-string-other expression)))) (defmacro define-string (name-and-options &rest expressions) "Define a function that will return random stringified elements of `expressions`. `name-and-options` should be of the form: (name &key documentation (distribution :uniform) (arguments '())) If no options are needed a bare symbol can be given. `name` is the symbol under which the resulting function will be defined. `documentation` will be used as a docstring for the resulting function. `distribution` denotes the distribution of elements returned. `arguments` is the arglist of the resulting function. Examples: (define-string color \"white\" \"gray\" \"black\") (define-string (animal :distribution :weighted) (100 (color \"cat\")) (100 (color \"dog\")) (100 (color \"dragon\"))) See the full documentation for more information. " (compile-define-rule #'compile-string-expression name-and-options expressions)) (defun create-string (expressions &rest options) "Return a function that will return random stringified elements of `expressions`. `options` should be of the form: (&key documentation (distribution :uniform) (arguments '())) `documentation` will be used as a docstring for the resulting function. `distribution` denotes the distribution of elements returned. `arguments` is the arglist of the resulting function. Examples: (create-string (list \"white\" \"gray\" \"black\")) (create-string '((100 (color \"cat\")) (100 (color \"dog\")) (100 (color \"dragon\"))) :distribution :weighted) See the full documentation for more information. " (compile-create-rule #'compile-string-expression options expressions)) (defmacro generate-string (expression) "Generate and stringify a single Chancery string expression. Example: (define-string x 1 2 3) (generate-string (x x x)) ; => \"1 3 1\" " (compile-string-expression expression)) (defun invoke-generate-string (expression) "Generate and stringify a single Chancery expression. THIS FUNCTION IS EXPERIMENTAL AND SUBJECT TO CHANGE IN THE FUTURE. Because this is a function, not a macro, you'll need to do the quoting yourself: (define-string x 1 2 3) (generate-string (x x x)) ; => \"1 3 3\" (invoke-generate-string '(x x x)) ; => \"2 1 2\" " (eval (compile-string-expression expression))) ;;;; Modifiers ---------------------------------------------------------------- (defun cap (string) "Capitalize the first character of `string`." (assert-nonempty string "Cannot capitalize an empty string.") (string-capitalize string :end 1)) (defun cap-all (string) "Capitalize each word of `string`." (assert-nonempty string "Cannot capitalize-all an empty string.") (string-capitalize string)) (defun a (string) "Add an indefinite article (a or an) to the front of `string`." (assert-nonempty string "Cannot add an article to an empty string.") (cat (if (vowelp (ch string 0)) "an " "a ") string)) (defun s (string) "Pluralize `string`." ;; todo: fix for caps ;; todo: make this suck less in general, see http://blog.writeathome.com/index.php/2011/12/how-to-make-nouns-plural/ (assert-nonempty string "Cannot pluralize an empty string.") (case (char-downcase (ch string -1)) (#\y (if (vowelp (ch string -2)) (cat string "s") (cat (chop string 1) "ies"))) ((#\x #\s #\z) (cat string "es")) (#\h (cat string (case (ch string -2) ((#\c #\s) "es") (t "s")))) (t (cat string "s")))) (defun pos (string) "Make `string` posessive by adding an apostrophe (and possibly an s)." (assert-nonempty string "Cannot make an empty string posessive.") (cat string (if (eql #\s (ch string -1)) "'" "'s"))) ;;;; Scratch ------------------------------------------------------------------