How to discover functions which apply to a given object?

The solution by @WschW is very clever and displays the nice introspection facilites of Julia, but as clarified later, it breaks inference.

As @DNF suggested, this is primarily a problem for tooling. Also, to use Julia productively, it is best to realize that it isn’t Python, and should not be remade in the image of the latter. When learning a new language, I think it is best to learn it as is for about a year or two (or, alternatively, 100k LOC), and save language redesign for later.

10 Likes

It doesn’t, but I was arguing that object.method syntax is useful for other reasons. Here are my reasons in support of such a syntax:

1. There are many instances when it is easier to read / write in the reverse order.
Consider

eigenvalues(laplacian(transitive_reduction(fill_from(A, edge_list))))

vs.

A.fill_from(edge_list).transitive_reduction().laplacian().eigenvalues()

The very existence of piping serves as a testament that there are instances where it is easier to put it in the reverse order, but while

fill_from(A,edge_list) |> transitive_reduction |> laplacian |> eigenvalues

works when the functions only take a single argument, it requires ugly and noisy syntax for anything else, for example

fill_from(A,edge_list) |> x -> connected_components(x, 3)

2. Helps with conceptual flow.
Perhaps this is something which can be developed in time, however if you have object x and you want to pass it through a set of transformations which will eventually return some object y, it seems intrinsically easier to start from something that exists rather than envisioning what object y might be, what the last transformation you would apply would be, then the next-to-last, and so forth.

3. Provides a natural way to narrow scope and discover functionality via tab completion.
Would it be possible to come up with something such as A<ctrl+.> (as you suggested) to expand a list of methods which take A as a parameter? Sure. Would it be natural to use it this way? Not really, because you’re writing something (the object name) which isn’t actually in the position it would end up. The code would have to be flipped around and turned into function(A).

Part of the reason tab completion is so nice it that it is a completion. You are still writing the code the way you will eventually see it appear, if you get part way through but then realize you forgot the spelling of the method you want to call then you can tap tab and it will just fill out the rest, you can use partial names to reduce the number of suggested functions, etc. etc. If I was trying to get the connected components of a graph and I forgot if the function was connected_components or conn_comp, with code completion I would write A.conn, realize I didn’t remember the spelling, and then just hit <tab> and it would just fill out the rest. With your proposed method I guess I would have to type A<ctrl+.> and then maybe an interface would appear where I could start narrowing down the choices? But the point is that this is a totally different order of typing, and I would have to had consciously made a decision to use the function suggestion tooling rather from the start.


I would like to flip the question around - what disadvantage does UFCS have? You don’t have to use it. It is simply syntactic sugar, it doesn’t make Julia more or less object oriented. If Julia had UFCS, would you be asking for it to be removed?

In most respects, I agree. However, with aspects related to ease of learning, it’s easy to forget the aspects which were challenging at the start once you are proficient. Furthermore, once you are proficient, you don’t care as much about those aspects which would make it easier for others. In that sense, I think focus on these types of issues when learning a language is valuable.

Perhaps you misunderstood something — methodswith and similar introspection facilities are useful for all programmers to Julia, regardless of how experienced they are.

You are arguing for an introduction of a parallel facility. Why not use what you already have available? Also, note that interfacing with this functionality is a separate issue, and you should not have to hijack the property API for it.

EDIT: also, if you have a proposal for extending/changing the language, please open a new topic; the First steps category is not the best place for it.

For my purposes, methodswith is not all that useful. For example, for a graph A then methodswith(typeof(A)) returned 0 methods, and methodswith(typeof(A), supertypes=true) returned 242 methods. InteractiveCodeSearch already provides a more usable way to interface with it, the only disadvantage is that it needs to be kicked off by typing some code such as @searchmethods typeof(A) which, again, is a separate action from just writing the code. Sure, this might be made slightly easier by adapting the tooling to kick this off by typing A<ctrl+.>, but the point is that you still need to have decided beforehand that “I’m going to search for a function now” rather than just writing code, which is what completion allows.

My point is that Julia’s syntax is inherently prohibitive of this sort of completion. No matter what fancy interfacing or tooling that is laid on top of it, you will never be able to simply write some partial function name and then complete it with type awareness because the functions always have to come first.

I am not sure about this, at the REPL one could look up completions for

f(arg1, arg2, ...)

