William Hatch <email@example.com>
|(require extenor)||package: extenor|
The Extenor package is an experimental package providing a particular kind of extensible object. ExteNoR stands for “Extensible Nominal Record”, and an extenor can be an instance of multiple extenorcls, or “Extensible Nominal Record Class”. An extenorcl is somewhat like a struct-type or a class, in that it defines a set of fields that an instance must have, as well as struct-type-properties. Each extenorcl comes with a predicate that is true for extenors that are instances of that extenorcl, as well as setter and getter functions. Each extenor is immutable, and using a setter or extending an extenor with a new extenorcl are functional opetations that return a fresh extenor.
The main motivation behind this library is to have something like structs that are extensible while still being functionally updatable. Racket structs may be subtyped, so you can extend any given struct with new fields and struct-type-properties. However, if you ever use a functional setter of the original struct-type on your extended struct instance, the object returned will be of the non-extended struct-type! Extenors keep their extensions when they are functionally updated by functions that don’t know about their extensions.
However, extenors are also an experiment with some other ideas in the hopes of making them particularly useful in conjunction with the Rash language. In particular, extenorcl fields have “visibility”. Hidden extenorcl fields are more like struct fields – they can only be accessed with accessor functions specific to the extenorcl. Visible extenorcl fields may also be accessed and updated via extenor-ref and extenor-set. Visible extenor fields are more duck-typable (or perhaps structurally-typable, my dear pedantic PL nerd friends). The main motivation here is that you might have various shell utility functions that you use in pipelines that operate on standard field names, and you may add fields dynamically in a pipeline to make data from some system administration function/command work with some other command. Thus extenors give you something that simultaneously has some of the properties of a dictionary and some of the properties of a nominal record, and in particular the capability to have struct-type-properties (and in fact different struct-type-properties on each instance).
> (require extenor) ; Let's make some extenorcls, IE extenor classes. > (define-extenorcl point ([hidden x] [hidden y])) > (define-extenorcl frog ([visible size] [visible name]))
> (define point-frog-v1 (extenor-extend (extenor-extend empty-extenor point 5 7) frog 10 "Jeremy")) ; Without a custom write procedure, extenors don't print very well. > (println point-frog-v1)
> (require extenor/extenorcl/custom-write-extenorcl) > (define point-frog (extenor-extend point-frog-v1 prop:custom-write-extenorcl)) ; This custom write implementation makes it print visible fields. > (println point-frog)
#<extenor name:"Jeremy", size:10>
; point-frog is both a point and a frog. > (point? point-frog)
> (frog? point-frog)
; We can access the point information with struct-like accessors. > (point-x point-frog)
> (point-y point-frog)
; We can access frog info that way too. > (frog-size point-frog)
; But we can also access visible names with extenor-ref. > (extenor-ref point-frog 'size)
; Hidden fields can't be accessed with extenor-ref or extenor-set. > (extenor-ref point-frog 'x)
extenor-ref: No value found for key: 'x
; We can also functionally update. > (define bigger-frog (extenor-set point-frog 'size 100)) > (println bigger-frog)
#<extenor name:"Jeremy", size:100>
> (define biggest-frog (set-frog-size point-frog 9000)) > (println biggest-frog)
#<extenor name:"Jeremy", size:9000>
; Interned symbols can be used as extenorcls. > (define blue-frog (extenor-extend point-frog 'color "blue")) > (println blue-frog)
#<extenor color:"blue", name:"Jeremy", size:10>
; extenor-set with a name that's not in the extenor extends it ; like extenor-extend with a plain symbol. > (define green-frog (extenor-set point-frog 'color "green")) > (println green-frog)
#<extenor color:"green", name:"Jeremy", size:10>
; Because the fields of point are hidden, they don't conflict ; with other extenorcls that have (hidden or visible) fields named ; x or y. ; This example just causes confusion, but this can allow encapsulation ; of private fields without needing to craft names that nobody else ; will use. > (define extra-x (extenor-set point-frog 'x 'x-value)) > (println extra-x)
#<extenor name:"Jeremy", size:10, x:'x-value>
> (extenor-ref extra-x 'x)
> (point-x extra-x)
; Extenors are dict-like. Let's make a frog implement prop:dict. > (require racket/dict) > (require extenor/extenorcl/dict-extenorcl) > (define dict-frog (extenor-extend green-frog prop:dict-extenorcl)) > (dict-ref dict-frog 'size)
; Actually, it's probably better to start with a more capable ; extenor than the raw empty-extenor.
> (define base-extenor (extenor-extend (extenor-extend empty-extenor prop:custom-write-extenorcl) prop:dict-extenorcl)) ; Maybe we will get related extenors from multiple sources and ; want to join the data. > (define a-point (extenor-extend base-extenor point 42 24)) > (define a-frog (extenor-extend base-extenor frog 3 "Toad"))
> (define yet-another-point-frog (extenor-simple-merge a-point a-frog)) > (point? yet-another-point-frog)
> (frog? yet-another-point-frog)
> (println yet-another-point-frog)
#<extenor name:"Toad", size:3>
(extenor? v) → bool?
v : any/c
(extenor-extend an-extenor an-extenorcl field-val ...) → extenor? an-extenor : extenor? an-extenorcl : extenorcl? field-val : any/c
(extenor-ref an-extenor field-name [fallback]) → any/c
an-extenor : extenor? field-name : symbol? fallback : any/c = (λ () (error 'extenor-ref))
(extenor-set an-extenor field-name new-value) → extenor? an-extenor : extenor? field-name : symbol? new-value : any/c
→ (listof struct-type-property?) an-extenor : extenor?
(extenor-remove-extenorcl the-extenor the-extenorcl) → extenor? the-extenor : extenor? the-extenorcl : extenorcl?
(extenor-remove-extenorcl-with-key the-extenor key) → extenor? the-extenor : extenor? key : (and/c symbol? symbol-interned?)
(extenor-remove-extenorcl-with-struct-type-property the-extenor stp) → extenor? the-extenor : extenor? stp : struct-type-property?
(extenor-simple-merge l r [ #:equality equality]) → extenor? l : extenor? r : extenor?
(or/c #t #f (-> any/c any/c any/c)) = equal?
(extenorcl? v) → bool?
v : any/c
an-extenorcl : extenorcl?
Stability - less stable than the rest of this library!
→ (listof struct-type-property?) the-extenorcl : extenorcl?
(make-extenorcl [ #:name name #:guard guard #:properties properties] field-name-spec ...)
(list/c extenorcl? (-> any/c any/c) (listof (-> extenor? any/c)) (listof (-> extenor? any/c extenor?))) name : (or/c #f (and/c symbol? symbol-interned?)) = #f guard : (or/c #f procedure?) = #f properties : (hash/c struct-type-property? any/c) = (hash)
(listof (cons/c (or/c 'hidden 'visible) (and/c symbol? symbol-interned?)))
An extenorcl object.
A predicate for extenors containing the extenorcl object.
A list of getter functions that operate on extenors containing the extenorcl.
A list of setter functions that operate on extenors containing the extenorcl. Note that they are functional setters that return a new extenor with an updated field, not a mutational setter. Extenors are all immutable, though they may have mutable values in their fields.
The name argument provides a name for the extenorcl. Stability - the name field is less stable than the rest of this library!
The guard can be a procedure that guards what values can be set in the extenorcl (both via the setter returned from this function and from extenor-set) . The guard must be a procedure that accepts a value for each field of the extenorcl and must return a list of the same size. Guards are run for extenor-extend, for the setter of any field from the extenorcl, or when extenor-set is used on a field from the extenorcl. In other words, it can arbitrarily interpose on field setting, though this is probably not advisable. It can also raise an exception, which is probably the better thing to do when given input that is improper for the extenorcl. Stability - I may change the API for guards. As it is the guard can’t tell which field is being set. I want guards to be able to access all fields, so they can guard fields that have invariants relying on each other, but I also want a guard to have easy access to check an individual field.
Each field-name-spec is a pair specifying field visibility and field name. Each field name must be an interned symbol. Field visibility is either 'hidden or 'visible. Hidden fields can be accessed with a getter and setter returned by make-extenorcl, but not with extenor-ref or extenor-set. While each hidden field within a single extenorcl must have a unique name, different extenorcls can use the same hidden field name. Visible fields can be accessed with extenor-ref and extenor-set (though setting a field may trigger a guard). Visible fields in one extenorcl may conflict with another, so that an extenor can only have one of the two extenorcls.
TODO - extenorcls should also support generics. However, at the time of writing generics have only a static interface. Properties can be added to dynamically generated structs via make-struct, while generics can’t be. This is poor, and despite wording in many parts of the Racket docs, generics should not be preferred over struct-type-properties until they have a similar dynamic interface.
(define-extenorcl name (field-spec ...) keyword-arg ...)
field-spec = field-name | [hidden field-name] | [visible field-name] keyword-arg = #:guard guard-expression | #:property prop-expr val-expr
(define-extenorcl frog (weight [hidden poisonous?] [visible name]) #:property prop:custom-write (λ (self port mode) (fprintf port "<Frog: name=~v>" (frog-name self))) #:property prop:procedure (λ (self . args) (extenor-set self weight (apply + (frog-weight self) args))))
frog as an extenorcl
frog? as a predicate for extenors that contain the extenorcl
frog-weight as a getter for the visible weight field
set-frog-weight as a setter for the visible weight field
frog-poisonous? as a getter for the hidden poisonous? field
set-frog-poisonous? as a setter for the hidden poisonous? field
frog-name as a getter for the visible poisonous? field
set-frog-name as a setter for the visible poisonous? field
Then we could run:
(define arnold-the-frog (extenor-extend empty-extenor frog 25 #f "arnold")) (define fat-arnold (arnold-the-frog 5 10 50)) ; Returns 90 (extenor-ref fat-arnold 'weight)
(make-prop-extenorcl some-property prop-val) → extenorcl? some-property : struct-type-property? prop-val : any/c
Stability - less stable than the rest of this library!
merge-extenors – this should have various options related to conflicts. IE what equality predicate (if any) to decide whether a field/property is the same in both, when both extenors have field values that aren’t equal should the merge raise an exception or should it prefer the field in one of the extenors, etc
prop:custom-write-extenorcl : extenorcl?
prop:dict-extenorcl : extenorcl?
Or, lack thereof.
I’m not yet willing to commit to anything here. You can email me if you think I should change my mind about this.
Besides some things marked throughout this document as things I may potentially change, I may even change the names “extenor” and “extenorcl”. In particular, the two names are really similar and easy to confuse.
The code is available on github.
This library is distributed under the MIT license and the Apache version 2.0 license, at your option. (IE same license as Racket.)