The problem
Imagine a simple pointer class:
1 2 3 4 5 6 7 8 9 | package A [initCtor] datatype Ptr(type: Type) using ValueType = type _ptr: @ValueType [protected] fun get(this: Ptr) = _ptr |
Each time the user would want to use this, the code would look something similar to:
1 2 3 4 5 6 7 8 9 10 11 12 13 | [initCtor] datatype MyObj x: Int fun print(this: @MyObj) cout << 'MyObj.print: ' << x << endl fun test var o: MyObj = 0 var p: Ptr(MyObj) = o p.get().x = 30 p.get().print p.get print |
You can see that the extra `get()` is annoying.
Previously, we've implemented a hack with the `->` operator. This would be implemented like a macro in Sparrow. The usage code would look like:
1 2 | p->x = 30 p->print |
Things are better, but still not ok. From an usability point of view, we have to use a completely different operator, which is not ideal. But the biggest problems come from what the compiler needs to do.
In the previous implementation `->` was a macro that would translate `a -> b` into `a __dot__ get __fapp__ () __dot__ b`, where `__dot__` and `__fapp__` corresponds to a dot and a function application -- that means, the end result would look like `a.get().b`. We used these `__dot__` and `__fapp__` infix operators so that we can properly implement `->` precedence.
The bad part comes from the fact that each time the user uses a compound expression (`a.b`) the compiler would use a `__dot__` infix operators, and would perform all the steps in the operator selector. The same for function applications. Needless to say, that this added more complexity on the compiler side.
Dot operator to the rescue
From the usability point of view, the solution can be much simpler: the user has to define the following function near `Ptr`:
1 | fun .(this: Ptr) = _ptr |
with this defined, the usage becomes simpler:
1 2 3 | p.x = 30 p print p.print |
Extremely simple. The pointer doesn't actually interfere with the usage. This works well with operator selection and with compound expressions. And moreover, for code that doesn't use it, it won't slowdown the compilation.
How does it work?
First, let's discuss about operator selection. In the above example, this is the `p print` line.
For handling the dot operator we just add an extra fallback at the end of the process. If the current searches (inside datatype, near datatype, from current context) didn't find the proper name that would make a successful overload selection (i.e., cannot find a function that can be called), then we attempt to search for the dot operator. If we find the dot operator, then we will attempt to find a match, but this time searching from the resulting type of the dot operator. The final code would look something like `(p .) print` (except the fact that it's illegal to use `.` as a postfix operator in Sparrow).
The process works similarly for compound expressions (`p.x` and `p.print` in our example). We perform the search for names. If no names are found, then we fallback to searching for the dot operator, and if found, make the search from the resulting type.
There is a small difference however between the two use cases. In the case for compound expressions we don't have the overloading procedure to tell us whether the match is good or not; we can only rely on the search. So, if the search is successful, we won't be using the dot operator. This is in accordance with the idea of not changing existing behavior, but it can lead to some surprises. If, for example, the `Ptr` and the `print` functions were in the same package, dot operator would not be applied.
I believe that the dot operator is a nice addition to Sparrow. It makes the code cleaner, and it relieves the implementation of some hacks that would also affect compilation time.