-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
720 Allow methods in maps with access to $this #916
Conversation
I appreciate the effort spent in the new proposal. There are basically two aspects that continue to make me feel uneasy:
As I indicated in the original thread, I’d feel much more comfortable if we had a custom data type for objects that allowed for more static optimizations. Maps certainly have properties that make them suitable for simulating objects. Other properties are contradictory and error-prone: Variables can be removed at runtime that functions rely on; functions can be overwritten with arbitrary data or functionality; the initial record type can be easily changed by performing updates that don’t match the item-type definition; etc. etc. Another important aspect is that error messages are already wildly cryptic when functions are involved. A basic example: declare item-type num:int as record(n as xs:integer, square as %method function() as xs:integer);
declare function new-int($n as xs:integer) as num:int {
map { 'n' :$n, 'square': %method fn() { $this?n * $this?n } }
};
let $int := new-int(3)
let $updated-int := map:remove($int, 'square')
return $updated-int?square() The raised error messages would be something like “An empty sequence cannot be invoked”. The actual “bug” occurs earlier in the code, though: Why can methods be removed at all? Shouldn’t it be the task of the programming language to prevent users from doing this? If we had an object type… declare function new-int($n as xs:integer) as object(*) {
object { 'n': $n, 'square': fn() { $this?n * $this?n } }
}; …we wouldn’t provide functions like However, I’m aware that it may be unrealistic to already achieve this with version 4.0 of the language if we decide to finalize it in 2024. |
An editorial note: When I first read the proposal, it was not obvious to me that the record definition is not a prerequisite for declaring maps with methods. Perhaps the first example could be stripped down to the basics: declare function geo:rectangle($height, $width) {
map {
"height": $height,
"width": $width,
"area": %method function() { $this?height * $this?width }
}
}; …or even… let $rectangle := function($height, $width) {
map {
"height": $height,
"width": $width,
"area": %method function() { $this?height * $this?width }
}
}; |
Thanks for the feedback.
I think the question of maps being rather too flexible and therefore too error-prone is valid, but it's orthogonal to the proposal. I think you're looking for some way of labelling a map with a type that constrains what operations can be performed on the map; I think that's a separate issue. It's more ambitious than this proposal, and I would couple it with the equally desirable (and ambitious) aim of defining an explicit class hierarchy. The point about this proposal is that it gives you a lot of bangs for the buck. |
Another editorial note: I've rewritten the examples to use a potential let $calc := object {
"product": fn($in) {
if (empty($in))
then 1
else head($in) * $this?product(tail($in))
}
}
return $calc?product((1.05, 1.04, 1.03))
let $lib := object {
"even": fn($v) { $v = 0 or $this?odd(abs($v)-1) },
"odd" : fn($v) { not($this?even($v)) }
}
return $lib?odd(23) …but I think we should go further and provide a custom declarator (maybe there's a better name than declare object int($value as xs:integer) {
(: variables; could also be generated automatically
from the defined parameters :)
value := $value,
(: functions.. here, `$value` would work as well :)
square = fn() { int($this?value * $this?value) }
}
let $int := int(5)
return $int?square()?square()?value It would be much easier for processors (@rhdunn maybe even IDEs?) to raise static errors for undefined functions, such as |
First of all: Nothing is easier than countering elaborate work with contrasting suggestions, and hoping that someone will fix all the bits and bobs for you. That being said, I’ll exactly continue with that…
I think I’m looking for more than that, and yet I believe it’s not too far away: One essential aspect that I consider essential, besides user feedback, is efficiency: Objects and functions can be implemented very slickly and straightforwardly if their structure is statically known: you can work with fixed offsets to address their contents, etc. “Looking up” functions in maps already implies that it’s not necessarily a cheap approach: There is usually no need to go and search something if it always exists in the same place. Just think of sequences: While it is possible to store their contents in maps, no implementor would do that as there are smarter and more efficient ways. This may all be irrelevant for an occasional computation of a square or product value, but it makes a big difference if we want to allow users to write serious applications. By spending some more time on the fundamentals, I believe we can push the concept much further than by just extending an existing data structure that was primarily designed to hold a variable size of keys and values. If we introduce methods as proposed, I doubt we’ll envisage a more thorough solution later on. The good thing is: Hardly any map function would need to be duplicated for a new object/struct/record type. Next, if we had a declarator for this data structure, the types could be derived from that constructor. Just think of the example in the proposal: declare function geo:rectangle(
$height as xs:double, $width as xs:double)
as record(height as xs:double,
width as xs:double,
area as %method function() as xs:double) {
map{"height": $height,
"width": $width,
"area": %method function(){$this?height * $this?width}
}
}; Wouldn’t it be much more convenient to drop the redundant information and derive the structure from the declaration?… declare record geo:rectangle($height as xs:double, $width as xs:double) {
height := $height,
width := $width,
area := function() { $this?height * $this?width }
}; …or even (: definition: record: height as xs:double, width as xs:double, area as function() as xs:double :)
declare record geo:rectangle($height as xs:double, $width as xs:double) {
area := function() { $height * $width }
}; It’s tempting to combine this with the already existing record definitions, but then we would again resort to maps.
I like the phrase. It’s probably exactly the bangs I’m pretty much afraid of… |
I think records should be sufficient w.r.t. static checking, as they define the properties that are required and optional. That includes function type signatures. The object concept would help in the case where the functions are the same across all instances of the object. In that case, an IDE could more easily link/navigate to the function definition. Otherwise, the IDE would need to analyse where/how the instance was created in order to try and extract that information. There should be enough context with the current proposal for an IDE to infer the presence and type of the |
As a potential user, I find some aspects of this PR opaque and unexpected. I am under the general impression that the expressions Further, if the method is not activated under the last two of the three expressions, what can a user expect returned when calling I assume that a map can refer to a function defined externally for its method function, e.g., , |
Takeaway from today's discussion. I think there are really two things we are trying to achieve. One is the ability to write functions that refer to the map/record with which they belong using a variable such as $this. As pointed out, this can be done easily enough using the The second is the ability to give the function a name with local scope, so that we can have an area() function for rectangles that is different from the area() function for circles, without having to use globally-scoped names. One solution would be to have an operator that is like "?" in that it looks for the function within the map (or interprets the function name with local scope in some other way), but is like "=>" in that it uses the LH operand as the implicit first argument of the function. Perhaps we could define Or perhaps we could allow the syntax In both cases area is a regular function, so if you want to do a regular function call you can, for example
|
Another already existing variant for
|
If we're going to end up adding magic, can we just define the problem out of existence with more magic? Can we simply say that if the expression on the right hand side of
Given that we have |
That breaks compatibility, and it stops you putting ordinary ("static") functions in a map. Also note that There's a range of possibilities for making Let's explore that idea:
Semantically kludgey, but it doesn't impose much cognitive load on the typical user. |
Good to read, that’s what I also had in mind yesterday. It would be my preferred approach (and I believe it would cause fewer complications than dormant variables at compile time). |
My notation was a bit sloppy, I guess. When I said |
I hope it is still not too-late. I propose to have a simple syntax for invoking a "member-function" with key myFunName on a map $m like this: $m ?> myFunName({any-arguments-here}) The rules are simple:
|
That's certainly viable, though the number of unfamiliar operators is becoming a bit daunting. |
There are nice Unicode symbols like this: ⋗ Or this: ⌦ Or: ▶ , ⧁ , ⩥ , ⫸ We could establish a good business selling keyboards with keys for these 😄 |
More seriously, going with this eliminates the need to add annotations to the member-functions. |
An advantage of From the implementer’s point of view, the |
OK, after 2-days considerations I find the below one the best - it is a very clear visual sign and it is also just 2 ordinary key-strokes: |> |
I don't see why this could be an "advantage" - more like unnecessary limitation. We do have a much simpler alternative now in this discussion and this is significant. |
I do not like this %method annotation It might hurt the performance if the processor has to check at each lookup if it returns a function and what annotations the function has.
I think that is better With a syntax, the processor only has to deal with member functions if the user actually wants to use member functions |
I'm coming to the conclusion that a custom operator is indeed probably the best way to go. While I would love to exploit some of the richness of Unicode, one of the problems is that many of the symbols are not easily distinguishable - that applies in particular to the many arrow shapes; it would be a nightmare for users to find the right one. So I think an ASCII composite symbol is probably prudent. Both "?>" and "|>" seem viable, we can take a majority vote. Another alternative might be middle dot,
|
If we are convinced that we want to introduce syntactic sugar, …
@michaelhkay Would |
Yes provided that A is a singleton map and f() doesn't take the form f(x, y) where x and y are context-sensitive expressions. |
I am in favor of |> . We are using "?" in so many different operators that this places a significant cognitive load on the brain when matching the text to one of the many operators that contain "?". Also, a "?" intuitively implies doubt/questions which also may intervene with the brain's straight-forward processing, reading and comprehension. When the developer specifies that a certain member-function should be invoked, he should not have any questions/doubts whether or not this is possible - this is more of an order - not a question. |
This proposal has been superseded. |
This proposal allows functions within maps to access the containing map using the variable $this.
The proposal needs editorial work to integrate it fully into the text, but it is intended to be sufficiently complete to enable a full technical review.
Fix #720