Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Wren seems like a really clean alternative to a language like Python, but it makes one mistake that really drives me nuts. In order to provide binary operators, you must implement a method on the left object. From the documentation [0]:

> The left operand is the receiver, and the right operand gets passed to it. So a + b is semantically interpreted as “invoke the +(_) method on a, passing it b“.

This means that if you want to implement complex numbers, or matrices, or a bunch of other useful stuff, you'd have to be able to extend the type on the left (which you didn't write). You really want to be able to do things like:

    var z = Complex.new(1.0, 2.0)
    var result = 3.0 - z

    var A = Matrix.ident(3, 3)
    var scaled = 4.0*A
Python hacks around this by having "right' versions of these methods too. It's kind of ugly - if the left object fails to support the operator method (__sub__ or __mul__) with the right type, the interpreter looks for a __rsub__ or __rmul__ operator on the right object to try. Ugly, but it works.

C++ works around this a much better way: You can either have the operator method on the left object, or have an overloaded operator function that isn't tied to either object.

Rust puts the operators on the left object too, but in some cases you can add methods/trait specializations to left classes. Unfortunately, Rust doesn't let you do this with generics, but that's a longer more complicated topic.

Maybe there's some way Wren lets you tack on additional methods to the builtin Num class (monkey patching), but I couldn't find it. I would love an elegant alternative to Python, but this is enough of an issue that it keeps me from using Wren.

[0] - https://wren.io/method-calls.html#operators



The solution I've been playing with for $interp_thing (mostly in my head so far) is roughly the python version but with some type dispatch checking to pick the "most specific" of the two - which is easy when one is a subclass of the other but gets gribbly in more complicated cases. I suspect ultimately it'll be a case of "run a multimethod style selection algorithm for 'most specific' and arbitrarily declare 'LHS wins'" but I've not got far enough to consider that remotely fully baked.

($interp_thing being a placeholder for something I'm experimenting with of late)


Yes, OO inheritance and subclassing complicates the multimethod selection algorithm, allowing for there to be ambiguity or ties. I don't use inheritance very often, so if it was my $interp_thing I would just leave it out.

I don't remember the details very well, but "The Art of the Metaobject Protocol" is a book which goes into this topic. That's in the context of multimethods for Common Lisp, and I suspect they were very thorough and careful with the solution they used. https://en.wikipedia.org/wiki/The_Art_of_the_Metaobject_Prot...


You also have the Ruby solution of calling a `coerce` [0] method on the right hand side.

[0] - https://github.com/ruby/ruby/blob/0703e014713ae92f4c8a2b31e3...


I don't know Ruby very well, and I'm curious how this would work with my example above. It seems like Ruby's builtin number types, upon not knowing what to do, calls coerce on the right-side object. That right-side object returns a pair of new objects, and then the binary operation is called as a method on the left one of those?

If I've got that right, it sounds functionally similar to the Python way. It's a little awkward if you want different types depending on the operation. For instance, maybe A-B returns a different type than A/B. In both subtraction and division, it looks like Ruby would call coerce(), and coerce doesn't know the operator. Still that could build some smart placeholder type that then knows how to treat the operations differently...

For my example with Complex and Matrix types, it seems like the Complex type would need to implement the coerce protocol on its own. Maybe I've got that wrong, but does the coerce thing automatically happen for any type, or is that special behavior implemented by Ruby's Number types?


> If I've got that right

You did.

> it sounds functionally similar to the Python way

It definitely is, however it allows to handle all operators by defining a single method, so it's a bit more usable.

> does the coerce thing automatically happen for any type, or is that special behavior implemented by Ruby's Number types?

Only for number types. For other core types using binary operators, you have dedicated implicit conversion methods. For instance `"foo" + MyType.new` will try to call `MyType.new.to_str`.

`to_str` being specifically for implicit conversions, and `to_s` for explicit conversions. So implicit conversions won't happen unless specifically defined.


This approach seems nice in that it fast-tracks the common case and only delegates if needed.

> it allows to handle all operators by defining a single method,

That's nice and pragmatic too.

Back to the subject of this submission, I suspect the coerce idea could be bolted onto Wren without really breaking or changing much of anything (just an additional branch in the error handling of Wren's builtin Num type). That would allow Wren to grow it's own numpy-like library someday.


Ruby also let's you extend all object/classes - all the way up/down to Object.

I don't know that I think that's a very good idea, but at least it's possible.

It allows things like:

  require 'active_support/time'
  5.days.ago


Lua addresses this problem by having metamethods, with a special way of failing over when operators meet a type error:

https://www.lua.org/manual/5.3/manual.html#2.4

>If any operand for an addition is not a number (nor a string coercible to a number), Lua will try to call a metamethod. First, Lua will check the first operand (even if it is valid). If that operand does not define a metamethod for __add, then Lua will check the second operand. If Lua can find a metamethod, it calls the metamethod with the two operands as arguments, and the result of the call (adjusted to one value) is the result of the operation. Otherwise, it raises an error.


Thank you for the link, but I'm not sure this solves the problem. I mean, what if Abbie makes complex numbers, so she provides a metamethod to work with the builtin number type. Then Bobby wants to build on Abbie's work and add matrices. Lets say Abbie's complex type is on the left, but her metamethod doesn't know anything about matrices... Bobby is willing to support complex numbers multiplied with matrices, but the matrix metamethod never gets called:

    z = complex(1, 2)
    A = ident(3, 3)
    -- Won't this next line call the wrong metamethod?
    X = z*A
For a dynamicly typed language, I think you'd want something like multimethods to do this cleanly. (Or something like Python's dirtier approach)


If you want to allow unknown types to do your multiplying, you can write __mul like so:

    [...]
    elseif getmetatable(right).__mul and getmetatable(right) ~= complex then
       return getmetatable(right).__mul(left, right)
    [...]
Of course this requires a little forward thinking, but it doesn't require you to know what other metatables (classes) you'll be compatible with. It's not as clean as multiple dispatch, though.


I think the most elegant solution is to break the subordination of methods to classes by having structs (possibly with inheritance) and multimethods.


But this solution needs to write down the type(struct) of every parameter. I think the biggest reason why dynamic language is popular is they don't force users to write type every time. So this is contradicting to the charm of dynamic language(as literally, all scripting language is dynamic, including wren).

We can mark those function which doesn't specify all the types as default, but it will make code very ambiguous, like:

function add(A a, B b) function add(A a, b) function add(a, B b) function add(a, b)


You only need to specify the type if you want to dispatch on the type. In CL, unannotated arguments are considered to be of type T, which indicates they could have any value.

This isn’t any different from the way methods are dispatched in Wren or Python or anything: it’s just that the type of the single argument that’s relevant for dispatch (this/self/etc.) is the class. But, the disadvantage is that since methods are nested in the class definition, there’s no clean way to extend pre-existing classes without modifying the source of the defining class. If “method” is its own concept, on equal footing with classes, you can put all the base cases together and then users that want to extend the set of types the operation works on can specify additional cases to define the interaction between their new types and the pre-existing ones.


I’ve written a lot of Common Lisp now, and I’ve found that multimethods help eliminate a lot of boilerplate where you write classes that wrap an existing class and delegate to it and similar patterns.


Soo:

    class complex(real, imag);
    
    function add(number a, complex b) {}
    
    function add(complex a, complex b) {}


Why could you not write:

z + -3.0 and A * 4.0 ?


Yeah, you could do that in some cases. You could also just call functions and not have operator overloading at all.

However, it's not hard to construct a case where instead of using literal values they are arguments to a function (or elements in a list), and you don't know which type comes first until runtime.

But really, if you're translating math from an equation (say in a book or something), juggling the order of the binary operators is gross.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: