[ANN] DotCall.jl OOP-like dot notation

DotCall.jl lets you call methods as x.f() rather than f(x). It was registered a few weeks ago.

This is an old (2 or 3 years) package, with a new, more descriptive name. And a couple of minor fixes.

[EDIT: simplified example in originial post]

julia> using DotCall;
julia> DotCall.@dotcallify Vector (map=(v, f) -> map(f, v),);
julia> [-1,1].map(abs)
2-element Vector{Int64}:
 1
 1

This package is related to a lot of the discussion in the OOP-Like Dot Notation post. But it’s probably not the best solution if your main concern is discoverability and would like it to be applied everywhere possible.

I haven’t found time recently to explore it, for example chained calls, as much as I would like.

11 Likes

Why is @eval needed in your examples?

1 Like

Turns out it’s not (at least in Julia 1.11). I did run into examples a few weeks ago that did not work until I used @eval. But this is not one of them. I’m a bit puzzled because it took me a while to find a solution (to whatever it was) using @eval. And I pulled the example from my REPL history.

I’ve edited the original post above to remove @eval.

DotCall.jl was somewhat complicated by the time it got the desired behavior. I might re-read it and add some comments. The development worked like this:

I was mentoring someone who wanted to use this oop dot call thing (I wish there were a good word for it) with a struct. Their solution was like this:

julia> struct A f end;
julia> a = A(() -> 1);
julia> a.f()
1

Except the struct was mutable and had a lot of data fields and maybe 20 functions x, y, z, cx, etc. It was a stripped-down translation of a Python class defined over 6500 LOC.

Among the disadvantages to this approach are: The runtime performance of calling methods this way was very slow. And each instance has to store at least an alias to the function.

My first attempt at DotCall worked ok. But calling methods was not performant (maybe because of runtime dispatch). The current version of DotCall has these advantanges:

  • If a method and a struct are defined in a module, the method can be called without importing the method. For example you can call a.x(), without importing x.
  • There is no runtime penalty
  • TAB completion works. So you get this basic level of discoverability.
1 Like

In case it’s not obvious, mucking with Vector this way is good for experimenting, but is dangerous.

For example, If you make a PR to say, OrdinaryDiffEq.jl, with this

DotCall.@dotcallify Vector (map=(v, f) -> map(f, v),);

you’ll have a (very) hard time getting it merged.

I think the “official” term which looks appropriate here would be

1 Like

The problem is that it’s not uniform here. It’s not uniform in c++ or Python, either.

Maybe “Nonuniform Function call Syntax”. Yeah that’s it. NUFCS — “nuffcks”

1 Like

As far as I understand from the Wikipedia article, the “uniform” stands for that the notation a.foo(b) can be used for all functions, i.e. not only methods which are defined in classes/objects, but also free (global) functions. For that latter case this syntax is converted to foo(a, b). This is indeed not supported by C++ or Pyhton. And frankly Julia doesn’t have member methods in the OOP sense, but it looks like this syntax conversion is exactly what you do in DotCall.jl (no intention to debate your name choice).

1 Like

I understood “uniform” to mean all functions get this out of the box, like in Nim. Not only those that you choose.

But the discussions around C++ seem to agree with how you are interpreting UFCS:

proposed a Uniform Calling Syntax (UCS), allowing specially annotated free functions to be called with member function notation

From the beginning, I discarded using UFCS as a term to even talk about this because I thought “everywhere, all the time” was a necessary condition. But I can see why you don’t suggest changing the package name. UFCS.jl is pretty bad.

Yeah I agree it’s somewhat ambiguous. I haven’t looked too close at your package yet, but have you considered to implement it that way, i.e. the user puts only using DotCall at the top of their package/script and it would work automatically for all functions? That would probably be better suited to be implemented in a separate new package then, but I imagine that it would be simpler from the usage perspective. I can’t think of a good use case where you want to make this syntax possible only for some functions. But if I understand your comment [ANN] DotCall.jl OOP-like dot notation - #4 by jlapeyre correctly, it might be bad or not possible in all cases?


As the primary advantage is the autocompletion when typing the dot, I guess the next step could be to push for support in the language server. There already seems to be a related PR at dot methods completion by xgdgsc · Pull Request #1210 · julia-vscode/LanguageServer.jl · GitHub which even automatically does the syntax conversion, but it is dying a slow death without any reviews.

This is not something I’d work on implementing at the moment.

  • If you do this to functions outside your module it’s piracy, at least in spirit, and probably in letter.
  • For example, to dotcallify map(f, ::MyStruct) I rewrite (if I recall correctly!) some functions dealing with properties and fields for map.
  • I’d need logic (reliable logic) to determine where functions and types are defined to know where I can and cannot apply it.
  • Currently doing dotcall for map(f, ::AbstractVector) causes a segfault. I’m pretty sure it’s a bug in DotCall that’s not hard to fix. But there will others; they could be serious as well.
  • I make a choice that, for map, the second argument is special, not the first. I think getting a heuristic for deciding this would be difficult.
  • There are some big recompilation (invalidation probably) penalites. This could perhaps be mitigated. Even if it can be, it likely would take some work. I haven’t measured how much of a problem this is in the context of a package.

I’m thinking DotCall could be useful for trying ideas, or the ergonomics of chaining method calls. Or similar things. Or maybe it would make it easier to use a particular type in a particular package. At the moment, it doesn’t seem much of a burden to use (compared to all the magic you’d have to implement to make it automatic). If people really wanted to use it in packages, then sure it would be worth considering.

I think the namespace advantage is a big one. But I don’t like the idea of importing tons of symbols willy-nilly. I’m sure many people don’t mind that using APackage imports function x. Just avoid naming other things x.

My guess is that if the only thing you want is discoverability and completion and things like that, it’s better to put that in the LanguageServer or similar tooling. And don’t mess with the semantics of the language itself. Of course you’d also want to implement something for the REPL as well.

OTOH, an advantage of DotCall is that you don’t do x.af[TAB] to get methods on x that start with af only to have it rewritten into your code as afunc(x). This is apparently what the PR you referenced does.

EDIT: You could possibly do tricks like define a new function named map in your module and have it call Base.map. This might avoid piracy and possible long-range bugs. This would be opaque to the user.

R calls this form of OOP “Reference Classes