Remix.run Logo
elcritch 2 days ago

> Syntax - UFCS (Uniform Function Call Syntax)

UFCS is such an underrated language feature. When you have UFCS you can toss out 90% of the uses of methods in favor of just plain ole functions. Add generic functions and concepts and you rarely end up needing OO support.

pjmlp a day ago | parent | next [-]

OO support is more than just record.method(), which is something you don't even have in OOP systems based in multi-dispatch.

sirwhinesalot a day ago | parent [-]

I always find it funny how people focus on the .method() syntax. You have the "pipe" operator in some functional languages that achieves the same thing, pass the result of an expression as the first argument of the chained call on the right. Nothing to do with OOP.

pjmlp a day ago | parent | next [-]

I think it needs a higher level experience across procedural, logical, functional and object based languages, the unique ways each one applies ideas into their programming model, how that interacts to CS, for finally understanding how those concepts come together, instead of being so fixated in language syntax.

xigoi a day ago | parent | prev [-]

This is because in languages that don’t have UFCS, methods are often abused to create “natural” looking syntax like

    expect(6).toBe(even)
kmarc 2 days ago | parent | prev | next [-]

Reminds me of vim script's implicit method syntax [1]

Eg. any function call can be converted to a method call on the function's first parameter:

    let mylist = [3, 2, 1]
    " prints "1" as these two are equivalent
    echo sort(mylist) == mylist->sort()
Helps a lot with chaining.
kyleee 15 hours ago | parent [-]

Quirky and awesome

kmarc 8 hours ago | parent [-]

If you think about it, it's the mirrored version of python object/class methods: they receive the object (self) / class (cls) as the first parameter. Same with Rust's trait impl's methods.

layer8 2 days ago | parent | prev | next [-]

Dot syntax tends to work better for code completion, though.

In addition, without uniform call syntax, adding a new method can only break subclasses, whereas with uniform call syntax it can break other client code.

Doxin a day ago | parent [-]

There's nothing preventing UFCS from working with code completion. e.g. given:

    int addOne(int v){
        return v+1;
    }
You can now write code like this:

    int foo=3;
    writeln(foo.addOne);
There is absolutely no reason that typing "foo." would not suggest "addOne" as possibility.
layer8 a day ago | parent [-]

My comment was about the reverse, using function syntax for methods.

Furthermore, I don’t think it necessarily makes sense for all functions that happen to take, say, a string as their first argument, to be listed in the code completion for method invocation on a string variable.

If you merely want to define auxiliary methods outside of a class, which is the thing the GP seems to like, that’s what’s usually called “extension methods”. It doesn’t require uniform call syntax.

Doxin a day ago | parent | next [-]

> using function syntax for methods

hmm, yeah fair enough I suppose. I don't think I've found a good use-case for that yet. I guess having the symmetry there makes the feature easier to explain at least? I dunno.

> Furthermore, I don’t think it necessarily makes sense for all functions that happen to take, say, a string as their first argument, to be listed in the code completion for method invocation on a string variable.

All functions in scope that happen to take a string as their first argument. If this turns into an actual problem in practice it's quite doable to refactor things such that it's not an issue.

I find that when I use autocomplete I'll be typing the first bit of the method name in any case. I never "browse" autocomplete to look for the thing I need.

Extension methods are another way to do the same thing yes, but that feels like special-casing behavior, where UFCS is more general. With extension methods you need to think of how to implement it wrt things that don't usually have methods attached. With UFCS that just works the way you'd expect it to.

jasperry a day ago | parent | prev [-]

I agree with this. I'd go even further and say that dot syntax should only be used to access things that are /actually a part of the object/, whether record fields or methods. If you use the dot for everything just because it's convenient, you're making the code structure harder to understand by syntactically conflating different mechanisms.

WalterBright 2 hours ago | parent | next [-]

I use it when the idea is data flows from left to right.

zem a day ago | parent | prev [-]

unless your intent is to simulate open classes, and the functions you call via the dot are conceptually meant to be an extended set of methods for the type

motorest a day ago | parent | prev | next [-]

> When you have UFCS you can toss out 90% of the uses of methods in favor of just plain ole functions.

Can those go away if you use inheritance or polymorphism, and you need your functions to access protected or private members? I mean, OO is as much about methods as functional programming is about functions.

zozbot234 a day ago | parent [-]

Implementation inheritance is generally a misfeature, especially in modern languages with proper generics. If you insist on having it, you can replicate it manually by using interface inheritance and the generic typestate pattern.

motorest a day ago | parent [-]

> Implementation inheritance is generally a misfeature (...)

Not really. It's value proposition is code reuse. It's not a misfeature just because it breaks a simple explanation.

zozbot234 a day ago | parent | next [-]

You only ever need interface inheritance for code reuse. The genuine value proposition for implementation inheritance is, quite unsurprisingly, the same as for typestate and generic typestate, namely better type checking within a single self-contained program module.

Implementation inheritance "in the large" remains a total misfeature and should not be used, other than for very tightly defined, extensible "plug in" architectures (where the point of extensibility can itself be treated as a single module). But these are really quite rare in practice.

motorest a day ago | parent [-]

> You only ever need interface inheritance for code reuse.

Not really. Interfaces play no role in invoking member functions, even if they are defined in a base class. Inheritance is used to allow member functions declared in a parent class to be called in a member functions without requiring boilerplate code. Instead of having to duplicate code, inheritance provides a concise way to specify a) this is what I want to reuse, b) this little thing is what I want to change in the implementation.

> The genuine value proposition for implementation inheritance is, quite unsurprisingly, the same as for typestate and generic typestate, namely better type checking within a single self-contained program module.

No. The value proposition is not requiring any boilerplate code to extend or change any detail in a base class. With inheritance, you just declare the thing you want to add or change, and you do not need to touch anything else. The alternatives being floated fail to meet very basic requirements such as visibility, encapsulation, and access control.

> Implementation inheritance "in the large" remains a total misfeature and should not be used (...)

Not true. This is a personal belief based on specious reasoning. You need to go way out of your way to ignore the problems that inheritance solved while ignoring the negative impact of the alternatives being floated.

zozbot234 a day ago | parent [-]

> Inheritance is used to allow member functions declared in a parent class to be called in a member functions without requiring boilerplate code.

The defining characteristic of implementation inheritance is open recursion. When you call a "member function declared in a parent class" there's no telling what code will actually be run, because that member function may have been overridden at any point in the class hierarchy. There's no way of knowing whether the caller and callee code will agree on the required semantics and invariants involved, or even what these should be for any given case. That's why this is a misfeature for a "programming in the large" scenario.

By contrast, these issues can be managed when using implementation inheritance within a self-contained, smaller-scale program module, and then its open recursion behavior, properly managed, matches what we expect from the use of the typestate pattern.

motorest 6 hours ago | parent [-]

> The defining characteristic of implementation inheritance is open recursion.

Recursion does not register as a concern in inheritance or polymorphism.

> When you call a "member function declared in a parent class" there's no telling what code will actually be run, because that member function may have been overridden at any point in the class hierarchy.

This is a feature, and a valuable one. You don't care what your parent class is doing. Your concern is that you want to extend it, and that's it.

> By contrast, these issues can be managed when (...)

You didn't pointed any issues. Also, composition addresses any concern you might have.

You're searching for problems that fit a solution, and so far you pointed no problem.

galangalalgol a day ago | parent | prev | next [-]

I don't find code reuse to be an unqualified virtue. The most hideous codebases I have been involved with all share an obsession with DRY that destroyed them. Most of them were also really obsessed with extensibility which didn't help. So while implementation inheritance can certainly help with code reuse, I consider that an anti-feature.

elcritch a day ago | parent | prev [-]

You get about as much reuse from a set of re-usable functions as you get from inheriting implementations.

Though I grant that having an object hierarchy does make it a bit more explicit what’s being inherited or needs implementing. However, a OO hierarchy also tends to obscure the actual parent implementations as well. Just having a set of functions from a module generally lowers the number of indirections.

In general I find working through a non-OO code base generally easier to grok and understand. Especially the OP culture from Java or C# style OO, even if I’m generally good at understanding OO patterns.

motorest 6 hours ago | parent | next [-]

> You get about as much reuse from a set of re-usable functions as you get from inheriting implementations.

There's far more to inheritance that code reuse. For example, encapsulation, access control, and static type checking, etc.

galangalalgol a day ago | parent | prev [-]

Ok, this is what I was trying to say in my sibling post, but without the snark and including the why.

girvo 2 days ago | parent | prev | next [-]

I was about to say "Yeah, its my favourite feature of Nim!" and then I realised what account I was replying to ;)

elcritch a day ago | parent [-]

Haha, Nim is my preferred UFCS language.

jayd16 2 days ago | parent | prev [-]

If all you're doing is accessing public members, sure.

nicwilson 2 days ago | parent | next [-]

`private` is only private to the module, not the struct/class, (In other words, all functions in the same module are all C++ style `friend`s) and so free function in same module work.

WalterBright 2 days ago | parent | prev [-]

It works for all functions that have parameters, `f(a)` and `a.f()` are equivalent.

jayd16 2 days ago | parent [-]

Yes, but I presume that f() cannot access private members of a.

WalterBright 2 days ago | parent [-]

That's correct.