Dot notation for object field access and method calls - and bracket notation for indexing

The new language super adds (to an existing language such as racket) traditional dot notation for object field access and method calls.

1. o.f                       access field f of object o
2. o.f1.f2                   access field f2 of object o.f1
3. (o .m a ...)              invoke method m on object o with arguments a...
4. (o .m1 a1 ... .m2 a2 ...) same as ((o .m1 a1 ...) .m2 a2 ...)
5. (o.m a ...)               invoke method m on object o wth arguments a... 
6. (o.m a ... .m1 a1 ...)    invoke method m1 on resultof object (o.m a ...) with arguments a2 ... 

As an added bonus expressions of the type id[expr] now expands to (#%ref id expr) and can be
used for indexing.

See GitHub - soegaard/super: Adds syntax to racket languages

Installation:

raco pkg install super
16 Likes

Thank you for this! This notation has been one of my favorite parts the Sketching #lang you made (which is also awesome!) and I'm glad to see it released standalone.

That said, it doesn't seem to work on structs here? Can you please add that (should I make a github issue for it?), since I use structs far more often than objects in my work and having to write the struct type for every access is more annoying to me than writing send.

Also if there's another way to avoid writing the struct type for every access please let me know, because so far I've only seen it in Sketching.

Hi @hasn0life

Standard structs do not have enough reflection information on runtime. In Sketching I made a new struct that expands to the standard struct and at the same time made information on the struct available at runtime. The lack of reflection information, is why super doesn't support dot-notation.

However you can use the Sketching version of struct in a standard Racket program. Import the Sketching versions of struct and #%top (which handles the dot notation).

#lang racket
(require (only-in sketching struct #%top :=))

(struct horse (breed height color))
(define bella (horse "Danish Warmblood" 171 "brown"))
bella
bella.height
(:= bella.height 172)
bella

The output is:

(horse "Danish Warmblood" 171 "brown")
171
(horse "Danish Warmblood" 172 "brown")
3 Likes

[self-promotion]

Jens's language addition does more than struct-plus-plus, but if all you're looking for is dotted notation you can get that and a bunch more without the additional language pack.

#lang racket

(require struct-plus-plus)

(struct++ person (name))
(define bob (person 'bob)) ; construct as usual
(println (person.name bob)) ; dotted accessor. prints 'bob                                                      

(define fred (person++ #:name 'fred)) ; keyword constructor                                    

(struct++ person1 ([name string?])) ; contract on the field
(define alice (person1++ #:name 'alice)) ; ERROR! Must be string                               

(struct++ person2 ([name any/c ~a])) ; accepts anything, converts to string                    
(define tom (person2++ #:name 'tom))
(println (person2.name tom)) ; prints "tom"                                                    

(println (struct++-ref tom)) ; data structure containing reflection info                       

[/self-promotion]

2 Likes

Now that I think about it doing dot notation is fundamentally at odds with dynamically typed languages. I figured coming from languages like C or C# that Racket should have no issue here, but one can't actually do static dispatch with the dot notation without types as far as I can tell.

Like if you have

(struct horse (breed height color name))
(define bella (horse "Danish Warmblood" 171 "brown" "bella"))

(struct person (name))
(define bob (person 'bob))

(define (get-name my-struct)
    my-struct.name)

the dot notation there has to dynamically dispatch on the name parameter because it can't know if its a horse or person. In C# this wouldn't be an issue because the function could only take a horse or person type struct and you'd have to write two functions or overload it. Sketching actually does the dynamic dispatch there, which is awesome :smiley:

In that sense I'm surprised racket doesn't have enough reflection information because it can tell when you get the type wrong.

(define (get-name my-struct)
    (horse-name my-struct))

   (get-name bob)
. . horse-name: contract violation
  expected: horse?
  given: #<person>

I have no idea how you'd leverage that aspect to create dynamic dispatch though.

@dstorrs to be specific, I mean I want (bob.name), not (person.name bob). Writing the type of the struct is what I'm trying to avoid. Now I can see why it's done that way though.

1 Like

@hasn0life

I wouldn't say " dot notation is fundamentally at odds with dynamically typed languages",
but having extra type information available would make dot notation both more efficient
and nicer to work with.

Let's look at the example:

(struct horse (breed height color name))
(define bella (horse "Danish Warmblood" 171 "brown" "bella"))

(struct person (name))
(define bob (person 'bob))

(define (get-horse-name my-horse)
    my-horse.name)

When my-horse.name is compiled, it would be useful to know that my-horse is expected to be a horse. One option would be to introduce a new form, say, declare that can associate indentifiers with struct types.

(define (get-horse-name my-horse)
    (declare my-horse horse)
    my-horse.name)

Now, if would be nicer, if declare was integrated in define: as in:

(define (get-horse-name my-horse:horse)
    my-horse.name)

but that is going to be harder to add to the existing system.

Apropos dynamic languages and typing: Python has the the concept of "type hints".
Consider this function, which accepts a name (a string) and returns a greeting (a string):

def greeting(name) :
    return 'Hello ' + name

With type hints this becomes:

def greeting(name: str) -> str:
    return 'Hello ' + name

Now the type hints are not enforced by the system (cpython), so greeting(3) won't give you an error message, but tools such as type checkers, editors etc can make use of the type hints.

In our horse example, if the editor has seen (declare horse) and has found the declation
(struct horse (breed height color name)) then an editor could show a list of fields to choose from when the user writes my-horse.

It works surprisingly well for Python with Pylance in Visual Studio Code.

2 Likes

It's not the notation you wanted, but does this accomplish your goal?

#lang racket

(require struct-plus-plus)

(struct++ person (name))
(struct++ dog    (name))
(struct++ horse  (color))
(define bob  (person 'bob))
(define fido (dog    "fido"))
(define secretariat (horse 'brown))

(define (get-field from field-name)
  (-> any/c symbol? any/c)

  (define info (force (struct++-ref from)))
  (for/or ([field (in-list (struct++-info-fields info))])
    (match field
      [(struct* struct++-field ([name name] [accessor accessor]))
       (and (eq? name field-name)
            (accessor from))])))

(println (get-field bob 'name))   ; prints 'bob                                                
(println (get-field fido 'name))  ; prints "fido"                                              
(println (get-field secretariat 'name))  ; prints #f
(println (get-field secretariat 'color)) ; prints 'brown                                      

3 Likes

@hasn0life

The example from @dstorrs shows that struct++ has the runtime information needed.
If you want to challenge, then try changing .top from super to use struct++ instead.

It's relatively short:

Note that struct++ by choice doesn't support mutable fields.
But on the bright side it does have other nifty features such as rules and converters.

3 Likes

Expanding on that: It allows you to declare the entire struct mutable, but not an individual field.

One of the features I've had on my list for a while is to have mutational setters that check the field contracts, the same way the functional setters do. Haven't found the tuits.

Granted, you sorta-kinda get the benefit since the dotted accessors check that the value they are returning meets the field contract, meaning that the contracts are checked on access instead of on assignment. Not the same and not great, but it's in the ballpark.

#lang racket

(require struct-plus-plus try-catch)

(struct++ person
          ([name non-empty-string?])
          #:mutable
          #:transparent
          )

; the standard racket constructors / accessors do not check contracts
(define alice (person 'alice)) 
(person-name alice)
; 'alice   


; keyword ctor checks contracts; the result is an exn
(pretty-display (defatalize (person++ #:name 'bob)))
;; #(struct:exn:fail:contract:blame person++: contract violation
;;   expected: non-empty-string?
;;   given: 'bob
;;   in: the #:name argument of
;;       (-> #:name non-empty-string? person?)
;;   contract from: (function person++)
;;   blaming: /Users/dstorrs/test.rkt
;;    (assuming the contract is correct)
;;   at: /Users/dstorrs/test.rkt #<continuation-mark-set> #<blame-yes-swap>)

(define charlie (person++ #:name "charlie"))
charlie ; is valid, since name meets `non-empty-string?`
; (person "charlie")

; Racket-generated mutator does not check contract
(set-person-name! charlie 'bad)
charlie ; is NOT valid, since name does not meet `non-empty-string?`
; (person 'bad)

; dotted accessor DOES check contract
(pretty-display (defatalize (person.name (person 'charlie)))) ; exn because accessor failed
;; #(struct:exn:fail:contract:blame person.name: broke its own contract
;;   promised: non-empty-string?
;;   produced: 'charlie
;;   in: the range of
;;       (-> person? non-empty-string?)
;;   contract from: (function person.name)
;;   blaming: (function person.name)
;;    (assuming the contract is correct)
;;   at: /Users/dstorrs/test.rkt #<continuation-mark-set> #<blame-no-swap>)

1 Like

@soegaard I agree that adding type hints would help, would typed racket be useful here?

@dstorrs yea that would work if you could hide the get-field function in the dot, as @soegaard suggests, though I wonder what the performance implications of that lookup would be. I'm guessing the missing runtime info were the struct fields, which struct++ provides?

That said, while I'm not opposed to adding struct++ to super, what was the issue with overriding the structs with the implementation used in Sketching? It worked well enough for me.

@dstorrs

Expanding on that: It allows you to declare the entire struct mutable, but not an individual field.
Makes much more sense! I skimmed the docs a bit too fast :-).

@hasn0life

I agree that adding type hints would help, would typed racket be useful here?
For implementing type hints? I don't think so, but some implementation techniques could
probably be reused. I think, there are some infrastructure made for Rhombus that could be used though.

But if you were to use a Typed Racket program, then I think there is enough information available to make dot notation work for a Typed Racket program.

1 Like

The implementation in Sketching works fine.

The suggestion for making a super++ language with dot-notation for struct++ was just in case you wanted to have fun implementing dot notation.

A few reasons:

  1. Sketching is a language focused on graphical applications which happens to include structs with dot notation accessors as a small part of it. struct-plus-plus is a module suitable for use with any language and it includes dot notation accessors, variant constructors, field contracts, field wrappers, accessor wrappers, functional setters, functional updaters, rules, converters, and reflection data.
  2. struct-plus-plus came out two years before Sketching. The proper question is "why didn't Sketching override the structs with the implementation used in struct-plus-plus?" :>
2 Likes

Forgot to respond to this part: You can in fact dig the fields out of Racket's struct descriptor, which exists for any struct. For examples on how to do this, check the code for Alexis King's struct-update module. That module is a giant upon whose shoulders I stood when writing SPP.

What you can't get from the struct descriptor, of course, is all the stuff that SPP adds, such as field contracts, wrappers, rules, etc. For that I needed my own reflection data.

1 Like
  1. struct-plus-plus came out two years before Sketching. The proper question is "why didn't Sketching override the structs with the implementation used in struct-plus-plus ?" :>

In general Sketching has minimal dependencies.
With respect to structs, I didn't need more than a small macro could do the job.

But two years - that begs the question: Why haven't struct-plus-plus got dot-notation yet? :wink:

1 Like

Oh, sure. You're doing something different than I was, and (from what I can tell by the docs) doing it extremely well. I was being tongue in cheek.

Erm...it does? It's had it since day one.

Is there a dot notation that handles the following:

(struct a (n))
(define an-a (a 1))
an-a.n ; ok this is simple.
(define the-same-a an-a)
(the-same-a.n) ; I think this is not simple.

Somewhere deep in my archives I have a system that does this, but it is not practical, because it consumes much time during execution for looking up the struct to which struct-type n refers in
(the-same-a.n)., especially if there is another struct-type with fieldname n too.
Jos

I tried this (based on the comment of @soegaard above) and it works:

> (require (only-in sketching struct #%top :=))
> (struct a (n))
> (define an-a (a 1))
> an-a.n
1
> (define the-same-a an-a)
> (the-same-a.n)
application: not a procedure;
 expected a procedure that can be applied to arguments
  given: 1
 [,bt for context]
> the-same-a.n
1
1 Like

So I've been looking for "dot notation" in Racket for a long time and I have considered struct++ in the past. The issue is that it doesn't do what Sketching does, and as far as I can tell, Sketching is the first to what I want that I've found in the racket ecosystem.

I think to clarify, its not actually dot notation that I'm looking for in Racket per se. What I actually care about is the dynamic dispatch part (at least in a non statically typed language).

Or to be more specific, the thing that I find annoying is that while Racket is dynamically typed it doesn't have dynamic dispatch or generics, so you're constantly writing types at the call site.

So like in the examples I showed above

(struct horse (breed height color name))
(define bella (horse "Danish Warmblood" 171 "brown" "bella"))

(horse-name bella)
"bella"

the issue for me is having to write horse every time I want to access bella's name. For example I would settle for a syntax like

(.name bella)
"bella"

which I think is how clojure does hash table access?

This issue extends to Racket's other data structures as well. Only lists let you write "non-prefixed" functions. So you can call length on a list, but you have to call vector-length on a vector, same with vector-map and vector-set!, etc. What using the bracket notation in Super #lang and Sketching gives is dynamic (or generic?) access to the data structures, and Sketching also let you mutate structs without prefixing the setter as well.

This all makes a big difference for me because if you're trying to use these data structures in line with the code operating on them, you're either writing long prefixes every time you use them, or you destructure them ahead of time every time and give them a shorter name. I find both options boiler-platey, hard to read, and annoying to write. It wasn't until I tried Sketching that I realized how much I disliked this. I believe other dynamically typed languages like javascript and python all do dynamic dispatch for the dot notation and usually have special syntax for data structure access. The performance is probably worse that way, but I would at least like the option.

I welcome dot notation for objects as well, though I use them less and I don't have to keep writing the type so its not as bad. But I just really want it for structs too. We have it in Sketching and I want the rest of the Racket eco system to get it as well. If I have to write (require (only-in sketching struct #%top :=)) then fine, but I think it should be in Super. (I guess I can also fork it and add it myself but yea)

/essay rant lol

edit: Actually on further thought, the point of racket objects and classes is to do the dynamic dispatch, right? So maybe I should leave the structs alone and I should just use the objects for that sort of thing, especially with supers dot notation? Perhaps what I'm ultimately after is the syntax or I'm just used to typed languages where one can statically dispatch stuff and I miss that.

I was being tongue in cheek.

I was attempting the same.

But two years - that begs the question: Why haven't struct-plus-plus got dot-notation yet? :wink:

Erm...it does? It's had it since day one.

I think we are thinking of different versions of dot-notation. But you are right,
we are are trying to do different things with structs in struct++ vs Sketching.
My main concern was to make Sketching programs feel like Processing ones.

1 Like