Extenor
1 Guide
2 Reference
extenor?
empty-extenor
extenor-extend
extenor-ref
extenor-set
extenor-keys
extenor-struct-type-properties
extenor-remove-extenorcl
extenor-remove-extenorcl-with-key
extenor-remove-extenorcl-with-struct-type-property
extenor-simple-merge
extenorcl?
extenorcl-name
extenorcl-struct-type-properties
make-extenorcl
define-extenorcl
make-prop-extenorcl
2.1 APIs that should probably exist
2.2 Library Extenorcls
2.3 prop:  custom-write
prop:  custom-write-extenorcl
2.4 prop:  dict
prop:  dict-extenorcl
3 Stability
4 Code and License
8.12

Extenor🔗ℹ

William Hatch <william@hatch.uno>

 (require extenor) package: extenor

1 Guide🔗ℹ

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

Examples:
> (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)

#<extenor>

> (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)

#t

> (frog? point-frog)

#t

; We can access the point information with struct-like accessors.
> (point-x point-frog)

5

> (point-y point-frog)

7

; We can access frog info that way too.
> (frog-size point-frog)

10

; But we can also access visible names with extenor-ref.
> (extenor-ref point-frog 'size)

10

; 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)

'x-value

> (point-x extra-x)

5

; 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)

10

; 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)

#t

> (frog? yet-another-point-frog)

#t

> (println yet-another-point-frog)

#<extenor name:"Toad", size:3>

2 Reference🔗ℹ

See Stability.

procedure

(extenor? v)  bool?

  v : any/c
Predicate - is this an extenor?

The empty extenor.

procedure

(extenor-extend an-extenor    
  an-extenorcl    
  field-val ...)  extenor?
  an-extenor : extenor?
  an-extenorcl : extenorcl?
  field-val : any/c
Add an extenorcl to an extenor. The field-vals must appropriately match the fields of the extenorcl.

procedure

(extenor-ref an-extenor field-name [fallback])  any/c

  an-extenor : extenor?
  field-name : symbol?
  fallback : any/c = (λ () (error 'extenor-ref))
Get a visible field out of an extenor. Note that the field-name must be an interned symbol. The fallback optional argument acts like with hash-ref, if it is a procedure it is executed with no arguments, but as a convenience non-procedures may be provided and returned as-is.

procedure

(extenor-set an-extenor    
  field-name    
  new-value)  extenor?
  an-extenor : extenor?
  field-name : symbol?
  new-value : any/c
Non-destructively (functionally) update a visible field of an extenor. IE get a new extenor with the field value replaced. If the extenor previously had no visible field, this effectively adds a degenerate extenorcl providing that visible field.

procedure

(extenor-keys an-extenor)  (listof symbol?)

  an-extenor : extenor?
Returns a list of keys for visible extenor fields.

procedure

(extenor-struct-type-properties an-extenor)

  (listof struct-type-property?)
  an-extenor : extenor?
Returns a list of struct-type-properties supported by the extenor.

procedure

(extenor-remove-extenorcl the-extenor    
  the-extenorcl)  extenor?
  the-extenor : extenor?
  the-extenorcl : extenorcl?
Returns an extenor like the-extenor but with the-extenorcl removed.

procedure

(extenor-remove-extenorcl-with-key the-extenor    
  key)  extenor?
  the-extenor : extenor?
  key : (and/c symbol? symbol-interned?)
Returns an extenor like the-extenor but with the extenorcl that provides key removed. Note that this may remove multiple keys and/or struct-type-properties.

procedure

(extenor-remove-extenorcl-with-struct-type-property 
  the-extenor 
  stp) 
  extenor?
  the-extenor : extenor?
  stp : struct-type-property?
Returns an extenor like the-extenor but with the extenorcl that provides stp removed. Note that this may remove multiple keys and/or struct-type-properties.

procedure

(extenor-simple-merge l    
  r    
  [#:equality equality])  extenor?
  l : extenor?
  r : extenor?
  equality : 
(or/c #t #f
      (-> any/c any/c any/c))
 = equal?
Merges two extenors. If l and r contain different extenorcls that provide the same visible key or structure-type-property, extenor-simple-merge will raise an exception. If l and r share any extenorcls, the values of the extenorcls will be compared with equality. If equality returns a non-#f value (or if equality is #t), the value in r is used and the value of l is discarded. If equality returns #f (or if equality is #f), an exception is raised.

procedure

(extenorcl? v)  bool?

  v : any/c
Predicate – is this an extenorcl?

procedure

(extenorcl-name an-extenorcl)  any/c

  an-extenorcl : extenorcl?
Get the name of an extenorcl.

Stability - less stable than the rest of this library!

procedure

(extenorcl-struct-type-properties the-extenorcl)

  (listof struct-type-property?)
  the-extenorcl : extenorcl?
Return a list of the struct-type-properties supported by the-extenorcl.

procedure

(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)
  field-name-spec : 
(listof (cons/c (or/c 'hidden 'visible)
                (and/c symbol? symbol-interned?)))
Unless you want to dynamically generate extenorcls, you probably want define-extenorcl instead.

Make an extenorcl. This returns a list containing:
  • 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.

syntax

(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
Static definition form wrapping make-extenorcl. Note that the #:property clause can be used multiple times to add multiple properties to the extenorcl.

Here’s an example:
(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))))

This would define:
  • 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)

procedure

(make-prop-extenorcl some-property    
  prop-val)  extenorcl?
  some-property : struct-type-property?
  prop-val : any/c
Convenience function for making an extenorcl with no fields and a single struct-type-property.

Stability - less stable than the rest of this library!

2.1 APIs that should probably exist🔗ℹ

2.2 Library Extenorcls🔗ℹ

2.3 prop:custom-write🔗ℹ

value

prop:custom-write-extenorcl : extenorcl?

An extenorcl providing a prop:custom-write implementation that displays visible keys with their values. Useful for manually inspecting an extenorcl since by default they are opaque.

2.4 prop:dict🔗ℹ

value

prop:dict-extenorcl : extenorcl?

An extenorcl providing a prop:dict implementation. It basically uses extenor-ref and extenor-set, and as such only accepts interned symbols as keys.

3 Stability🔗ℹ

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.

4 Code and License🔗ℹ

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