6 ms·
> a lot of people seem to really like the object.method() look. There’s a pretty simple reason for that: autocomplete. In most halfway decent dev environments
by cgrealy 7y ago
> a lot of people seem to really like the object.method() look.
There’s a pretty simple reason for that: autocomplete.
In most halfway decent dev environments, typing “object.” will present you with a list of possible operations on that object. OTOH, if you want to do “method(object)” you need to know the method name (in all current scopes, including globals).
I’m not saying this is the only or even best way to write code, but it’s definitely a factor IMHO.
- xscott 7y agoYou're almost certainly right, this is pretty compelling. It's not the way I work, but I'm sure lots of people really like their IDEs. Despite that, I recently changed one of my APIs from being: result foo(bar b, other stuff) result foo(baz b, other stuff) result foo(bum b, other stuff) To: result r = bar.foo(other stuff) result r = baz.foo(other stuff) result r = bum.foo(other stuff) Not because of the IDE, but because the compiler error messages are horrible for the ones above when you make a mistake. If you pass the type as the first argument, the compiler "helpfully" tells you 3 pages of information about all of the overloads. However, if you use method syntax, it only tells you overloads for the one object. It's a little frustrating that the hammer is changing the shape of our hands, and not vice versa.
- jcelerier 7y ago> It's a little frustrating that the hammer is changing the shape of our hands, and not vice versa. the "hammer" in this case being "doing things in causal order" - hard to find a different tool in the shed.
- xscott 7y agoI don't understand what you mean by "causal" in this context. A.f(B) is just a syntax sugar around f(A, B) unless dynamic dispatch comes into play. Then it's usually syntax sugar around something like: A._vtable[f](A, B) It really is just a function of two arguments, even when the privileged first argument looks like it's outside of the parens.
- jcelerier 7y ago> I don't understand what you mean by "causal" in this context. I mean that if you want contextual autocompletion (e.g. have only the relevant functions being shown given the type of an argument) then at some point you have to give your IDE this type information before typing the function - however you look at it it will always be [context] [function] instead of [function] [context]. > A.f(B) is just a syntax sugar around f(A, B) unless dynamic dispatch comes into play. this is a complete implementation detail of your operating system ABI and the way OO languages compile down to it, and in no way a "general truth" (of course there is an equivalence between both). There used to be some CPUs with hardware instructions for OO method calls in the 80s for instance - you may want to call that syntax sugar around electrons, but I would respectfully disagree :-) (even then, for instance Microsoft has the __thiscall calling convention for C++ methods which differs from __cdecl used for C functions - so even there there are fundamental differences between a.f(b) and f(a, b) as arguments will be passed differently to the method.)
- jstimpfle 7y agoI just don't use overloads, and not even a language that has overloads.
- xscott 7y agoWhat language? Unless it's Ocaml, I'll be surprised if addition isn't overloaded for ints and floats at the least. Most people are fine with some overloading... To each their own though.
- jstimpfle 7y agoYes but that doesn't really apply to your point. Addition is built in. It doesn't affect completion, and barely affects type checking. The "overload" is fixed and generally well understood. It's almost not an overload at all if you view it as an operation on the set of all integers. There is only a clamping in the end that depends on the size of the type (unless it's unlimited precision arithmetic on BigNums)
- DonHopkins 7y agoThe world has been changing the shapes of our hands since they were fins. The problem I have with most language's function call syntax (except for point-free stack based languages like FORTH and PostScript) is that you can have multiple fingers on your "in" hand, but only one finger on your "out" hand. C#'s in/out/ref modifiers and Lisp's multiple-value-bind are hacks. https://en.wikipedia.org/wiki/Tacit_programming https://en.wikipedia.org/wiki/Tacit_programming FORTH's /MOD ( numerator denominator -- remainder quotient ) naturally takes two integer inputs and returns two integer outputs, and it doesn't use need any special clumsy syntax to express that.
- xscott 7y ago> only one finger on your "out" hand I think Python's way isn't amazing, but it's not too bad either: d, r = divmod(100, 3) It's a bit uglier in C++, but you could make this work: int d, r; refs(d, r) = divmod(100, 3); I'm certain there's either a Boost or STL library for this. (I wrote my own for C++98 at one point) It's clear you like stack based languages, and I certainly admit they can be concise.
- dragonwriter 7y ago> The problem I have with most language's function call syntax (except for point-free stack based languages like FORTH and PostScript) is that you can have multiple fingers on your "in" hand, but only one finger on your "out" hand. I dunno, in many modern languages that aren't point-free stack-based languages you can (and the difference between these is one of perspective more than concrete substance, arguably) either have multiple “fingers” on either hand or only one on each, but the one value each touches can be arbitrarily structured and destructured.
- DonHopkins 7y agoThat same logic can be used to argue that functions should only support one input argument, too. If you can have multiple inputs, then what's wrong with multiple outputs? And if multiple outputs are so bad, then why have multiple inputs? There's a big difference between simply and efficiently returning multiple values on the stack without generating intermediate garbage and memory references, and packing multiple values up into a single tuple, polymorphic array, structure, or class, and then destructuring it later, or passing input parameters that are indirect pointers to temporary output locations in linear memory. Using indirect pointers for output parameters can also cause bugs and performance optimization problems with aliasing. https://en.wikipedia.org/wiki/Pointer_aliasing https://en.wikipedia.org/wiki/Pointer_aliasing To elaborate what I said: C#'s in/out/ref and Lisp's multiple-value-bind syntax are clumsy, inelegant, and inefficient hacks. And languages like Java that don't have pointers can't even do that, and just have to proliferate pointless container classes and generate garbage intermediate objects. Quick: How do you implement swap(a, b) in Java? (Without using an IntermediatingObjectSwapperDependencyInjector- ProxyThunkingShadowEnumeratorComponentBeanAdaptor- DecoratorReferenceRepositoryServiceProviderFactoryFactory!) http://www.javapractices.com/topic/TopicAction.do?Id=37 http://www.javapractices.com/topic/TopicAction.do?Id=37 https://stackoverflow.com/questions/1403921/output-parameters-in-java https://stackoverflow.com/questions/1403921/output-parameter... https://stackoverflow.com/questions/3624525/how-to-write-a-basic-swap-function-in-java https://stackoverflow.com/questions/3624525/how-to-write-a-b... With WebAssembly, for example, returning multiple values has a significant effect on performance and code size, because it's much more costly to return multiple values indirectly through linear memory than on the stack. (The stack is in a separate address space that you can't point to like linear memory.) That's why there's an active multiple value return proposal for WebAssembly, which is implemented in Chromium release 80: https://www.chromestatus.com/feature/5192420329259008 https://www.chromestatus.com/feature/5192420329259008 https://hacks.mozilla.org/2019/11/multi-value-all-the-wasm/ https://hacks.mozilla.org/2019/11/multi-value-all-the-wasm/ >But Why Should I Care? >Code Size >There are a few scenarios where compilers are forced to jump through hoops when producing multiple stack values for core Wasm. Workarounds include introducing temporary local variables, and using local.get and local.set instructions, because the arity restrictions on blocks mean that the values cannot be left on the stack. >Consider a scenario where we are computing two stack values: the pointer to a string in linear memory, and its length. Furthermore, imagine we are choosing between two different strings (which therefore have different pointer-and-length pairs) based on some condition. But whichever string we choose, we’re going to process the string in the same fashion, so we just want to push the pointer-and-length pair for our chosen string onto the stack, and control flow can join afterwards. [...] >This encoding is also compact: only sixteen bytes! >When we’re targeting core Wasm, and multi-value isn’t available, we’re forced to pursue alternative, more convoluted forms. We can smuggle the stack values out of each if and else arm via temporary local values: [...] >This encoding requires 30 bytes, an overhead of fourteen bytes more than the ideal multi-value version. And if we were computing three values instead of two, there would be even more overhead, and the same is true for four values, etc… The additional overhead is proportional to how many values we’re producing in the if and else arms. [...] >Returning Small Structs More Efficiently >Returning multiple values from functions will allow us to more efficiently return small structures like Rust’s Results. Without multi-value returns, these relatively small structs that still don’t fit in a single Wasm value type get placed in linear memory temporarily. With multi-value returns, the values don’t escape to linear memory, and instead stay on the stack. This can be more efficient, since Wasm stack values are generally more amenable to optimization than loads and stores from linear memory.
- SamReidHughes 7y agoI’d say infix syntax and somewhat asymmetric method lookup/overloading rules are the bigger factor. Notwithstanding that it matches the implementation of virtual dispatch.
- xscott 7y agoYeah, both Rust and Go also use the method syntax to provide dynamic dispatch, and it's simpler for them since this dispatch is chosen (presumably vtable style) based on the privileged first operand. However, I think using methods to provide infix syntax is a real mistake. For instance, if I want to make an infix operator which allows me to subtract mytype and yourtype, I can do the following: mytype - yourtype; // mytype.sub(yourtype) However, the opposite requires me to add methods to yourtype: yourtype - mytype; // yourtype.sub(mytype) Python gets around this by having 'r' versions of functions which get invoked if yourtype refuses to acknowledge my type. However Go and Rust could resolve which binary function to use statically, but they don't want to support overloading unless it's a method on the first object.
- SamReidHughes 7y agoInfix operators are out of scope of my comment. I am only referring to method invocation syntax itself, x.a(y, z).b(v), being infix.
- xscott 7y agoI misunderstood then. However, if you just meant that people prefer method syntax because method syntax is infix, then it seems to come full circle to asking why they like infix syntax for methods... Doesn't really matter - it is what it is.
- SamReidHughes 7y agoWell, if you imagine some prefix method syntax -- let's say, foo[a](b, c, d) instead of a.foo(b, c, d) -- it misses out on one of the advantages, in that method chaining is easier to read. I mean this is a nice feature for function call syntax in general and the benefits of infix syntax is not specific to methods. This is one reason why some new languages make f(a, b, c) and a.f(b, c) produce the same AST. Anyway, I wanted to separate the effects of infix positioning of the method name from those of having distinguished syntax for method invocations, which with static dispatch still carries other advantages.