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.
It 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.
Sorry 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 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?
Yeah - `__add__` then `__radd__` isn't exactly double dispatch, but it's close. (There are corner cases involving inheritance where double-dispatch will work correctly but Python's approach will fail to pick the most-specific method.)
It is a mistake, because if you can't do "1.0 - z" or "0.5 * A", I don't know why you have operator overloading at all. What types with useful binary operators don't need to interact with scalars? Sets? Sequences? There aren't many math-y things that don't need numbers. The absence of complex values and matrices makes me sad, and I can list a dozen more on top of those.
Yes, static typing would be a huge change, but I think you're greatly exaggerating the complexity of multimethods for (only) binary operators. You don't want to do it, which is fine. It's your language, and you don't owe anyone anything, but be honest about your rationale.
You've already got a failure path to report errors when the types don't work, so falling back to a single global table of "first_type x second_type => function" is not more "huge" than anything else. You don't need scoping for operators like this, so it's literally one table. You can see the code in your head as you read this. I'm sure you could fill in the details or imagine some other simple implementation if you wanted to.
> I don't know why you have operator overloading at all.
Wren doesn't have "operator overloading". There is no separate feature in addition to the normal operator support that also lets you overload them. Wren just uses single dispatch for infix operators. Even the "built in" operator behavior like addition on numbers is simply pre-defined methods on the Number class. Like Smalltalk (which Wren is heavily inspired by), everything is just a method call and there is little "special" behavior for primitives.
Scala 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)
In 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.
I 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.
Wouldn'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.
Haskell already has an extension called UndecidableInstances.
Funny enough, even your basic Hindley-Milner style type inference has exponential runtime in the worst case, even if you don't add any bells nor whistles.
In practice, people don't tend to write code that triggers that worst-case.
Am I the only one that likes vanilla Haskell? I find that it quickly becomes messy with all the GHC extensions. I am although more of an SML kind of guy.
Many libraries try to stick to Haskell 98. Also whenever someone writes a paper about some new techniques, they always seem to take a lot of pleasure in pointing out when their technique works in Haskell 98.
I like that you can mix and match GHC extensions even in the same project. So one library (or even just one module) might use some crazy and messy extensions, but you can still use it from vanilla Haskell.
> 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 ... ".
Oops, I got carried away at the end there. Yes, maximally-restricted tolerable representation buys maximized compiler leverage. But might we more often metaprogram that out of diversely-performant maximal power, rather than bootstrapping it on performant but constraining circumscribed power?
Multiple dispatch, as in Julia, Mathematica, CommonLisp, etc, does get you much closer to this vision than single dispatch. To an extent, but then you're on your own again.
Maybe consider compilation as a collaboration of a computer and a wetware compiler. You have knowledge and do analysis which you are unable to share with the computer compiler. For silly tiny illustration, you might know some size variable has to be an integer power of 2, but most language's type systems don't let you tell the compiler that. Or think of writing a datatype in old C - a struct and a bunch of functions. You have a mental model of how they should all hang together, and then you mentally compile that down and hand emit C code and tests. Or you could hand emit assembly code and tests instead. But the C compiler is the more helpful collaborator. You can talk about more (with some sacrifices), and more easily. Now given say two structs and a bunch of associated functions, multiple dispatch allows weaving them together without pains which in single dispatch so discourage weaving. Which is better, but still, you've a couple of structs, a bunch of functions, and a still limited ability to express your intent so the computer compiler can help you with it.
So you can have multiple dispatch of + and - , and more easily add new numeric types, but any relationship between + and - is still all in your mind - the compiler has no idea that you think them connected.
Julia avoids the "ad hocery on multimethods, or kludgery slathered thick over single dispatch", but I'm not sure about "first-class explicit algebras, with operators, laws, equality, types".
Would "first-class explicit algebras" involve the language allowing redefining of operator precedences? Julia doesn't allow that. Would it require support for customizable associativity? You can painstakingly do that by defining custom types for every intermediate operation, but it isn't part of base Julia.
The original problem in the thread's start, "builtin_object <operator> your_object", is neatly solved by multiple dispatch. But the ask in your grand-parent comment is much bigger, and Julia only has slightly more support for that compared to other languages (i.e. it's a bit less of a pain in the ass to create such algebras, but you'll still have to create it yourself).
Yes. Though I was thinking of the Julia case as "ad hoc" - you define a couple of multimethods, you intend that they're related in some way, collectively creating some algebra with some properties, but all Julia gets is "ah, some random methods".
> redefining of operator precedences?
That can be confusing, but it's a fun thought. Maybe type-sensitive parsing? Or maybe "^--- the precedence of this operator is ambiguous between the theories visible at this point - please disambiguate which theory's operator you intended, either Int32:+ or ..."?
> What are the deficiencies of Haskell in this area, to your mind?
So I knew a wizzy haskell programmer, who repeatedly did the following. They'd deeply understand the mathematics of some problem domain. Use Coq to explore, analyze, and find a solution approach. Design a haskell model to support it. Extend ghc internals to permit that model. And then mentally compile a domain solution down to haskell. Oh, yeah, and the haskell compiler would then emit binary.
Through much of that, the haskell compiler isn't helping. It's serving as an inconveniently inexpressive intermediate representation, the backend target for a wizzy wetware compiler. I so very can't do that - just no way. I'd need a language and compiler which can collaborate with me much more extensively, far earlier in the process. One with which I can describe and discuss the problem domain.
I want to be able to say, drat, looks like I'm stuck yet again implementing another bloody 2D graphics like thing... Ok, give me a bounded discrete affine space over numeric tuples with all the trimmings. And for the implementation type, lets start in with this set of cache-aware tradeoffs. Ok, maybe that will satisfice. Moving on...
I'd like a language which supports a pushout lattice of theories. Given a theory, add a couple of types, operators, or laws, added in any order, and you get to the same theory. Permitting programming which feels like math. Not years of committee discussion to make monads applicative. I don't even need Agda-style "let's prove the world" - I'm mostly ok with Ruby-style "tests are green so it's all good". But I really want to be able to express problem domains, and to work collaboratively with tooling to craft solutions. And that's not (yet?) haskell.
:) Decades of frustration, and some fun conversations with good people. Credit for the "pushout lattice of theories" characterization to one - "sounds like what you want is a ...".
Wish I knew how to nudge progress faster. Late 1980's "seems clear I want something X-flavored, and with so much related work, we'll be there soon!" became "progress is sooo slow and diffuse". Became "I still hope to dance code before I die". Became "oh well, not looking likely". Or how to avoid burnout on moonshot spikes... or...
A couple of years before covid, Boston Haskell meetup group was active and wizzy. And bar discussion would repeated turn to what a "Haskell-NEXT" might look like. Aside from similar bars at conferences, I've not encountered such discussion elsewhere. Having such discussion work has seemed very sensitive to including extreme outlier quality people. Which very isn't me - I mostly just ask questions. They go home, and even though people left have wizzy math backgrounds and are doing their own language implementations... continuing can be hard. And the costs of such discussion in text, or in zoom... I can't picture that. So... I'm unclear on how progress might be fruitfully nudged. The bottleneck seems waiting for research papers to drop, and existing language efforts to incrementally improve, slowly creeping closer to "oh yeah, we could do that soon".
With a big caveat of its been years since I even slightly tried to track the big picture, here are two thoughts on directions which I then felt were getting less attention that I might have liked. First, having wizzy type system power, while not much caring about static typing. I thought of it as sort of a optional-typing CommonLisp attitude. Expressive power, but fine-gain collaborative-compilation tradeoff control over where to pay for what assistance. And second, extremely extensible languages, as can do bulk subsumption of others. So imported modules providing compiler cross-sections, from syntax to type analysis to optimization tweaks. ... Ah well, if humans don't get to it, maybe ML Copilot2030 will give "sure, just express what you want it any mixed mashup of different languages and math, and I'll try guessing what you're trying to say ;)
So... I'm unclear on a fruitful way to proceed...? Sorry... I appreciate the thought.
I'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.
C# 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).
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.