Allowing the object.method(args...) syntax as an alias for method(object, args ...)

I agree that the shortest symbols should be reserved for operations that are fundamental, and that those symbols should offer a visual mnemonic of their function. The clarity of a symbol is always dependent on the audience (e.g. Julia users know that . means broadcast; Mandarin users know that 地图 means map), but if you have too many then they’re hard to remember. > is a good example of a symbol that’s fundamental and mnemonic. A chaining operator may be sufficiently fundamental as well, probably some more experimentation is needed.

Yes, of course, clarity is relative, not only to people, but also to conventions of programming languages. In pretty much any language that accepts the syntax x.f(y), it means taking a field f from object x and applying it to y. In Python, this field is also called method, which is a function in which x is curried as its first argument, but that is detail. E.g.:

# python3
Python 3.9.5 (default, May 11 2021, 08:20:37) 
>>> func = "abcdefghij".replace
>>> func('a', '1')
'1bcdefghij'

So, the syntax the OP is proposing would be confusing to Julian users and to newcomers. Because, in Julia, methods are not fields and Strings don’t have a field called replace.

Now, the syntax x⤷f(y) is not confusing and it could be made clear and consistent. However, you need to remember the shortcut for ‘⤷’, which requires more than one char. Another problem is it’s not flexible - if you want to apply map and friends to collections, you would need to specify that the incoming object is the second or third argument of your method, so you would need some marker to specify in which place the object is curried - like _.

In the end of the day, vec⤷map(sqrt, _) and vec |> map(sqrt, _) requires that you type around the same number of chars, but the latter contains only one new syntax to understand - and that new syntax is useful in much more contexts than chaining.

10 Likes

Those are all good points, and it helps me clarify my own thinking.

x⤷₂map(sqrt) would work for that purpose.

x⤷₂map(sqrt)             # 12 char
x ⤷₂ map(sqrt)           # 14 char
x|>map(sqrt, _)          # 15 char
x |> map(sqrt, _)        # 17 char

I’m not advocating for this currently, still exploring the design space. Cheers for the thoughts.

1 Like

I think one of the possible reasons of the misunderstanding is that I can see 2 different use cases. One is seeing objects as something simple and primitive, like a mathematical object that requires some memory allocation but not much else, associated with a type and a variety of external methods (mathematical operations) that can be in common with many other types. And one where “objects” are large, very specialized blocks, with complex states and state transitions. Like blocks in signal/image processing pipeline. One block that does image segmentation, one that does classification, one that does temporal filtering, etc etc. In this second case there is an obvious “most-important” object, and people take comfort in having the complex internal state isolated from the external world, having the methods that can alter this internal state clearly declared in one place to “fix the API”, etc, etc.
I understand that this is an even larger discussion, and I’m mentioning it mostly to “get help” in clarifying my own thought process.
From “outside” it seems that Julia is focusing a lot on the first use case and not so much on the second. And it seems that there is some kind of understanding in the community that you cannot have both in the same language and from there a lot of discussion of which one is better “classical” OOP vs multiple dispatch, etc etc.

Is my (approximate) mental model of Julia incorrect? What am I missing? Is there a fundamental obstacle of having one language support both use cases. Is it a sociological risk?

1 Like

Not exactly. I do not believe Julia focus more on one case than the other (i.e., primitive vs complex objects). It is just that Julia design does not need the same machinery the OOP languages often provide, and providing this machinery would only lull programmers into a false sense of familiarity. Julia is not OOP, this does not mean there is any lack of support to large complex objects, and any OOP syntax/machinery would only be a crutch to people coming from OOP languages confusing the rest and, in the long run, probably harming even the people coming from OOP languages.

Sincerely, I am very tempted to just say “OOP is just a worse paradigm, no reason to tarnish Julia with that” but this would both disregard a lot of nuance and start a flamewar.

The thing is, the way Julia is, there is a lot of things that other languages have primitive mechanisms to enforce (and that enforcing things is seen as a good practice) but in Julia these things are done informally, this is, documentation say you should not and then you don’t. I prefer it this way and, unless the compiler can do more instead of less by allowing these extra constraints, these extra constraints end up being training wheels and nothing more.

1 Like

It’s not just a different syntax. In Julia, x.f already has a meaning "take a property from x which is named f". It cannot mean “take a function f from the global scope and curry f into it”, that would create ambiguities, inconsistencies and a lot of confusion.

See also my following comment:

6 Likes

It’s unfortunate that we have allocated such a good syntax x.f to an operation that’s generally not even public API.

11 posts were split to a new topic: Regarding getproperty and public vs. private APIs

I don’t think anyone has yet made a good argument to how that’s “good syntax”. “Familiar to pythonistas”, sure, but it doesn’t do anything special, it just shuffles arguments around to give the (false) impression that functions have owners.

2 Likes

This was my attempt to answer that:

I don’t think there’s much danger of users thinking that a function has an owner since that concept doesn’t exist in Julia.

In general, I would like to encourage more exploration of the design space rather than repeated comparisons to Python – we don’t have to be limited to the Python way of thinking. Nim is another language that has Julia-style multimethods and automatically rewrites x.f() to f(x) (with some limitations).

