Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Ziglang Infix Function Sugar #8204

Closed
ityonemo opened this issue Mar 11, 2021 · 6 comments
Closed

Proposal: Ziglang Infix Function Sugar #8204

ityonemo opened this issue Mar 11, 2021 · 6 comments
Milestone

Comments

@ityonemo
Copy link
Contributor

Specific definition:

The expression (a <+> b) is parsed as equivalent to
@"<+>"(a, b). The contents of the expression must be either
one or two ascii characters taken from the range A..Z | a..z
| 0..9 | ['+', '-', '/', '*', '.']

Motivation

There have been lots of people complaining that zig has no operator
overloading, and thus it is an unsuitable replacement for certain things
that C++ is good for (advanced math, some game physics stuff). I
suspect that people don't want operator overloading, they want a
binary infix form, so that they can easily spot logical errors and
debug them quickly. This proposal seeks to provide binary infix forms
while still satisfying the philosophy of zig and gently discouraging
undesired uses of operator overloading.

Spot the bug

Normal distribution function (presume you are using say fixed point
numbers so you can't use the standard operators):

(correct) math formula:

1/(sigma * sqrt(2 * pi)) * exp(-((x - mu)/sigma)^2/2)

current zig (buggy):

const sqrt2Pi = ...
return mul(div(1, mul(sigma, sqrt2Pi)), 
  exp(neg(div(div(sqr(sub(x, mu)), sigma), 2))))

with infix functions (buggy):

const sqrt2Pi = ...
return ((1 </> (sigma <*> sqrt2Pi)) <*>
  exp((neg(sqr((x <-> mu)) </> sigma)) </> 2))

What this is not.

  • this is not function overloading. In the example given, there
    must be a fn @"<+>"(a: ..., b: ...) ... {...} defined "in the
    normal way", in the proper scope.
  • this is not hidden control flow. (_ <+> _) is to be understood
    as "a function call".
  • this is not hidden memory allocation. If the step requires
    memory allocation, the allocator must be defined by the parameters
    and arbitrated by the function. Memory failures must then be either
    caught within the function or directly bubbled up as an error in
    the standard way that Zig handles errors.

Design details

  • chosen to be reminiscient of a mathematical operator, and ugly
    enough to not be confused as a builtin operator.
  • chosen to discourage stupid stuff like << becoming "stream
    concatenation".
  • parentheses are required. This avoids having to worry about
    zig knowing binary operator precedence. Also adds to the ugliness
    to discourage overuse.
  • two characters makes composition more ergonomic, you can privately
    pull a <+> function from a struct, reassign it as <+a> and then
    export a <+> that uses <+a>.

Details that don't have to be decided atm

  • The outer characters
  • Exact character ranges

Scope

  • Binary operators only. No unary, no ternary.

Implementations

  • @ityonemo implemented this in the stage2 part, that worked until Andrew
    refactored the parser to be more DDD; it was about ~100 LOC including
    tests; however was not able to test that it "actually works" due to stage
    2 not working yet.

Pros

Cons

  • It is literally a language change.
  • "two ways to do things" @"<+>"(a, b) vs. (a <+> b)
@ityonemo
Copy link
Contributor Author

(discussed @ meetings 10 Mar 2021)

@daurnimator
Copy link
Contributor

Essentially a duplicate of #427.

@g-w1
Copy link
Contributor

g-w1 commented Mar 11, 2021

If a library exports a function like this, do you have to do @"<+>" = lib.@"<+>"; or will (1 lib.<+> 1) work?

@ityonemo
Copy link
Contributor Author

ityonemo commented Mar 11, 2021

lol sorry for the confusion, the decision was to close it, this will not be a part of zig. Consider it a "steel man" against the operators concept.

@tauoverpi
Copy link
Contributor

The restriction to just one or two characters doesn't make much sense given the delimiteters and full names would often be nicer for certain things. Having (n <divmod> 10) (probably a bad example) and similar would be clearer in cases where <> creates operator soup such as </%>.

@SpexGuy
Copy link
Contributor

SpexGuy commented Mar 12, 2021

This was a difficult decision, but ultimately we decided to close this issue.

This proposal sidesteps the primary problem with operator overloading. With this proposal, it's clear to someone who knows Zig that a function is being called (not hidden control flow). It also has some nice properties. For programs which are highly mathematical in nature, the math formula is a form of documentation for the code. Having the code be closer to the formula may make it more readable, and could potentially prevent bugs. And it took us a solid 10 minutes to find the bug in the "current zig" example. However, it also took us quite a while to see the bug in the infix operators example. Finding the misplaced parenthesis is not trivial, even with operators.

We think this is a better target for how this example would be written in status quo zig:

sigma.mul(sqrt(2 * pi)).recip().mul(x.sub(mu).div(sigma).square().div(2).neg().exp());

In some ways, this feels clearer than the mathematical formula. The order of operations is primarily left to right, so there is little spiral reading needed. You don't need to mentally track many parentheses, it's clear what order things happen. It's true that this is very different from the mathematical representation of the formula, but it's certainly not unreadable. So we don't feel that there is a large amount to be gained from this change.

Additionally, the problem of overload selection is difficult. #427 resolved this by doing a lookup into the namespace of the type on the left hand side. But that doesn't work with things like 1 </> complex. This proposal suggests looking in the current namespace. This has some potential benefits, in that you must declare the algebra you will use (with e.g. usingnamespace @import("complex").operators;). However, these cannot be composed at the language level. If you want to do math with both vectors and complex numbers in one function, you need some way to generate a single function that will do overload resolution in userland. This is what the issue's linked gist demonstrates. The feature feels incomplete without this support library, but also this support library would need to be pretty complicated. The one provided only does overload resolution based on the left hand side, which is likely not enough in practice. It also generates error messages that would be very difficult to diagnose and fix. We think that putting comptime execution and usingnamespace on the path between an operator and the function it calls makes the code more difficult to read and understand, and obscures the code enough that it erases the benefits of looking more like the math.

Ultimately, we think that the status quo solution is good enough. The 1 </> complex problem becomes trivial to write and understand when written as complex.recip(), and you can easily find the definition of the called function. Even relative newcomers to the language can understand what is going on, without looking up any documentation. We don't think the benefits of this syntax outweigh the added complexity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants