Skip to content
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

The =?> lookup arrow expression operator is weird, difficult to use, difficult to understand, difficult to read and unnatural #1800

Closed
dnovatchev opened this issue Feb 14, 2025 · 31 comments · Fixed by #1817
Labels
Bug Something that doesn't work in the current specification Decluttering Cut out dead wood Discussion A discussion on a general topic. Enhancement A change or improvement to an existing feature Feature A change that introduces a new feature PR Pending A PR has been raised to resolve this issue XPath An issue related to XPath

Comments

@dnovatchev
Copy link
Contributor

The XPath 4.0 language now includes a way for a function defined as a member of a map to easily access other members (siblings) that belong to the same map instance. Special syntax, the =?> operator, was introduced to call such a function. As a whole this is a huge step forward providing the user with a new, powerful mechanism to conveniently express relationships and calculations over several member-values of a map instance.

I am raising this issue with the goal of further improving and simplifying for the user the way to define and call a member function of a map/record, giving it a convenient way to access the values of other members of the instance of the map, on which the call has been issued.

In my work, I have been trying to define a number of functions that must belong to a map/record and that should be able to access other members of the same map/record to which these functions belong.

The experience was far from satisfying and here I describe the main problems I encountered when trying to use the =?> operator, and some obvious suggestions how we can further simplify the syntax for calling any member function of a map or record.

1. Problems trying to use the =?> operator

Here are the main problems I ran into.

Problem1. The =?> operator was:

  • weird-looking;
  • difficult to use;
  • difficult to understand;
  • difficult to read;
  • feeling unnatural.
    It would be much better if we didn't have to use any special operator at all in order to call a member function "myFunction" of a map $m by simply:
    $m?myFunction(<tuple of any arguments defined in the signature of the function>)

Problem2. There is no example, in the sections that describe the record type (3.2.8.3), showing a record member-function that accesses the values of other members of the same instance of the record.
Thus, the new feature is effectively hidden for people who want to work with records.
We need such an example for a record, so that we don't forget that any record is also a map and possesses all functionality a map has to offer. And a statement to this effect must be added to the description of records.

Problem 3. This syntax is overcomplicated and difficult to use and remember, resulting in unnecessarily long and complex expressions:

let $rectangle := {
  "width": 20,
  "height": 12,
  "area": fn($this) { $this?width * $this?height }
} 
return $rectangle =?> area()

It would be significantly better to use a much simplified syntax such as:

let $rectangle := {
  "width": 20,
  "height": 12,
  "area": fn() { ?width * ?height }
} 
return $rectangle ? area()

Recognizing that ?name is already used since XPath 3.1 as Unary Lookup Operator, and to avoid the unlikely case of collision, when a member function accesses other members of the map-owner-instance that happen to have identically the same names as expected constituents of the current context item (upon which the function is applied), we can introduce a special character to denote the current map-owner-instance, thus the above example could look like this:

let $rectangle := {
  "width": 20,
  "height": 12,
  "area": fn() { ^width * ^height }
} 
return $rectangle ? area()

Solutions

Solution for Problem 1 above (weirdness of the =?> operator:
Do not introduce any special operator. Just use ? to invoke the member-function.

Solution for Problem 2 above (lack of example of a record having a member-function that accesses other members of the same map-owner-instance).
Obviously, provide such an example. Also reiterate there that all features and functionality of a map continue to be available for records.

Solution for Problem 3 above (overcomplicated syntax:

  • Get rid of the =?> operator. Use ? for all references to member-functions.
  • Don't use any special variable like $this. For example, the current example in the documentation:
    "area": fn($this) { $this?width * $this?height }
    should instead be:
    "area": fn() { ^width * ^height }
  • use the ^ character to denote owner-map-instance membership. Thus ^width means: "The member named "width" of the map instance upon which the current function was invoked"

Conclusion

I will issue a PR with the solutions, provided there are not any substantial comments hilighting problems with this proposal.

@liamquin
Copy link
Contributor

if =?> could be defined to return the empty sequence if the LHS was the empty sequence, it’d be a lot like .? in JavaScript. Is that worth exploring?

@dnovatchev
Copy link
Contributor Author

if =?> could be defined to return the empty sequence if the LHS was the empty sequence, it’d be a lot like .? in JavaScript. Is that worth exploring?

@liamquin,

  1. You mean not =?> but ? - as when this issue has been fixed there will no longer exist a =?> operator.
  2. Why should we mask obvious errors? Trying to invoke a function that doesn't exist must be explicitly signaled to the programmer, not hidden intentionally.

We don't alter at all the current behavior of the ? operator.

@dnovatchev dnovatchev changed the title The =?> lookup arrow expression operator is weird, difficult to use, difficult to understand, difficult to read and unnatural **The =?> lookup arrow expression operator is weird, difficult to use, difficult to understand, difficult to read and unnatural** Feb 14, 2025
@dnovatchev dnovatchev changed the title **The =?> lookup arrow expression operator is weird, difficult to use, difficult to understand, difficult to read and unnatural** The =?> lookup arrow expression operator is weird, difficult to use, difficult to understand, difficult to read and unnatural Feb 14, 2025
@dnovatchev dnovatchev added Bug Something that doesn't work in the current specification XPath An issue related to XPath Enhancement A change or improvement to an existing feature Feature A change that introduces a new feature Discussion A discussion on a general topic. PR Pending A PR has been raised to resolve this issue Decluttering Cut out dead wood labels Feb 14, 2025
@ChristianGruen ChristianGruen pinned this issue Feb 14, 2025
@ChristianGruen ChristianGruen unpinned this issue Feb 14, 2025
@ChristianGruen
Copy link
Contributor

While I am generally not too fond of dropping features that we have already accepted in the past, I wouldn't mind getting rid of the particular =?> operator.

Many of the questions about method references had already been discussed in #916, so it might be valuable to (re)consider them in the envisaged PR. For example, what would be the result of the following expression?

let $a := { 'a': 1, 'f': fn() { ^a } }
let $b := { 'a': 2, 'f': $a?f }
return $b?f()

@dnovatchev
Copy link
Contributor Author

dnovatchev commented Feb 14, 2025

What would be the result of the following expression?

let $a := { 'a': 1, 'f': fn() { ^a } }
let $b := { 'a': 2, 'f': $a?f }
return $b?f()

This should have already been decided when publishing the PR for #916, thus, the same as the current equivalent:

let $a := { 'a': 1, 'f': fn($this) { $this?a } },
    $b := { 'a': 2, 'f': $a?f }
return $b =?> f()

Please, do note that this issue is only making the already agreed syntax of #916 more user-friendly, and any questions about the more intrinsic rationale of #916 do not belong here.

I remember that we decided that $this means the current owner of the member-function, against which it is executed - and in this example the call of f() is performed against $b. Thus I would expect the result to be 2.

$a?f is not a call, it is just a reference.

Unfortunately, the BaseX fiddle is once again inoperative so this cannot be verified:

Image

Update:

Now the fiddle is working and the result is as expected: 2
Link to the fiddle

Image

@ChristianGruen
Copy link
Contributor

I remember that we decided that $this means the current owner of the member-function, against which it is executed

We didn't decide any such thing. It was an idea in the #916 proposal, but not the final decision. The name of the $this parameter is completely arbitrary. Please read the conversations to get a full picture, or have a look at the existing spec.

any questions about the more intrinsic rationale of #916 do not belong here.

I am sorry they definitely belong here, and they need to be clarified in an upcoming PR. While the semantics of fn($this) { $this?a } is well defined, and while it can be parsed and evaluated completely without lookups or the lookup arrow expression, it remains to be defined how fn() { ^a } is supposed to work.

Unfortunately, the BaseX fiddle is once again inoperative so this cannot be verified:

Thanks, we will look into that. It is just a testbed; feel free to get the desktop version if you want to have a safe environment.

@michaelhkay
Copy link
Contributor

I concur that this operator has difficulties, and I don't think it would be an enormous loss to drop it. I would prefer to improve it if we can, but it might be that the only way to make it really usable is to move closer to full object orientation with classes, inheritance, and encapsulation.

It would certainly nice to be able to make $object ? area() work as the invocation syntax. The problem is that the proposed semantics for this rely on a very slippery concept called "the current map-owner-instance". Formalizing that concept and providing an unambiguous definition of what it means is the key to getting this right. I don't think it's impossible (it's closely related to what we are trying to achieve with pinned maps and labels) but it's not easy.

@dnovatchev
Copy link
Contributor Author

I concur that this operator has difficulties, and I don't think it would be an enormous loss to drop it.

Yes, and let us spend another 3 weeks doing the most important work of our lives: designing the "ordered map" ...

I would prefer to improve it if we can, but it might be that the only way to make it really usable is to move closer to full object orientation with classes, inheritance, and encapsulation.

We are very close to having a good formal description, without having to reinvent the OOP wheel.

It would certainly nice to be able to make $object ? area() work as the invocation syntax. The problem is that the proposed semantics for this rely on a very slippery concept called "the current map-owner-instance". Formalizing that concept and providing an unambiguous definition of what it means is the key to getting this right.

Correct.

I don't think it's impossible (it's closely related to what we are trying to achieve with pinned maps and labels) but it's not easy.

It is both possible, and not too-difficult, depending on one's definition of "difficult" and "easy".

Are there any problems that you see in the following formal definition?

Here is the formal definition:

Map Execution Context - MEC or MC:

The map execution context is the stack of maps on which function calls are being evaluated, and each of which is initiated with the following type of expression
$someMap ? <map-key>(<argument-tuple>).

As part of the evaluation of any such function call, depending on the body of the function, another, contained function call (of this same type) against a map can also be under evaluation, and within the evaluation of the body of this nested function call yet another function call (of this same type) against a map can also be under evaluation, and so on..., and so on.

Therefore, within the evaluation of an expression there is a stack (maybe empty) of such maps, function calls (of this same type) upon which are being evaluated.

Definition:

The Map Execution Context (MEC) is a part of the dynamic evaluation context. Its value is the stack of maps upon which function calls are being evaluated, each of these using the syntax $someMap ? <map-key>(<argument-tuple>) .

fn:get-map-execution-context()

Summary
Returns the map which is at the top of (the stack of) the Map Execution Context.

Signature

fn:get-map-execution-context() as map(*)?

Properties
This function is ·nondeterministic·, ·context-independent·, and ·focus-independent·.

Map Sibling Lookup operator ^

^name is formally defined as:
fn:get-map-execution-context()?name

Example

This code:

let $rectangle := {
  "width": 20,
  "height": 12,
  "area": fn() { ^width * ^height }
} return $rectangle?area()

is equivalent to:

let $rectangle := {
  "width": 20,
  "height": 12,
  "area": fn() { let $m := get-map-execution-context() return $m?width * $m?height }
} 

@michaelhkay
Copy link
Contributor

The map execution context is the stack of maps on which function calls are being evaluated, and each of which is initiated with the following type of expression
$someMap ? ().

I think this definition is problematic. It implies a loss of referential transparency: the expression $map?f($x) now has a different effect from let $fn := $map?f return $fn($x). XPath does have expressions that break referential transparency, specifically those expressions that change the dynamic context, but for this case I think it's difficult to isolate exactly what the relevant expressions are. Consider, for example $someMap ? * [1] (arguments)

I think it's possible to avoid this problem by relying on labels attached to function items. We can say (indeed, in particular circumstances it is already the case) that evaluating $map?f will be an item having the label {'parent':$map}. We can then say that the when evaluating a function item, the body of the function should have access to the function item's label and in particular to the parent property (which could perhaps be renamed "owner" or "container"). All that remains then is to define usable syntax for accessing this property: note that in another context, issue #1775 proposes using ".." for this. That would provide the syntax

let $rectangle := {
  "width": 20,
  "height": 12,
  "area": fn() { ..?width * ..?height }
} return $rectangle?area()

A complication is that we need to define very clearly under what circumstances a function item loses its label. For example, what happens when the function item is added to another map, or when the function item is partially applied. The existing spec for labelled items ought to cover this, but it needs careful checking.

@dnovatchev
Copy link
Contributor Author

dnovatchev commented Feb 16, 2025

The map execution context is the stack of maps on which function calls are being evaluated, and each of which is initiated with the following type of expression
$someMap ? ().

I think this definition is problematic. It implies a loss of referential transparency: the expression $map?f($x) now has a different effect from let $fn := $map?f return $fn($x). XPath does have expressions that break referential transparency, specifically those expressions that change the dynamic context, but for this case I think it's difficult to isolate exactly what the relevant expressions are. Consider, for example $someMap ? * [1] (arguments)

I am trying hard to understand this. What is the problem? The definition of Map Execution Context is precise and the rule is precise: for any function whose body contains a ^name expression, this expression will be expanded to:
get-map-execution-context()?name.

Also, I don't understand at all how this could be meaningful:

$someMap ? * [1] (arguments)

But still, for whatever function-member is selected for execution, the above rule still applies. What is the problem?

Cannot discuss "solutions to the problem", if it is not clear what the problem is.

And, btw, you quoted the rule incorrectly. The expression in it is not:

$someMap ? ()

But the expression in the rule is:

$someMap ? <map-key>(<argument-tuple>)

MHK> Sorry, that was a markdown tagging error.

@michaelhkay
Copy link
Contributor

The problem is that the way you have tried to define it, $map?f($x) now has a different effect from let $fn := $map?f return $fn($x), and in an expression based language, that is highly undesirable. Referential transparency requires that the result of an expression depends only on the values of its subexpressions, and not on the syntactic form of the subexpressions.

@dnovatchev
Copy link
Contributor Author

The problem is that the way you have tried to define it, $map?f($x) now has a different effect from let $fn := $map?f return $fn($x), and in an expression based language, that is highly undesirable. Referential transparency requires that the result of an expression depends only on the values of its subexpressions, and not on the syntactic form of the subexpressions.

Definitely not a problem if f(x) is not context dependent - the result from the call is the same.

And when f(x) is context dependent (as among other cases, from the Map-Execution Context), it is by definition that, depending on the context, a context-dependent function can produce different results. Even for different map execution contexts, f(x) has the same behavior, provided that get-map-execution-context() returns a map that has the keys that f(x) is referencing off it (off the map-execution-context) and the values for these keys are of compatible types.

@michaelhkay
Copy link
Contributor

michaelhkay commented Feb 16, 2025

You say

Definitely not a problem if f(x) is not context dependent - the result from the call is the same.

I don't understand that answer. You say that a map is only added to the stack by an expression whose syntactic form is $map?f($x), by implication it is not added if the syntactic form is let $fn := $map?f return $fn($x). So how can the result be the same?

You also say:

The definition of Map Execution Context is precise

I'm sorry, but it isn't. You define it as:

The Map Execution Context (MEC) is a part of the dynamic evaluation context. Its value is the stack of maps upon which function calls are being evaluated, each of these using the syntax $someMap ? <map-key>(<argument-tuple>)

There are lots of unanswered questions here. What exactly is <map-key>? Do other kinds of expression leave the stack unchanged, or do they start a new empty stack? What happens if <argument-tuple> contains another expression of this form - which evaluation goes onto the stack first? I think that with some effort one could find an answer to all those questions, but it's not there in your proposal.

And this still leaves the problem of referential transparency: the dynamic function call $F($ARGS) now has different results depending on the syntactic form of the expression that computes $F, which causes havoc for optimisers trying to rewrite expressions with their equivalents.

You wouldn't expect, for example, that $map?area() produces a different result from trace($map?area, 'label')(), but under your proposal it does so.
`

@dnovatchev
Copy link
Contributor Author

What if we dedicate a special operator for "method execution", but a simpler one than the current =?>, for example |> or ~>

Could we then instead of having to write:

let $rectangle := {
  "width": 20,
  "height": 12,
  "area": fn($this) { $this?width * $this?height }
} 

use the below as a lexical synonym:

let $rectangle := {
  "width": 20,
  "height": 12,
  "area": fn() { ^width * ^height }
} 

And cause the evaluation with:
$rectangle ~> area()


This is still better and more convenient
"area": fn() { ^width * ^height }

than the current specification's:
"area": fn($this) { $this?width * $this?height }

@michaelhkay
Copy link
Contributor

michaelhkay commented Feb 17, 2025

I propose the alternative solution:

  1. Drop the current =?> operator.
  2. Allow an inline function expression to be annotated %method. Definition: a method is a function item with the annotation %method.
  3. The static context for the body of a method is augmented with a variable $this of type map(*). No user-defined variable declared within a method may have this name.
  4. The dm:iterate-map() accessor, and all operations that depend on it (including map:get() and the lookup operator ?) , are enhanced so that when the selected value is a single method M, the captured context of M is enhanced with a non-local variable binding that binds the value of $this to the containing map. Note that this happens at the point where the method is selected from the map, not at the point when the method is invoked, and it happens regardless what operations are used to select the method.

What this means is that the person declaring a record type can write:

let $rectangle := {
  "width": 20,
  "height": 12,
  "area": %method fn() { $this?width * $this?height }
} 

and the person invoking the method can write $rectangle?area().

The benefits of this proposal are:

  • at the user level, the mechanism is simple and natural for both the person defining the method and the person invoking it.
  • it works regardless of the syntax used to select the method from the containing map
  • it makes extensive re-use of existing concepts, such as function annotations and captured context, rather than introducing new concepts
  • no enhancements are needed to the data model (other than a tweak to dm:iterate-map) or to the static or dynamic context
  • no changes are made to the grammar, in particular there are no new operators to remember

The main downside is that it involves a tweak to a low-level primitive operation, namely selecting an item from a map. But I think we can live with that. (An alternative would be to do this only for a subtype of maps which we could call objects. But introducing such a subtype would involve much more extensive changes, opening the door to additional requirements such as inheritance, object identity, and encapsulation.)

@dnovatchev
Copy link
Contributor Author

dnovatchev commented Feb 17, 2025

I propose the alternative solution:

  1. Drop the current =?> operator.
  2. Allow an inline function expression to be annotated %method. Definition: a method is a function item with the annotation %method.
  3. The static context for the body of a method is augmented with a variable $this of type map(*). No user-defined variable declared within a method may have this name.
  4. The dm:iterate-map() accessor, and all operations that depend on it (including map:get() and the lookup operator ?) , are enhanced so that when the selected value is a single method M, the captured context of M is enhanced with a non-local variable binding that binds the value of $this to the containing map. Note that this happens at the point where the method is selected from the map, not at the point when the method is invoked, and it happens regardless what operations are used to select the method.

What this means is that the person declaring a record type can write:

let $rectangle := {
  "width": 20,
  "height": 12,
  "area": %method fn() { $this?width * $this?height }
} 

and the person invoking the method can write $rectangle?area().

This is indeed a significant step forward.

As a next step, let us drop the %method annotation and have the static context for the body, of any function that is a member of the map, augmented with a variable $this of type map(*) . This simplifies the syntax further. Member-functions that do not intend to access other members of the map simply are not affected by the new variable that is added to their static context.

Then, finally, let us have a rule that ^name is a synonym for $this?name.

Then the above example will be written as:

let $rectangle := {
  "width": 20,
  "height": 12,
  "area": fn() { ^width * ^height }
} 

and the caller invokes the function: $rectangle?area().

@michaelhkay
Copy link
Contributor

... static context for the body, of any function that is a member of the map ...

A function is not a member of a map at the point it is created; it is created first, and then added to the map later. That's why identifying the function as one that qualifies for this treatment has to be part of the expression that constructs the function, not part of the expression that adds it to a map.

@dnovatchev
Copy link
Contributor Author

dnovatchev commented Feb 17, 2025

... static context for the body, of any function that is a member of the map ...

A function is not a member of a map at the point it is created; it is created first, and then added to the map later. That's why identifying the function as one that qualifies for this treatment has to be part of the expression that constructs the function, not part of the expression that adds it to a map.

Sorry, I totally don't understand this.

let $rectangle := {
  "width": 20,
  "height": 12,
  "area": fn() { ^width * ^height }
} 

The above function fn() { ^width * ^height } is created as part of the map creation, not after this map's creation.

A function can never be "added to the map" - because maps (as everything in XPath) are immutable.

"Adding a function, or any new member, to a map" creates a new map, and the function is associated with the new map at the point of this new map's creation.

Isn't this true?

@ChristianGruen
Copy link
Contributor

It is ironic to see that this discussion is basically a sequel of what was initially proposed in #916 (👍).

The above function fn() { ^width * ^height } is created as part of the map creation, not after this map's creation.

Note that the function is created before it possibly becomes part of a map. It could also be declared outside the map:

let $f := fn() { map:size($this) }
return { 'size': $f }

But it’s true, maybe the static context of a function body could implicitly be augmented with $this if a function item becomes part of a map. This step could possibly be skipped if a smart compiler detects that $this is not referenced in the function body.

I would still have some sympathy for an fn:this function, to avoid variables without explicit declaration. It has happened more than once that I was asked how $err:description can be “renamed” – which obviously makes no sense, as it is created automagically inside catch clauses.

@dnovatchev
Copy link
Contributor Author

But it’s true, maybe the static context of a function body could implicitly be augmented with $this if a function item becomes part of a map. This step could possibly be skipped if a smart compiler detects that $this is not referenced in the function body.

💯
Yes, the function $f remains the same, but the pairs (function-context1, $f) and (function-context2, $f) are different.

I would still have some sympathy for an fn:this function, to avoid variables without explicit declaration. It has happened more than once that I was asked how $err:description can be “renamed” – which obviously makes no sense, as it is created automagically inside catch clauses.

Yes, I prefer this to be a function, and actually don't care too much, provided that this function is only called behind the scenes and the user doesn't have to reference it explicitly.

@ChristianGruen
Copy link
Contributor

Yes, I prefer this to be a function, and actually don't care too much, provided that this function is only called behind the scenes and the user doesn't have to reference it explicitly.

I think there are many cases in which users will want to do more than just referencing map entries. Next, it will certainly not be obvious to everyone what ^width means.

If we come to the conclusion that we need new syntax, .width would be one more possibility.

@dnovatchev
Copy link
Contributor Author

I think there are many cases in which users will want to do more than just referencing map entries. Next, it will certainly not be obvious to everyone what ^width means.

If we come to the conclusion that we need new syntax, .width would be one more possibility.

. has become so heavily overloaded with all possible meanings, that I prefer not to see . in an expression... 😠

This aside, I am glad we kinda agree on the important things here.

If we come to the conclusion that we need new syntax, ...

Well, I have the advantage to be just a user, not an implementor. So, yes, users need the simplest possible forms of expressing the reference to sibling-members.

@michaelhkay
Copy link
Contributor

The above function fn() { ^width * ^height } is created as part of the map creation, not after this map's creation.

Subexpressions are conceptually evaluated first, and the resulting values are then used in the evaluation of the containing expression. Unless the containing expression defines a custom static or dynamic context for evaluation of its subexpressions, the evaluation of the subexpression takes no account of where it appears. It would be possible to say that a map initializer sets a special static context for its operands that makes any function automatically behave as a method, but I don't think that would be a good idea, mainly because I think it's better if the way you construct the map and the way you construct the function item are completely orthogonal to each other - orthogonality in language design is a good thing to aim for.

@michaelhkay
Copy link
Contributor

Yes, I prefer this to be a function

We have all the machinery for adding variables locally to the static context and for binding values of those variables to the captured context of a function item. We don't have corresponding machinery available if it were a function. It could be done, but would involve a lot more complex underpinning.

Plus, this is a variable (or pseudo-variable) referring to the "target object" in many programming languages that our users are likely to be familiar with, for example Javascript and C#. Why be different?

@dnovatchev
Copy link
Contributor Author

The above function fn() { ^width * ^height } is created as part of the map creation, not after this map's creation.

Subexpressions are conceptually evaluated first, and the resulting values are then used in the evaluation of the containing expression. Unless the containing expression defines a custom static or dynamic context for evaluation of its subexpressions, the evaluation of the subexpression takes no account of where it appears. It would be possible to say that a map initializer sets a special static context for its operands that makes any function automatically behave as a method, but I don't think that would be a good idea, mainly because I think it's better if the way you construct the map and the way you construct the function item are completely orthogonal to each other - orthogonality in language design is a good thing to aim for.

We already saw, in a previous comment, that:

The function $f remains the same, but the pairs (function-context1, $f) and (function-context2, $f) are different.

The function is not changed in any way, but the exact context in which it is executed is different in these different cases.

As for orthogonality, the pair (function-context, $f) is like the 2D coordinates of a point on a plane, it is an established, fundamental principle that the two axes X and Y are orthogonal.

@dnovatchev
Copy link
Contributor Author

Yes, I prefer this to be a function

We have all the machinery for adding variables locally to the static context and for binding values of those variables to the captured context of a function item. We don't have corresponding machinery available if it were a function. It could be done, but would involve a lot more complex underpinning.

Plus, this is a variable (or pseudo-variable) referring to the "target object" in many programming languages that our users are likely to be familiar with, for example Javascript and C#. Why be different?

My personal opinion is that each of: this variable, or this() function are a perfectly good choice, as long as it is used "behind the scenes" and the user is not forced (but may, if he wishes so) to refer to this explicitly.

@michaelhkay
Copy link
Contributor

@ChristianGruen wrote:

But it’s true, maybe the static context of a function body could implicitly be augmented with $this if a function item becomes part of a map.

A function item "becoming part of a map" is surely something that happens dynamically, so how can it change the static context?

@dnovatchev wrote:

The function $f remains the same, but the pairs (function-context1, $f) and (function-context2, $f) are different.

I'm not at all sure what was intended by that statement.

Note that in my proposal the function/method is not changed in any way at the time it is added to the map. The captured context of the function (specifically, the binding of $this) is established at the time the function is retrieved from the map. (the process is essentially the same as partial evaluation). That's absolutely essential, because at that time it's typically a different map, and the $this that is evaluated when the function is evaluated must be the map that the function was retrieved from, not some earlier map that it was originally added to:

let $rectangle := {
  "width": 20,
  "height": 12,
  "area": %method fn() { $this?width * $this?height }
} 
let $rectangle2 := map:put($rectangle, 'width', 3)
return $rectangle2?area()

returns 36.

@dnovatchev
Copy link
Contributor Author

dnovatchev commented Feb 17, 2025

@dnovatchev wrote:

The function $f remains the same, but the pairs (function-context1, $f) and (function-context2, $f) are different.

I'm not at all sure what was intended by that statement.

Note that in my proposal the function/method is not changed in any way at the time it is added to the map. The captured context of the function (specifically, the binding of $this) is established at the time the function is retrieved from the map. (the process is essentially the same as partial evaluation). That's absolutely essential, because at that time it's typically a different map, and the $this that is evaluated when the function is evaluated must be the map that the function was retrieved from, not some earlier map that it was originally added to:

let $rectangle := {
  "width": 20,
  "height": 12,
  "area": %method fn() { $this?width * $this?height }
} 
let $rectangle2 := map:put($rectangle, 'width', 3)
return $rectangle2?area()

returns 36.

Exactly!

The functions: $rectangle?area and $rectangle2?area are exactly the same (identical), but their contexts are different - $this is equal to $rectangle in the first case, and $this is equal to $rectangle2 in the second case.

Thus the pairs (function-context1, $area) and (function-context2, $area) are different, while in both cases $area contains the same function.

The results of the two function calls to the same function and with the same (empty) arguments-tuple values - these results are different, because the contexts (and specifically the value of the $this variable) are different in the two cases.

( Alternatively, one could argue that $rectangle?area and $rectangle2?area are in fact two different functions, if we regard them as partial applications, where the same argument ($this) has been fixed with two different values, or alternatively - as closures that have different context. All these interpretations are equivalent, so let us not argue which of them "is the right one"... We are all saying the same thing, but expressing it differently... 😄 )

Anyway, we all agree that a map value that is a function has a context that includes the special variable $this. Then this is the rational upon which to simplify the definition of referring to map-siblings from a map member-function.

Can we move forward to presenting this in the documentation?

@ChristianGruen
Copy link
Contributor

A function item "becoming part of a map" is surely something that happens dynamically, so how can it change the static context?

Thanks, nonsense the way I wrote it (vacation standby mode). I had catch clause variables in mind, which are bound later on.

A hardcoded %method annotation will certainly simplify the implementer’s life. From the user point of view, it feels superfluous: the %method/$this pair seems redundant. It would be more user-friendly to let the processor find out whether a $this reference exists. Similarly, a processor does not have to bind $err variables if they are not referenced within a catch clause.

@dnovatchev
Copy link
Contributor Author

dnovatchev commented Feb 17, 2025

A function item "becoming part of a map" is surely something that happens dynamically, so how can it change the static context?

Thanks, nonsense the way I wrote it (vacation standby mode). I had catch clause variables in mind, which are bound later on.

A hardcoded %method annotation will certainly simplify the implementer’s life. From the user point of view, it feels superfluous: the %method/$this pair seems redundant. It would be more user-friendly to let the processor find out whether a $this reference exists. Similarly, a processor does not have to bind $err variables if they are not referenced within a catch clause.

👍 🥇 ❗ 💯 🍦

@michaelhkay
Copy link
Contributor

I'm currently thinking about the problem: what happens when a method (a function item annotated as %method) is read from one map and added to another?

This happens implicitly when you do map:pairs(). It also happens implicitly when you do map:remove(). In the first case you don't really want $this re-bound to the KVP record; In the second case you do want it re-bound to the new map created by map:remove().

I think the cleanest solution is probably to say that the binding of $this only happens when you access the function using the lookup operator ?. If you get the function from the map any other way, for example using map:for-each(), then $this remains unbound (and the function therefore remains pro tem unusable).

This does mean that map:pairs($m)[?key="x"]?value will return a function item in which $this is bound to the key-value-pair, and not to $m. I think we can live with that. But it's a reminder of the one advantage of the status quo design: using a different operator such as =?> makes it quite unambiguous that this is the operation that binds the variable to a specific map. Using ? makes simple cases simple, which is always a good thing, but it also hides some semantic complexity which can bite you if you haven't mastered the detail.

It still leaves the question of what happens when a method is bound to a particular map and is then added to another map: for example

let $rect1 := {'x':1, 'y':2, 'area': %method fn() {$this -> ?x * ?y}}
let $rect2 := {'x':2, 'y':3, 'area': $rect1?area}
return $rect2?area()

We could define this one either way, and I don't think there's an obviously right answer. We could also perhaps make it an error (the expression $rect2?area fails because it attempts to bind $this, but $this has already been bound).

@ChristianGruen
Copy link
Contributor

Wow, so much to digest just after some days of absence.

I think the cleanest solution is probably to say that the binding of $this only happens when you access the function using the lookup operator ?.

My experience is that many people prefer to write $map('X') instead of $map?X, so maybe we should (try to) treat function calls and map:get equally.

From the implementer perspective, if possible, we use one implementation for all variants by rewriting 3.1 lookup operator constructs:

INPUT?(KEY)    →  INPUT(KEY)
INPUTS?(KEY)   →  INPUTS ! .(KEY)
INPUT?(KEYS)   →  KEYS ! INPUT(.)
INPUTS?(KEYS)  →  for $item in INPUTS return KEYS ! $item(.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug Something that doesn't work in the current specification Decluttering Cut out dead wood Discussion A discussion on a general topic. Enhancement A change or improvement to an existing feature Feature A change that introduces a new feature PR Pending A PR has been raised to resolve this issue XPath An issue related to XPath
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants