4 ms·
> My favorite is uniform function call syntax. In several languages (Nim, Koka, D, …), you can always write bar.foo(baz) instead of foo(bar, baz) and vice-versa
by IIsi50MHz 4y ago
> My favorite is uniform function call syntax. In several languages (Nim, Koka, D, …), you can always write bar.foo(baz) instead of foo(bar, baz) and vice-versa.
To me, these are "Tell bar's foo to do something with baz." and "Tell foo to do something with bar and baz.". So being 'able' to flipflop the syntax is at least temporarily semantic'ly confusing.
- gernb 4y agoThat arguably depends on your POV. Thinking like python where a method always declares 'self' as the first argument, then a function is just 1 thing (there's no such thing as a method). Then dot syntax is just syntactic sugar for passing the first argument, and there's nothing special about functions. You can manually pass the first argument. In other words, to me it's simpler and therefore less confusing.
- xigoi 4y agoI find the distinction between “foo” and “bar's foo” unnecessarily confusing. For example in C++, why is getting the last element of a “vector” something that belongs to it, but reversing a “vector” is something external?
- SebastianKra 4y agoExcept I find the concept of state "doing something with" other state unhelpful. Why is bar working with baz and not baz working with bar? I struggle with this in Unity: Player.collect(PickUp) // this? PickUp.boost(Player) // ...or this? Instead, the code should describe the interaction between the two units of state: onCollide(Player, Pickup) If we structure our code like in the last example, it makes sense to weaken the `a.b` vs `b(a)` distinction, and instead use the dot as a kind of pipe-operator.
- tsimionescu 4y agoIt shouldn't be that confusing. Ideally, a method should be defined on an object only if it needs to access the encapsulated state of that object. Otherwise, it should probably be a free function. So, in your case, depending on other modeling decisions, I could argue either for onCollide(Player, Pickup) - if Player and Pickup are both plain data, and don't need to guarantee any invariants Player.collect(Pickup) - if Player actually has to ensure some invariants such as health<100) I don't see any good arguments for pickup.boost(Player), in typical games. Of course, if both the Pickup and the Player have some invariants that need to be maintained on a collision, then arguably the design has to be changed at a deeper level.
- Y_Y 4y agoBut this isn't a discussion about pickups specifically. Attacking an example is pointless if the thing you take issue with is incidental and not fundamental to the argument. I bet you could think of a case where you pick something else and OP's example meets your standards.
- tsimionescu 4y agoMy point was that there is a meaningful, and I believe relatively simple, distinction to be made between free functions and methods bound to an object - a distinction which UFCS doesn't really help with. For any given example, I believe there is a reason to prefer one over the other, and I showed what reasoning I would use for the particular example raised by OP.
- SebastianKra 4y agoI got that and it's a good point. Although I rarely see Objects used this way. Often, methods are used to implement all related functionality. Unity even strongly encourages this. (...at the moment. They are working on Entity Component Systems which will work more similar to my third example) I concede that languages shouldn't use the dot as a syntactic tool, be it through Extensions[1] or UFCS, but rather offer a pipe-operator. If they don't, I'd still prefer UFCS rather than no way of chaining at all. [1] Extensions for interface/protocol conformance are fine of course.
- zmgsabst 4y agoI think which of the three is best depends on the context: What is the code driving it? - looping over players to update them? - looping over objects to update them? - some other event loop? I actually like the third least: The first two tell me what is happening, but the third doesn’t — I could be colliding to block motion or I could be picking up.
- eps 4y agoThe main selling point is the ability to chain calls foo.do_this(bar).do_that(baz) instead of do_that(do_this(foo, bar), baz)
- pritambaral 4y agoThat is easily handled by macros without sacrificing semantic clarity: do_this(foo, bar) |> do_that(baz) Or an explicit version: do_this(foo, bar) |<> do_that(<>, baz) (Yes, this is a bastardization of Elixir's pipe and some Lisps' arrow macros.)
- Doxin 4y agoThe use case for this sort of thing I like best is extending objects without touching the object. E.g. in D to convert between different types you can use std.conv: import std.conv; "123".to!int; 123.to!string; Ints don't need to understand string building and strings don't need to understand int parsing. The conversion code just needs to declare a couple functions taking the right arguments and it just works. To me the above is much more readable than to!int("123") to!string(123) in any case. It's also quite nice when dealing with C APIs since it allows you to pretend they are OOP in quite a lot of cases. e.g. with SDL: SDL_CreateRenderer(window, -1, 0); turns into window.SDL_CreateRenderer(-1, 0); and say I'd like to have a function to initialize all the renderer stuff in one go? I can simply declare void CreateRendererAndInitialize(SDL_Window* window){<snip>} and now you can do the following: window.CreateRendererAndInitialize(); It removes a lot of pain from extending 3rd party types you see in other languages.