with TAB if the cursor is on the f, and restrict to the applicable names. It just just has not been implemented (yet, AFAIK, I am not aware of an issue either). It would make a nice PR if you are interested.

1 Like

Sure, but I wouldn’t call that completion. (It’s similar to the tuple completion mentioned by @jonathanBieler above.) The point is you are not writing it in the same order as you would have written the resulting code, and that can never be the case with Julia because the function always comes first.

Sorry but I don’t understand. How else would you write functions than f(arg1, arg2, ...)? Except for infix operators, of course.

With UFCS, the following are equivalent:

arg1.f(arg2,...)
f(arg1, arg2, ...)

The first object of a function is often the “main” argument, and that often is enough to narrow the scope of potential functions to a short list.

Again, you don’t have to write things this way at all. I haven’t really heard an argument for not allowing this.

That’s not how computer languages should be designed — rather than including features because there are no arguments against them, it is better to extend the language when there is an argument for something.

That said, there are at least 3 a compelling arguments against the syntax you are proposing:

  1. the first argument is nothing special in Julia — in fact, it is even less special than in other languages with multiple dispatch, eg Common Lisp which would use order to resolve ambiguity.
  2. it would conflate the syntax for getproperty with function call syntax
  3. it is unclear how modules paths fit into this scheme — how would you translate eg Statistics.quantile(xs, ps)?

Personally, I doubt that UFCS fits naturally in Julia, but if you seriously want to argue for it, please do make a clear proposal in a separate topic. Again, First steps is hardly the place for this.

3 Likes

OK, if I want to push further for it I will consider making a proposal for it elsewhere. To address the arguments though:

While the language may not place special emphasis on the first argument, humans still do. Again, I’m not suggesting that this is a complete solution that is elegant and perfect in every use case, but it is amazingly useful in the other languages which have it.

There’s no ambiguity though - it tries getproperty and if it can’t find anything it tries a function. It’s not the only way to call a function, so if you happend to have a name conflict you just write the function the other way. Other languages (D, Nim) handle this exactly the same way.

If you really wanted to, xs.Statistics.quantile(ps), however it seems that Julia encourages pulling everything into the global namespace anyway so this wouldn’t happen that much.

Again, it’s meant as an option to use when it makes “grammatical” sense. It’s not meant to be the only way to apply a function, and it’s not meant to provide perfect discoverability, but it’s a fair bit better than what is possible now. It’s obvious these “grammatical” order differences are important, otherwise the pipe operator wouldn’t exist.

I’m not wed to the dot operator, it just feels natural because that’s what I’m used to. The pipe operator is promising, but currently limited to single-argument functions only which greatly neuters it’s usefulness. I would be perfectly happy if something like curry underscoring is implemented, as it would also allow true completion (i.e. typing your code in the same order whether you use completion or not). It is even more general than UFCS, at the expense of 5 extra characters of syntax. For example,

# Original method call:
connected_components(fill_from(A, edge_list), k)

# UFCS:
A.fill_from(edge_list).connected_components(k)

# Curry Underscoring Pipe:
A |> fill_from(_, edge_list) |> connected_components(_, k)

# Possible with Curry Underscoring, but not with UFCS:
some_array |> map(x->x^2, _)

# Splats are hopefully possible with Curry Underscoring?
C = (A_set, B_set)
C |> union(_...)

# Not sure about changing the order, for example
C |> func(_[2], _[1])
# but it doesn't have to be infinitely flexible,
# even UFCS provides a really decent level of flexibility.

Despite the noisier / cumbersome syntax I think I’m coming around on some sort of curry underscoring approach. It would work pretty well with completion, typing A |> <tab> could list all the methods which could take A in their input, auto-completing could even place the underscore where it should go based on type (if it was unambiguous). Limiting this auto-complete would be a challenge still, but it’s potentially progress.

@mboratko in case you haven’t yet, I highly recommend you to read the topic My mental load using Julia is much higher than in Python. It raises several points that are similar to yours, and has some quite insightful comments.

(I particularly liked this comment by @rdeits on that thread.)

2 Likes

Yes, that thread didn’t pop up in the related items at first but once this thread started rolling I noticed it linked (win for machine learning). I definitely share your sentiment regarding the mental load, as well as feeling overwhelmed by the seemingly open-ended task of selecting a function from all possible functions instead of getting the “narrowing context” effect I feel in Python.

This github issue and the related ones to it were also relevant.

Something that became clear to me during the discussion in this thread is the way that language syntax interacts with completion. In this way, I claim it is not just a question of tooling. It is also not, as suggested by one of the other responses to your thread, a OO vs functional paradigm shift as demonstrated by the highly functional proposals above.

For me, autocomplete is much more than just a convenience, and having good autocomplete is absolutely essential for any language designed for scripting and rapid prototyping. I appreciate the discussion about it here, it has helped me formalize the abstract properties necessary to be able to create a fluid autocomplete interface.

3 Likes

Even feeling a bit ashamed for reviving that topic, I want to refine the previously mentioned solution for tooling that might even be able to streamline both requirements (universal language & maximum discoverability).
If we formally do tab replacement instead of completion we get a lot of possibilities. The following dot notation of course is motivated by OO history and could be replaced by anything the usecase/tooling/user combination might want. Even a preference setting in tooling could be made of it.

A = Graph()
A.<some complete key>
#Showing propertynames(A) marked as properties and showing functions that return on methodswith(typeof(A)) marked as such
#selecting a function resolves to
fun(A,<cursor>[, other args])

That would increase discoverability while (in the dot case) keeping the property api clean. It even helps newcomers remembering the function names and patterns used in Julia as it transforms the style used for discoverability into the style used in Julia.

Following that road down, tab suggestions can even be further refined and tuned (e.g., for supporting functional style methods, aka, comparing the type to the first non-function argument for proper suggestion of filter & map etc). Even further down the road this can take traits into account.

In my opinion discoverability would be best to stay orthogonal to code design since the former mainly depends on your own history and previous type of programming while the latter IMO is a “feature” of Julia. This maintains maximum flexibility which again allows tooling to streamline both traits without sacrificing universality. That allows different tooling designed for different use cases based upon the same universal language, Julia. => Make a DSL by tooling/restricting suggestions rather than by language design restrictions.

As a more concrete example I like to compare that to the eclipse completion where you can cycle through different suggestion modes. In our case the first iteration could look like “only propertynames or only function suggestions(->methodswith)”. In the next iteration we could add some mixed approach. And in a further advanced iteration we might even add some sort of AI to predicting the most favourable completion. Each of those stay entirely independent because the language allows a fairly universal representation with some (focused but basic/generic and type stable) syntactic sugar.

In the end everyone who wants to can simply hook their own completion script into the suggestion by simply providing a function that completes the input in front of any configured trigger event. When we stay at the same universal language we can support maximum diversity at the same time as maximum compatibility within the code.

I know, you already brought these points and for me it sounds like some sort of utopia but making utopia real is the designated purpose of Julia (Just remember the original blogpost “Why we created Julia”).
And btw, I’m pretty sure underscore currying will come. There is enough value and reoccuring interest in it that it will make it into the language. Hopefully even before 2.0. But even if not, 2.0 will be a very astonishing release anyway (Still hoping for Invariant Tuples)

2 Likes

Welcome to the Julia community @mboratko! It is great to see people coming with extensive experience from other programming languages and tools. Notice that in the statement above you are assuming that the ideal world consists of object-oriented syntax x.foo. Consider the other case where people think first in terms of verbs and APIs, and then put the objects inside no matter their type. That is the power of functional programming. I usually don’t think what I can do with an object x in particular, I generalize/standardize common operations to work on any object of that “kind”, and proceed with code that looks generic like filter(f, x), map(f, x), … You don’t even need to know the type of the current object x if your code is well-designed and generic in this approach. While programming with Julia you will see that we hardly need to look up APIs because the devs already made their homework to make their types generic according to type traits. They work out of the box with functional style and there is a common set of verbs that you will get used to with time.

One of the nightmares I always had with the object-oriented paradigm was this nightmare that every type and object had a very specific, different set of verbs that I needed to type explicitly in code. That meant that my code never accepted other object types. It was never fully generic.

I do believe, however, that we need tooling for discovery of specific functions from a type/object. But don’t be surprised if someone comes up with an even nicer idea to do this in the near future. Tooling is improving with time now that the language is settling down in the 1.x era.

Ah damn, I guess I should’ve posted a big fat banner, that I revived the thread :thinking:
TL;DR: I guess he’s not new anymore :smiley:

It looks like 1.8 will support better discoverability via tab-complete of (x, y): https://github.com/JuliaLang/julia/pull/38791

2 Likes