But there is already a short, idiomatic, built-in way to write that function call: f(x, y).

3 Likes

The idea is to support chaining:

rather than

eat(serve(cook(scramble(crack(egg), bowl), :high), :hot))

which is hard to read,

egg.crack().scramble(bowl).cook(:high).serve(:hot).eat()

is easier. The pipeline operator x |> f doesn’t take arguments (at least until the f(_, y) proposal lands) and regardless is substantially longer without an increase in clarity compared to x.f(y) or x⤷f(y) or whatever.

3 Likes

I think that most gains from one-liners are imaginary, so I would go for something like

dish = cook(scramble(crack(egg), bowl), :high)
eat(serve(dish, :hot))

then if necessary I can refactor the last part as

suggested_temperature(_) = :hot
suggested_temperature(::Revenge) = :cold
eat(serve(dish, suggested_temperature(dish)))
5 Likes
serve(dish) = serve(dish, suggested_temperature(dish))
1 Like

@Henrique_Becker , I get your beliefs and preferences, but that doesn’t really move the discussion forward without some concrete examples. What’s the current best approximation already present in the language?

For example, can we use modules as proxy for complex objects that needs to maintain a state from one call to the other? Can I declare a module that represents a block in the pipeline and export only a subset of the variables and functions?

That’s how you can achieve a similar functionality in C using files/static variables. Just put each block in a separate .c file and declare some static variable in that .c file to maintain the “private” state. Would modules allow something similar? Or some other construct?

EDIT:
PS I believe, after looking at the documentation, that modules cannot hide variables/methods, correct me if I’m wrong.
If we put the code of the original question in module called temporalFiltering it would probably alleviate the discoverability issue I’m guessing. But I’m not sure if using modules as objects would be considered good practice.

Best approximation to what? OOP? My point is that one should not come from an OOP way-of-thinking, or an OOP solution, and try to port it to Julia by mapping concepts. Instead, one should understand what is the Julia way-of-thinking and start from the beginning, this is, from the problem, and not from an already existing structure created in another paradigm.

uhhhh, you can do it for singletons, because you can have methods and variables inside a module, but you cannot create multiple instances of a module. What is the problem you are trying to solve here, why do not use struct to create complex objects? It seems very weird to me to try to use modules as objects.

Okay. The idea of using Julia modules is similar to this idea of using .c files with static variables. Here you also cannot instance multiple objects/copies of each “block”/file. However, again, I really need to understand better what is the problem you are trying to solve. The problem is hiding things from the users? (i.e., making variables/fields private?) Julia will not help you in this endeavor. You can work against the language with some success, like redefining the getproperty and getpropertynames of a struct to hide its fields, but getfield will always be able to recover them.

To simplify for the sake of conversation that problem that I’m trying to solve is: if I have to start a project tomorrow within a large company, with a team of 100 people, all highly skilled but busy and with very different backgrounds/interests: quality engineers, electrical engineers, ML researchers, software engineers, some physicist, some mathematicians (some with a background in Python, some Matlab, some C, some C++) I want a convincing story of why using Julia would be a good idea and have answers for the obvious questions coming from those people. Nobody will want to start a project and realize after 6 months that the code is a unmanageable mess, and now we are stuck with it for another 18 months, etc etc. Or that I have to babysit 100 people everyday because the learning curve is too steep, etc, etc.

It’s a very “practical” problem of how a group of people wants to invest time/resources and evaluate risk, etc.

Within that “context” I was then trying to build a mental model of the current state of the language, community, etc.

This process prompted the original question of this thread and then of Regarding `getproperty` and public vs. private APIs. I guess similar in spirit to What don't you like about Julia for "serious work"? .

Going back to the specific issue, I guess yes, it’s a combination of encapsulation and auto-completion, for the case where there is a very specialized set of complex objects/blocks. The question about the modules come from the idea that I could sacrifice the ability of having multiple instantiations, if I can have the other 2 properties right now, while we wait for the language to possibly evolve, etc, etc…

PS In the same spirit I was of course looking at the status of the debugger, VS code integration, Pluto vs Jupyter, plotting, loading dynamic libraries in concrete examples, ability of deploying Julia “jobs” on clusters where Julia is not installed, ability to use GPUs, etc, etc

If you are starting such a project tomorrow, don’t use Julia. If you can’t convince them not to use oop, use oop.

This is like asking for a good way to use the handle of an electric screwdriver as a hammer, because your handymen are more used to hammers and nails than screws.

11 Likes

100 people working on a monolithic application will be a mess in any language. OOP encapsulation is of little help here.

I would first try to divide the application in at least 10 separate packages (or even microservices), each with its own unit tests and specified (public) API.
For example SciML consists of a large number of independent packages designed to work together.

4 Likes

@gustaphe ok, at least your analogy is funny. But some will hear: why do you want to have any safety protocol at all? that’s old fashion and it will slow you down, I’m sure none of the handyman will accidentally drill a few holes in the load bearing beam killing everyone else, no need to hide it and put a locked gate around it.

@lungben yes of course, that is exactly what it would look like. I was just wondering what are the facilities in the language that would facilitate the splitting, accompanied with some “enforcement” tools and some “discoverability” tools (like the tab completion that completes only to states and methods that are part of the public API)