rhombus-prototype icon indicating copy to clipboard operation
rhombus-prototype copied to clipboard

Aplicative vectors, hashes, ...

Open gus-massa opened this issue 4 years ago • 24 comments

I'd like to use something like:

(define v (vector 'a 'b 'c))
(v 1) ;==> 'b

and similar code for hashs, dicts, ... (and even boxes for completeness (boxes are similar to parameters, so it may make sense))

I prefer to use the exact same code for procedures and vectors (f 1), not like C that uses f(1) and f[1].

It would be necessary to think a generic friendly way to set! the values.

gus-massa avatar Aug 08 '19 22:08 gus-massa

  • It makes sense for vector, list, and hash (though I think indexing by list should be strongly discouraged). I don't think it makes sense for box as it has no index.
  • Would be nice if Racket could know statically that this is an indexing operation.
  • For hash indexing to be useful, I think you need to support "default value" as well (v.get(x, y) or v[x] || y idiom).
  • How do you distinguish dict-backed-by-list indexing and list indexing? (Is this the reason why you omitted list?)

sorawee avatar Aug 08 '19 23:08 sorawee

lists too, I just forget to include them because it's a bad idea for long lists. Perhaps add a comment in the docs, or if it get surprisingly very popular to use list by indices (why???) then make it more efficient using skip list or other advanced method.

In the case of a hash it can raise an error if the key is not found, like a out of bound index in a vector. In both cases it may be useful to have a default value.

gus-massa avatar Aug 09 '19 00:08 gus-massa

What if I make a struct that implements both gen:dict and prop:procedure? As long as that is possible, this seems to introduce a nasty ambiguity, unless you use a different syntax, e.g. x[a] is dictionary access and (x a) is procedure application.

default-kramer avatar Aug 28 '19 17:08 default-kramer

I think that it will use prop:procedure, but there should be a recommendation that the gen:dict is equivalent. It can be different, but it is not a good idea.

For example in a custom dictionary (dict-ref (dict-set! d 'k 'v) 'k) should be 'v in most cases. But it is only a recommendation, it is not enforced.

Also, make-keyword-procedure accept two functions, that in the normal case have the same result when there are no keywords.

Somewhat related: https://github.com/racket/racket/issues/2574

gus-massa avatar Aug 28 '19 23:08 gus-massa

You can do this by providing your own version of #%app, since (v 1) expands to (#% app v 1).

Now expanding all occurrences of #%app into: (if (or (vector? v) (hash? v) (dict? v)) (my-general-ref v . args) (standard-app v . args)) would work, but is not ideal since it effects all applications.

One approach is to use standard parens for normal applications and use braces for the fancy version.

See https://github.com/soegaard/this-and-that/blob/master/brevity/brevity.rkt for inspiration.

/Jens Axel

Den fre. 9. aug. 2019 kl. 00.55 skrev gus-massa [email protected]:

I'd like to use something like:

(define v (vector 'a 'b 'c)) (v 1) ;==> 'b

and similar code for hashs, dicts, ... (and even boxes for completeness ( boxes are similar to parameters, so it may make sense))

I prefer to use the exact same code for procedures and vectors (f 1), not like C that uses f(1) and f[1].

It would be necessary to think a generic friendly way to set! the values.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/racket/racket2-rfcs/issues/102?email_source=notifications&email_token=AADQXRM55N56EWJKOR75DCDQDSP67A5CNFSM4IKPDFQKYY3PNVWWK3TUL52HS4DFUVEXG43VMWVGG33NNVSW45C7NFSM4HEIXCWQ, or mute the thread https://github.com/notifications/unsubscribe-auth/AADQXRJM7FGNFEKSIAUQQ5LQDSP67ANCNFSM4IKPDFQA .

--

Jens Axel Søgaard

soegaard avatar Aug 28 '19 23:08 soegaard

I prefer something like the #app in rackjure/app.rkt#L73, except the part of the curly braces. It even has a test for procedure? so in the common case the optimizer can reduce it to the usual version.

I think part of the apparent drawback is that (v 3) can call arbitrary code if v is a function, but (vector-ref v 3) is just a memory access, but if v has a impersonator or a chaperone, it can call arbitrary code.

gus-massa avatar Aug 29 '19 02:08 gus-massa

@gus-massa I personally find the initial example you gave hard to read. I like that I can tell the difference between sequence accesses / dictionary lookups and arbitrary function calls, either because the former uses collection[key-or-index] syntax or because it requires calling a lookup function like (ref collection key-or-index). Could you share more details about your perspective? I'd especially find it helpful to see real-world code where you'd like to use this feature.

jackfirth avatar Aug 29 '19 04:08 jackfirth

The idea is that instead of

(define (average-xyz v)
  (/ (+ (vector-ref v 0)
        (vector-ref v 1)
        (vector-ref v 2))
     3))

you can write

(define (average-xyz/new v)
  (/ (+ (v 0) (v 1) (v 2)) 3))

Note that if you use

#lang racket
(define v1 (vector 10 20 30))
(define v2 (impersonate-vector v1
                               (lambda (v n x) (random (+ n 1)))
                               (lambda (v n x) (random (+ n 1)))))
(average-xyz v2)
(average-xyz v2)

the calculation of the average can run arbitrary code.

gus-massa avatar Aug 29 '19 14:08 gus-massa

So given that code:

(define (average-xyz v)
  (/ (+ (vector-ref v 0)
        (vector-ref v 1)
        (vector-ref v 2))
     3))

Here are some options:

Option 1: Applicative collections

We make (collection key) a synonym for (ref collection key).

(define (average-xyz v)
  (/ (+ (v 0) (v 1) (v 2)) 3))

Option 2: Generic ref

We don't add any special syntax or shortcuts, but we do add a generic ref function that works for any collection.

(define (average-xyz v)
  (/ (+ (ref v 0) (ref v 1) (ref v 2)) 3))

Option 3: Collection accessor syntax

We make collection[key] a shortcut for (ref collection key).

(define (average-xyz v)
  (/ (+ v[0] v[1] v[2]) 3))

Option 4: Applicative keys

We make (key collection) a shortcut for (ref collection key). Clojure does this, I think, as does the rackjure package.

(define (average-xyz v)
  (/ (+ (0 v) (1 v) (2 v)) 3))

jackfirth avatar Aug 30 '19 20:08 jackfirth

It might be worth considering making a local version

(with-mumble )

in order not to affect applications elsewhere.

/Jens Axel

Den fre. 30. aug. 2019 kl. 22.08 skrev Jack Firth <[email protected]

:

So given that code:

(define (average-xyz v) (/ (+ (vector-ref v 0) (vector-ref v 1) (vector-ref v 2)) 3))

Here are some options: Option 1: Applicative collections

We make (collection key) a synonym for (ref collection key).

(define (average-xyz v) (/ (+ (v 0) (v 1) (v 2)) 3))

Option 2: Generic ref

We don't add any special syntax or shortcuts, but we do add a generic ref function that works for any collection.

(define (average-xyz v) (/ (+ (ref v 0) (ref v 1) (ref v 2)) 3))

Option 3: Collection accessor syntax

We make collection[key] a shortcut for (ref collection key).

(define (average-xyz v) (/ (+ v[0] v[1] v[2]) 3))

Option 4: Applicative keys

We make (key collection) a shortcut for (ref collection key). Clojure does this, I think, as does the rackjure https://docs.racket-lang.org/rackjure/index.html package.

(define (average-xyz v) (/ (+ (0 v) (1 v) (2 v)) 3))

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/racket/racket2-rfcs/issues/102?email_source=notifications&email_token=AADQXRO2H7K7XT6VJMPGRC3QHF44JA5CNFSM4IKPDFQKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD5SU2IQ#issuecomment-526732578, or mute the thread https://github.com/notifications/unsubscribe-auth/AADQXROPQIVV65VHXDGOPELQHF44JANCNFSM4IKPDFQA .

--

Jens Axel Søgaard

soegaard avatar Aug 30 '19 20:08 soegaard

@soegaard Along the same lines, it could be something that you have to opt-in to with (require <mumble>).

jackfirth avatar Aug 30 '19 20:08 jackfirth

I find it difficult to predict whether something like this would be really good or really bad in a big codebase. So it would be good to hear from people who have used something like this a lot or experiment with bigger code examples. I think there might be other ways to approach this, e.g. maybe different forms of pattern matching?

(require <define-with-pattern-match-args>)

;; default arg would be declared with square brackets? [v (vector)]
(define (average-xyz (vector a b c)) 
  (/ (+ a b c) 3))

I do not necessarily endorse this pattern matching example here, it is only provided to say: maybe there are other ways of expressing this we haven't considered yet? I definitively think opt-in and local notations would be good, because I think it would allow to create programs which are at the same time more concise and readable.

In this particular example my preferred implementation would be:

(define (average collection)
  (/ (sum collection)
     (length collection)))

(define (average-xyz v : (vector-of number? 3))
  (average v))

Where : (vector-of number? 3) constraints the type of v to vectors of length 3 containing numbers and ideally that would be enough so that optimal accessor code is generated automatically.

SimonLSchlee avatar Aug 30 '19 22:08 SimonLSchlee

Functions can be thought of as a kind of key-value mapping. It's occasionally reasonable to refactor from a function to a hash map and vice versa. From this point of view, #%app is already an operation dedicated to looking up a key in a key-value mapping, so there doesn't need to be another generic ref.

(On the other hand, giving too many meanings to a single name can jump the shark. If a user thinks of #%app as an operation dedicated to doing only "function calls," they might be disappointed if they accidentally pass in a non-function and it silently succeeds at doing something they didn't expect. This is an argument that can be brought against all kinds of generic operations, so I don't hold a strong opinion either way.)


Anyway, this issue reminds me of some synergy in Arc's design.

Calling hash tables like they were functions was a common sight in Arc, where an explicit hash-ref operation didn't exist. This design choice, together with Arc's chaining a.b and a!b "ssyntax" sugars and Arc's setforms DSL for gettable and settable storage locations, made it pretty easy to perform destructive updates deep within JSON-style nested hashes.

For example:

(++ db!posts.post-id!likes)

Comparable JavaScript code in case that helps clarify the idea:

db.posts[ postId ].likes++;

Back to the Arc version, this time without using ssyntax:

(++ (((db 'posts) post-id) 'likes))

A version without invoking tables as procedures (if we pretend Arc had an explicit hash-ref operation to use instead):

(++ (hash-ref (hash-ref (hash-ref db 'posts) post-id) 'likes))

Finally, translating (++ (hash-ref ...)) into similar Racket code:

(let ([post (hash-ref (hash-ref db 'posts) post-id)])
  (hash-set! post 'likes (add1 (hash-ref post 'likes))))

This last step shows that Arc's approach encompassed a "generic friendly way to set! the values."

While Arc's setforms DSL has a lot in common with Racket's set! transformer DSL, it does a little bit more work to make sure subexpressions like (hash-ref (hash-ref db 'posts) post-id) here are only evaluated once. I think it'd be apt to call it something like a system of "mutating lens transformers."

Since Arc didn't actually have a type-specialized operation like hash-set!, the actual implementation of ++ expanded into a call to a more generic function called sref.

rocketnia avatar Sep 01 '19 23:09 rocketnia

For a reference pony does this

hashMap("a")?
hashMap("a") = 10

? - means that this is partial function (it can raise an error).

stereobooster avatar Jan 04 '21 05:01 stereobooster

Regarding @jackfirth 's nicely presented 4 options, here's my unrequested opinion:

  • Option 1 (OP proposal): Certainly interesting, worth investigating further—I was about to propose the same thing, before discovering this thread. I was even considering (v 1) for ref, (v 1 x) for set, and (v 1 proc default) for update. This didn't seem to mix well with immutable data types though.
  • Option 2: Certainly safe. ref is short enough. I dislike vector-ref and friends which are too verbose.
  • Option 3: I vote against new syntax to fix API issues. Let me have my s-exps!
  • Option 4: [shivers] That just doesn't make sense to me. Something is a key within a context, not because it is in application position.

Metaxal avatar Jan 04 '21 09:01 Metaxal

I prefer that the set! operation is obviously different than the ref operation.

(I strongly want to preserve the ability of using a bang in the name of identifiers. It's a nice warning that the function will mutate the arguments.)

The problem of using the number of arguments to decide what the function will do is

(define v (make-vector 3))
(define a (make-array 10 10))

(v 1 2)
(a 1 2)

The first is a write operation, and the second is just a read operation. But it is not obvious looking at the code.

I prefer something like setf in LISP, but overloading set!. Something like

(set! (v 1) 2)
(a 1 2)

I think that using (v 1) to access the values inside v is the obvious best option, but I'm not so sure about using (set! (v 1) 2) to modify it.

gus-massa avatar Jan 05 '21 16:01 gus-massa

you could also have (a '(1 2)) for get in 2d arrays. Then (a '(1 2) x) still makes sense for set.

Metaxal avatar Jan 05 '21 16:01 Metaxal

Generally I prefer collection views over setf-style syntactic DSLs.

;; Assuming a generic mutable-list interface, returns an implementation of that interface backed by the array's fifth row.
;; The view is live, meaning updates to the array are visible in the view.
;; The view is also write-through, meaning updates to the view cause changes to the array.
(define row (mutable-array-row 2d-array 5))
(list-set! row 7 'foo)

jackfirth avatar Jan 05 '21 21:01 jackfirth

Why choose?

(define row (mutable-array-row 2d-array 5))
(set! (row 7) 5)

rocketnia avatar Jan 05 '21 23:01 rocketnia

I like that, and furthermore it doesn't even need set! to be a macro. (row 7) can return a mutable box view. set! can just be set-box!

jackfirth avatar Jan 05 '21 23:01 jackfirth

Presumably (row 7) outside of a set! would retrieve the value at position 7, not a box view containing that value, right?

rocketnia avatar Jan 06 '21 02:01 rocketnia

Possibly. That kind of question is why I like to work out the underlying view semantics first and make the set! syntax a layer on top.

jackfirth avatar Jan 06 '21 02:01 jackfirth

Goodness. If anyone saw that comment I just posted and deleted, it was not only full of confusing old draft fragments but inflammatory.

@jackfirth, I don't understand focusing on mutable views here when the issue's motivating focus is on set! extensions. When you brought them up, it looked like you thought the two were in conflict, but I think they're orthogonal. The best connection I can think of is that "something you can get and set" is a description that applies to most set! extensions and most mutable box views. It could be fascinating to explore other mutable-view-collection-valued syntactic expression DSLs, but I think mutable box views are the most there is to consider in this case.

To be upfront about the inflammatory part, in the deleted post, I mentioned a time I was recommending for a generic map to be designed with functors in mind, not just streams, and you said a functor approach wasn't "inherently better" and that most people who hear about map think of streams. I feel like I'm on the other side of that now, not knowing any reason mutable view collections should get involved, and thinking that most people who want terse indexing notation are thinking primarily about a syntactic DSL.

rocketnia avatar Jan 06 '21 04:01 rocketnia

Anyhow, that's petty of me, it brings in the unrelated topic of map -- please nobody dig into that here, I don't wanna derail -- and it isn't even really comparable.

In this situation, I don't see any design tension, not even at the level of "people who look up one might be confused to learn about the other." I believe a mutable view collection API and a set! extension API can coexist with good synergy without either one of them depending on the other one's terminology.

rocketnia avatar Jan 06 '21 04:01 rocketnia