CL-USER> (with-open-file (f "/tmp/makapaka"
:direction :output
:if-exists :supersede)
(pprint '(a potentially (complex data structure)
mixing lists #(and vectors) 2/3
#C(2 5) and stuffs) f))
; No value
CL-USER> (with-open-file (f "/tmp/makapaka")
(read f))
(A POTENTIALLY (COMPLEX DATA STRUCTURE) MIXING LISTS #(AND VECTORS) 2/3 #C(2 5)
AND STUFFS)
In this example, I printed and read back a list containing symbols, lists, vectors, rationals and complex numbers.Basically: the reader converts strings to Lisp objects[0]. The printer converts Lisp objects into strings. When we say that an object has a "readable representation", it means that if you feed the result of the printer into reader, you get the same object back.
The evaluator (which I didn't use here, but OP did) evaluates Lisp objects (not strings!), and can e.g. introduce a new function to the environment when evaluating a defun form, i.e. a list of form (defun name (args...) "docstring" code). It is not needed (in fact, it is discouraged[1]) when all you care about is writing and reading back Lisp objects.
--
[0] - Note it's the same reader that actually reads your code when you compile/load file or use a REPL.
[1] - The usual eval-on-untrusted-data code execution vulnerability. Note that since the reader is mostly used to process code, by default it allows you to use the #. read-time-value reader macro. If you READ a string like: "(1 2 #.(* 2 2))", you'll get a Lisp list (1 2 4), since #. basically means "pipe the next s-expression to eval, and return that". When you use reader/printer for serialization/deserialization, you usually want to disable #. for those invocation of read function, by setting
*READ-EVAL*
variable to NIL.
To expand upon this, Lisp can write its lists to files (and most other things out of the box) which it can then read back from files and then eval. This is what Lisps mean by a printable/readable object.
In Clojure:
In Common Lisp: