10 ms·
Wren seems pretty elegant, but it makes the same mistake a lot of languages do when it comes to operator overloading. If you put the binary operator as a metho
by xscott 4y ago
Wren seems pretty elegant, but it makes the same mistake a lot of languages do when it comes to operator overloading. If you put the binary operator as a method on the first object, then there isn't a simple way to have "builtin_object <operator> your_object".
For instance, Wren doesn't provide complex numbers, so you aren't going to get a nice syntax for "1/z" or similar. I could make similar examples with scalar multiplication for vectors, matrices, or Numpy-style arrays, etc...
Python and other languages have fallbacks to work around this, but Wren does not. And really, I think of binary operators as 2 argument functions, not methods on the left object.
- TazeTSchnitzel 4y agoThis is an area where typeclasses (as they're known in Haskell) or traits (as they're known in Rust) really shine.
- fiddlerwoaroof 4y agoOr multimethods (as found in CL/Clojure/Dylan/Julia)
- fiddlerwoaroof 4y agoCan’t you solve this particular issue with extension methods the way Kotlin and C# do?
- sirwhinesalot 4y agoExtension methods rely on overloading, they don't use dynamic dispatch.
- fiddlerwoaroof 4y agoYeah, but typically in these languages operators don’t really need dynamic dispatch. (Although, I agree this causes surprising results).
- sirwhinesalot 4y agoC# and friends dispatch operators through overloading on static types. Extension methods rely on the same mechanism. A dynamic language like Wren cannot use such a mechanism. The author already pointed out the options: the current OO-style dispatch, Python-style "if the operator is missing, try the reverse direction", or multimethods like Julia (which are quite complex and heavyweight).
- mncharity 4y agoI really look forward to a language with first-class explicit algebras, with operators, laws, equality, types. Rather than ad hocery on multimethods, or kludgery slathered thick over single dispatch, or the ever popular "you just can't get there from here". "1 is not an object; + is not a message"... I'm failing to find the quote, but it dates back like 40 years to smalltalk, and yet here we still are.
- drmt 4y agoAre you thinking about something like AlgebraicJulia, specifically Catlab.jl? It allows to specify Generalized Algebraic Theories and comes with a large list of standard theories. https://algebraicjulia.github.io/Catlab.jl/dev/apis/theories/ https://algebraicjulia.github.io/Catlab.jl/dev/apis/theories... @theory Category{Ob,Hom} begin @op begin (→) := Hom (⋅) := compose end Ob::TYPE Hom(dom::Ob, codom::Ob)::TYPE id(A::Ob)::(A → A) compose(f::(A → B), g::(B → C))::(A → C) ⊣ (A::Ob, B::Ob, C::Ob) (f ⋅ g) ⋅ h == f ⋅ (g ⋅ h) ⊣ (A::Ob, B::Ob, C::Ob, D::Ob, f::(A → B), g::(B → C), h::(C → D)) f ⋅ id(B) == f ⊣ (A::Ob, B::Ob, f::(A → B)) id(A) ⋅ f == f ⊣ (A::Ob, B::Ob, f::(A → B)) end
- qsort 4y agoWouldn't it become very undecidable very fast going down that route? Programming by defining algebras is interesting idea with many practical applications, even in down-to-earth CRUD apps, but how much magic can you really add before it becomes impossible (or even just very hard) to compile? The closest example in mainstream languages is Haskell, but even there the compiler basically trusts you. Is it possible to add something else while remaining decidable/efficient? I genuinely don't know.
- mncharity 4y ago> but even there the compiler basically trusts you Yeah, I'm ok with that. I'm more after expressivity than safety or efficiency. Sometimes they're not separable, but confronted with the usual "we don't know how to efficiently compile that, so we won't allow you to express it", my response is always "the language is not the right level at which to choose such engineering tradeoffs - give me finer-grain control, and in program portions where it matters, I'll trade expressiveness for efficiency or safety... after maybe first trying caching and cloud parallelism and patience and external analysis and ... ".
- samatman 4y agoI'm surprised to hear that, given how closely Wren's semantics resemble that of Lua's. Lua uses the left metatable's metamethod for two tables or userdata, or the metamethod if the binary operator has a primitive on one side of the argument. This gives elegant results at the expense of some implementation complexity: there's no mechanism to figure out what's happening inside the metamethod, all you know is that if both sides are metatable-able, you're in the left side's method.
- still_grokking 4y agoScala solves this with a syntactic twist: If the (in this case usually symbolic) method name ends in a colon (like say `+:`) it's right associative. So the following code¹ shows a method call (`prepend`) on the right operand: val someList = List(2, 3, 4) val extendedList = 1 +: someList // As the later is just syntax for a regular method call // `extendedList` could be also defined with the same result as: // someList.+:(1) val otherExtendedList = someList.+:(1) @main def entryPoint = println(extendedList) println(otherExtendedList) ___ ¹ https://scastie.scala-lang.org/y55n254oSVOFmw2CjmckjA https://scastie.scala-lang.org/y55n254oSVOFmw2CjmckjA
- eru 4y agoIn Haskell, you can just declare which way your operators are supposed to associate (if any). That doesn't seem to cause any problems in practice. (Though it helps that Haskell is statically typed, so the compiler can yell at you when you guess the direction of associativity for eg your extend-a-list operator wrong.
- 10000truths 4y agoIn C++, you can define a global function for the operator that is overloaded on the number and type of parameters: BigInt operator+(BigInt a, int b); BigInt operator+(int a, BigInt b); BigInt operator+(BigInt a, BigInt b); ...
- munificent 4y agoIt wasn't a mistake, just a deliberate compromise. My goal was a very small, simple, but flexible language. It needed to be pretty tiny to be embeddable in lots of applications. (And pragmatically, so that I could design and build the whole language myself.) Single dispatch works really well for, like 90% of the kinds of operations you want to perform in programs. It reads really well syntactically and avoids having to namespace every function with its argument type like you see in C and Scheme (hash-remove!, dict-remove!, set-remove!, etc.). And it does this without needing types or static resolution. Also, it's simple to compile to something fairly efficient at runtime using a vtable-like structure. It just sucks for binary operators. But coming up with a better solution for that requires something like: * Static types and static dispatch like C++, C#, etc. do. * Multimethods and multiple dispatch. * Double dispatch like Python. * Hacking something special into the language for operators like not allowing them to be overloaded and just having a fixed set of types they apply to. Static types and multimethods are a huge jump in complexity. Python's approach is really slow. Special casing operators makes me sad. So I just took a relatively small expressiveness hit and decided that, yes, operators aren't symmetric. In practice, it's mostly fine.
- backslash_16 4y agoSorry for focusing on such a small part of your comment. I’m learning about language design (as much as I can) and I don’t really understand what you mean by “double dispatch like Python”. I think (maybe soon thought) that Python has single dispatch. Since you’ve invented languages and work on them I’m pretty much 100% sure I’m wrong and would love to learn why. I read https://en.m.wikipedia.org/wiki/Multiple_dispatch https://en.m.wikipedia.org/wiki/Multiple_dispatch and came to the conclusion Python has “single dispatch polymorphism” because the method resolution is based on the type of the calling object dynamically at runtime and there is no method signature overloading, which means the argument type(s) doesn’t play a part in picking/resolving the method to be called. If you have time, do you mind explaining or pointing me to some resources?
- geysersam 4y agoI think he meant that for Python operators, the method can be dispatched based on the first or the second argument.