hyrule icon indicating copy to clipboard operation
hyrule copied to clipboard

Pyrsistent

Open gilch opened this issue 8 years ago • 8 comments

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.

gilch avatar Feb 09 '17 02:02 gilch

+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.

refi64 avatar Feb 09 '17 03:02 refi64

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.

Kodiologist avatar Feb 09 '17 03:02 Kodiologist

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.

gilch avatar Feb 23 '17 01:02 gilch

Although, PVector and PSet seem equivalent to Python's built-in tuple and frozenset, respectively.

Kodiologist avatar Feb 27 '17 02:02 Kodiologist

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.

gilch avatar Mar 12 '17 23:03 gilch

Perhaps we should put this in extra with an optional dependency for Pyrsistent. This would keep Hy's core install smaller.

gilch avatar May 20 '18 20:05 gilch

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.

gabriel-francischini avatar Jun 04 '22 15:06 gabriel-francischini

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.

Kodiologist avatar Jun 04 '22 15:06 Kodiologist