hyrule
hyrule copied to clipboard
Pyrsistent
One of the nice things about Clojure is the persistent immutable data structures. These have been implemented in Python via the Pyrsistent library (and perhaps others).
It might be nice if we could use some of these with a short reader macro. It seems like a common desire for those coming from Clojure. Rather than letting everyone implement their own macro versions, this might be worth putting in contrib.
A more radical step would be to make Hy's default data structures use the Pyrsistent versions, and make the Python native structures available via the reader macros.
I'm less comfortable with that idea. It would create a dependency and might be less compatible with Python libraries or potentially future Python syntax. Though since Python is duck typed, probably not that much less compatible. Also, Pyrsistent is MIT licensed, so it wouldn't be that much of a problem as a dependency.
+1 to the idea of using reader macros, -1 to changing Hy's defaults. Part of the awesomeness of Hy comes from the fact that it's ultimately still Python, and changing that would partly lose that benefit, along with slowing things down quite a bit.
Yeah, adding some syntactic sugar for these things to hy.contrib seems fair enough, but making [1 2 3]
create something other than a plain list
by default is asking for trouble.
Something as simple as
(import pyrsistent)
(defreader p [form]
`(pyrsistent.freeze ~form) )
would get us pretty far. That's enough for PVector
PMap
and PSet
. The freeze
function is recursive, so it also works on nested structures,
=> #p[#{1 2} {1 "a" 2 "b"}]
pyrsistent.freeze([{1, 2}, {1: 'a', 2: 'b', }])
pvector([pset([1, 2]), pmap({1: 'a', 2: 'b'})])
but there are limitations:
=> #p #{[1 2] {1 "a" 2 "b"}}
pyrsistent.freeze({[1, 2], {1: 'a', 2: 'b', }})
Traceback (most recent call last):
File "<input>", line 1, in <module>
TypeError: unhashable type: 'dict'
=> #p #{#p[1 2] #p{1 "a" 2 "b"}}
pyrsistent.freeze({pyrsistent.freeze([1, 2]), pyrsistent.freeze({1: 'a', 2: 'b', })})
pset([pmap({1: 'a', 2: 'b'}), pvector([1, 2])])
This may or may not be what we want.
There's a lot more to Pyrsistent than freeze
though.
Although, PVector
and PSet
seem equivalent to Python's built-in tuple
and frozenset
, respectively.
The interface seems basically the same, but I think PVector/PSet share data between revisions in a trie like Clojure does. I'm pretty sure Python's tuple and frozenset implementations don't do that.
Perhaps we should put this in extra with an optional dependency for Pyrsistent. This would keep Hy's core install smaller.
I think that frozen
and thaw
are the ones that would benefit the most from a reader macro.
I'm not a fan of the #p
syntax, but it gets the job done.
In common lisp, it's a convention to use *earmuffs*
to name a variable that has special properties (*
for nonlexical/dynamic variables and +
for constants). Here's a mock example:
;; Players can join in from the network, so this variable is a global that has a different
;; mutability behavior (e.g. it may not be completely safe to use it within MAP or FILTER lambdas
;; because it may change between calls, so beware of its different mutability behaviors).
;; Check [some documentation link/some functions] to see how to read and write to it safely.
(defvar *current-players* 100)
;; For some reason, we have a hard limit of players in a single game session.
(defconstant +maximum-players-allowed+ 32768)
;; Somewhere far away from the previous declarations:
(defun create-server-with-exact-number-of-players (target-number
players-to-migrate-to-a-new-server)
"This function groups players, one at a time, into a new server with `target-number` players.
Useful for raid bosses in the final level of a dungeon."
(setq players-added-to-new-server 0)
(map (lambda (player)
(when (< *current-players* target-number) ; beware! *current-players* is a special variable!
(inc players-added-to-new-server)
;; This decrement may not work properly, and the *earmuffs* are a clear indicator
;; that we should double-check that we are doing what we intended.
;; A quick glance at *current-players*'s documentation should remind us of
;; what is the correct thing to do in this case.
(dec *current-players*)))
players-to-migrate-to-a-new-server)
(return players-added-to-new-server))
Maybe we could do something similar?
;; freezes
#+[1 2 3]
#+ #{5 7 11}
#+ MyVar
;; thaws
#- MyFrozenVector
#- MyFrozenNestedDictionary
I don't know how the community feels about earmuffs, because in an earmuff-heavy codebase this could happen:
(setv +my-vector+ #+[1 2 3])
(setv +my-set+ #++my-vector+)
But I'm not sure that #+MyVar
or #-MyFrozenVector
are even legal, so #++my-vector+
probably is a non-issue.
But I'm not sure that
#+MyVar
or#-MyFrozenVector
are even legal
They aren't, or rather, they'll try to call the wrong reader macros. You need a space to terminate the reader macro name, as in #+ MyVar
.