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: Exponentiation operator #2585

Open
dharmatech opened this issue Aug 17, 2015 · 133 comments
Open

Proposal: Exponentiation operator #2585

dharmatech opened this issue Aug 17, 2015 · 133 comments
Assignees
Labels
Needs Approved Specification This issue needs an LDM-approved specification Proposal champion
Milestone

Comments

@dharmatech
Copy link

dharmatech commented Aug 17, 2015

Consider adding an exponentiation operator that is higher in precedence than the multiplicative operators but lower in precedence than the unary operators.

In the Symbolism computer algebra library, +, *, /, and - are overloaded to create Sum, Product, Quotient, and Difference objects respectively. ^ is also overloaded to create Power objects. However ^, being the logical XOR operator, is lower in precedence than the multiplicative operators. Thus parenthesis must be used in expressions like a + b * (c ^ e).

MSDN - C# Operators

Exponentiation operator in some other languages:

Language Operator
F# **
VB.NET ^
TypeScript **
JavaScript **
Python **
Haskell ^, ^^, **
OCaml **
Swift Not built-in but can be defined
FORTRAN **
Ada **
Nim ^
ALGOL 60 **
APL *
@MouseProducedGames
Copy link

Why not a ** b to represent a ^ b? Prevents problems and confusion with the XOR operator, and should be quickly apparent.

@gafter gafter changed the title Exponentiation operator Proposal: Exponentiation operator Aug 18, 2015
@gafter
Copy link
Member

gafter commented Aug 18, 2015

Is this really so much better than

using static System.Math;

    var result = Pow(a, b);

@paul1956
Copy link

Yes, VB has it so it makes translation easier, also makes mathematical expression easier to read but the second point could be debated.

Sent from my iPhone
I apologize for any typos Siri might have made.
(503) 803-6077

On Aug 18, 2015, at 11:31 AM, Neal Gafter notifications@github.com wrote:

Is this really so much better than

using static System.Math;

var result = Pow(a, b);


Reply to this email directly or view it on GitHub.

@whoisj
Copy link

whoisj commented Aug 18, 2015

Why not a ** b to represent a ^ b? Prevents problems and confusion with the XOR operator, and should be quickly apparent.

👍

@dharmatech
Copy link
Author

Yes, a ** operator would be nice.

However, would it be possible to add this operator and not interfere with the dereference operator (*)? I guess since the dereference operator works on pointers, which aren't used in exponentiation operations, this seems like it would be OK.

F# also uses **. F# - Arithmetic Operators

@HaloFour
Copy link
Contributor

@paul1956

VB's ^ operator literally just translates into a call to Math.Pow so there is no additional benefit to porting between the languages. There is also nothing stipulating feature parity or direct porting of programs between the two languages and there are plenty of features between the two that cannot translate.

The second argument about math operations being easier to read is the more relevant argument in my opinion.

@LMLB
Copy link

LMLB commented Apr 27, 2016

VB's ^ operator can be used in constants.

@MgSam
Copy link

MgSam commented Apr 27, 2016

@LMLB 's argument is the most convincing to me- C# 7.0 is adding digit separators and binary literals for the "defining a constant" use case, and this seems at least as useful as either of those features.

@gregsdennis
Copy link

I think more pertinent to this specific use case is that the operator can be overloaded whereas Math.Pow() cannot.

@JeffreySax
Copy link

@gafter

Is this really so much better than

using static System.Math;

    var result = Pow(a, b);

It is.

Exponentiation is no different from addition or multiplication: it is a common mathematical operation for which there is a universal notation (including precedence rules) that is approximated in programming languages.

C doesn't have an exponentiation operator, while it does have operators for closer to the metal operations like bit-wise xor (^) and integer shifts. It seems java and C# inherited this design more or less by default without fully considering its merits. Most modern languages that are more oriented towards user-experience (Python, VB, R...) do have an exponentiation operator.

Furthermore, while operators are in scope and will be used when the defining type is in scope, this is not true for the static import in your example. It doesn't work within the scope of a type that itself defines a Pow method, such as a complex number type or a vector type.

@HaloFour

VB's ^ operator literally just translates into a call to Math.Pow

Not exactly. There are some small differences. For example, the LINQ expression tree for the VB operator uses a binary expression of type Power (where the Method is Math.Pow), while C#'s version uses a method call.

@gregsdennis It is possible to overload the VB operator from C# manually, i.e. by defining a static op_Exponent method with a SpecialName attribute. You can do the same for F#, but the operator method is called op_Exponentiation (section 4.1 of the F# language spec). This is an unfortunate and annoying inconsistency.

It would be better if this was all consistent: have all common mathematical operations available as operators that can be overloaded in the same way and that makes them accessible to all .NET languages.

@CyrusNajmabadi
Copy link
Member

it is a common mathematical operation

Citation? :)

@CyrusNajmabadi
Copy link
Member

Furthermore, while operators are in scope and will be used when the defining type is in scope, this is not true for the static import in your example. It doesn't work within the scope of a type that itself defines a Pow method, such as a complex number type or a vector type.

If you're in the type itself, you don't need the static import. You can just directly call "Pow(x, y)"

@JeffreySax
Copy link

If you're in the type itself, you don't need the static import. You can just directly call "Pow(x, y)"

But you can't access another type's Pow method that is statically imported without qualifying.

In the code below, class A's M method is successfully called from class B. However, in class C, the presence of the M method prevents it from being considered, unless you qualify it:

namespace N1
{
    public class A
    {
        public static void M(string x) { }
    }
}
namespace N2
{
    using static N1.A;

    public class B
    {
        public void M2()
        {
            M("Test"); // OK
        }
    }

    public class C
    {
        public static void M(int x) { }
        public void M2()
        {
            M(12); // OK
            M("Test"); // 'Argument 1: Cannot convert from string to int'
            N1.A.M("Test"); // OK
        }
    }
}

Citation? :)

This is like someone from Texas asking a Canadian for a citation that snow is a common weather phenomenon. :) Just because something isn't common in your experience doesn't mean it's not common for other people.

But since you asked: the code sample in the documentation for the static using directive contains two squares, written out as multiplications.

@MgSam
Copy link

MgSam commented Jan 19, 2017

Is exponentiation more common or important than square root? Where do you draw the line of which operations are important enough to deserve an operator?

The only benefit, IMHO, is that you can use mathematical operators statically. However, I think the constexpr feature (dotnet/roslyn#15079) is a lot more general so I'd be in favor of that to fulfill that use case rather than adding a new operator.

@iam3yal
Copy link
Contributor

iam3yal commented Jan 19, 2017

Related #14665 but like @MgSam said please check #15079, if you like the idea join the discussion. :)

@CyrusNajmabadi
Copy link
Member

Just because something isn't common in your experience doesn't mean it's not common for other people.

And just because it's common for you, doesn't mean it's common enough to warrant inclusion in the language. :)

This is like someone from Texas asking a Canadian for a citation that snow is a common weather phenomenon

No. This is like a member of the LDM asking you what the basis of your claim is.

@CyrusNajmabadi
Copy link
Member

Claim: the scientific computing community needs better representation on the C# LDM.
Citation: this thread.

This thread has 16 comments over an entire year of being open. There are only about 8 non-MS people even commenting on this. This seems to have very little interest (contrast with threads with dozens of people and hundreds of messages). Furthermore, the existence of interest does not mean that we should ,or will, take a request. We have literally hundreds to thousands of small requests like this. We definitely will not be taking the vast majority of them. We have to actually consider how important this is to the entirety of our ecosystem. Right now, the interest and impact of this feature are both low.

@CyrusNajmabadi
Copy link
Member

I have now seen that the comment i replied to has been deleted. So i'd like to nip this side of the conversation in the bud. My overall point is simply that this seems to be a feature without that much impact with very little interest.

--

From a technical perspective, i'm also worried about breaking changes here. Today, this is already legal code:

unsafe void X(int* p, int c)
{
    var v = c**p
}

We'd likely have to do some sort of unpleasant contortions to ensure we didn't break existing code like this.

@JeffreySax
Copy link

I agree that the syntactic ambiguity of ** is a large negative to the point where it's likely not worth the trouble.

D and Haskell use ^^, which isn't great, but isn't terrible, either. It does not have any syntax issues that I'm aware of.

To give you an idea of how common the operation is, I looked at one of our projects, which is heavy on technical computing. About 0.9% of code lines contains an exponentiation. It is much more common than shifts or xors. Element-wise operations on arrays/collections are also quite common.

Although it is not primary evidence of commonality, the fact that the operator is included in many programming languages suggests that others found it sufficiently common to include in the language. Some of these languages had very tight constraints, like Altair BASIC, which fit in 8192 bytes (see manual (PDF), page 27).

Microsoft provides tooling for many languages that have the exponentiation operator:

@dharmatech
Copy link
Author

dharmatech commented Jan 24, 2017

It's not clear to me that ** as an operator would cause an issue due to a conflict with the dereferencing operator.

Currently, in C#:

  • & is used to obtain an address
  • & is used for logical AND
  • && is conditional AND

There clearly aren't any implementation issues there.

Considering the example given above:

var v = c**p;

The above ** cannot be interpreted as a exponentiation operator because that would require treating p as an exponent and p is a pointer.

I'm fairly neutral regarding ** vs ^^. However, ** seems more common. If there isn't a strong technical reason against **, it seems like a good option.

@CyrusNajmabadi
Copy link
Member

It's not clear to me that ** as an operator would cause an issue due to a conflict with the dereferencing operator.

It would cause a syntactic ambiguity. Today we parse "a**b" as "a * (*b)" i.e. "a times the deref of b". With this new operator, we'd now parse that as "a ** b".

We do not change parse trees for correct code. So we would have to still parse things like the old way, but at semantics time, see if we had this pattern, and we'd have to determine what would be the right thing to do.

@dharmatech
Copy link
Author

It would cause a syntactic ambiguity. Today we parse "a**b" as "a * (*b)" i.e. "a times the deref of b". With this new operator, we'd now parse that as "a ** b".

We do not change parse trees for correct code. So we would have to still parse things like the old way, but at semantics time, see if we had this pattern, and we'd have to determine what would be the right thing to do.

If the code isn't in unsafe mode, then ** is unambiguously exponentiation.

If you are in unsafe mode and the right operand of ** is a pointer, then dereference and multiply. Otherwise, exponentiation.

@CyrusNajmabadi
Copy link
Member

If you are in unsafe mode and the right operand of ** is a pointer, then dereference and multiply. Otherwise, exponentiation.

yes. this could be done. But now we have the same syntax meaning different things with operators. I'm not a fan of this. ++ doesn't mean something different depending on context. i.e. we don't say "oh, in a++b, there is no ++ operator for 'a', this this is 'a added to plus-b'".

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Jan 25, 2017

It's not clear to me that ** as an operator would cause an issue due to a conflict with the dereferencing operator.

There is clearly a conflict here. The question is: is that acceptable? For example, there's a syntactic ambiguity with: (A)-B. That could be "minus B, casted to the A type". Or it should be "the value of parenthesized expr A, minus the value of B". We went with the former for C#. But once we decide on something, we don't change it**.

Here, we'd have to always parse this as "A times the deref of B". But we'd then have to have some special rule that says "if you have that, and there is no space between the operators, and B is derefable, then behave like so, otherwise, behave like so". That's pretty janky, and not something we've ever done before really.

--

** Ok, we also changed it for generic parsing: X(A<B, D>(E))

We changed the parsing here to make this into a nested invocation call, instead of two relational expressions. However, we're very wary of changing parsing. And i'm personally of hte opinion that it's very janky for us to special case the treatment of "a ** b" to processed differently depending on if 'b' is dereferencable or not.

@svick
Copy link
Contributor

svick commented Jan 25, 2017

@CyrusNajmabadi

it is a common mathematical operation

Citation? :)

API Port telemetry says that Math.Pow() is used in 11 % of apps. Not sure if that is common enough.

@MgSam

Is exponentiation more common or important than square root? Where do you draw the line of which operations are important enough to deserve an operator?

The same data says that Math.Sqrt() is used in 7 % of apps. So it is less common than exponentiation, but not by much. But while it's fairly easy to come up with decent operators for exponentiation (whether it's ** or ^^), I don't see what would be a good operator for square root (I don't think √ a.k.a. U+221A SQUARE ROOT is an option).

@CyrusNajmabadi
Copy link
Member

API Port telemetry says that Math.Pow() is used in 11 % of apps. Not sure if that is common enough.

I'd want to know how often it was actually used. If 11% of all apps use the call once, then i have no problem with them using Math.Pow. :)

@gregsdennis
Copy link

A sqrt operator would be superfluous. It's just x^^0.5; a special case of <1 exponentiation.

@CyrusNajmabadi
Copy link
Member

A ** operator would be superfluous. It's just a call to Math.Pow. :)

@gregsdennis
Copy link

gregsdennis commented Jan 25, 2017

An exponentiation operator is generic (general-use, not type generic), not a special case like a sqrt operator would be.

@HaloFour
Copy link
Contributor

@TahirAhmadov

Yes, but in other cases, the "root" (what is being exponentiated) is an expression, and multiplying becomes complicated.

For what I believe to be an exceptionally rare scenario I don't see why this is a problem. The language shouldn't optimize for cases that almost never occur.

Yes a few math constants don't make a huge difference, but it's not zero.

It's close enough. And still, handling this for one specific case does exceptionally little to move the bar here compared to something like constant expressions which could enable the use of Math.Pow and the plethora of other arithmetic methods at compile time instead of having to debate over language changes for each.

@TahirAhmadov
Copy link

You're right, it's not possible to define a const of that expression. But how often do you need a constant that relies on exponentiation?

That's the thing - I think about this less in terms of "how often is it used today" and more "how do we bring many more to the language".

What about all of the other mathematical expressions that would require the same treatment?

What are those examples? I don't see anything offered or needed quite as fundamental as exponents.

For the constant scenario I'd rather the language and runtime teams explore constant expressions so that the compiler can evaluate expressions containing method invocations during compile time.

I fully agree with this. There is either a discussion or an issue about this - I can't remember which one it is at the moment. I would definitely support that one being championed ahead of the exponent operator.

@TahirAhmadov
Copy link

Suggestion for those working on heavily math projects currently: define some extension methods on float, double, etc.:

public static double Pow(this double This, double exponent) => Math.Pow(This, exponent);
double area = side.Pow(2); // easier to write than Math.Pow(side, 2) and can almost be thought of as an operator placeholder

@HaloFour
Copy link
Contributor

HaloFour commented Nov 11, 2021

@TahirAhmadov

That's the thing - I think about this less in terms of "how often is it used today" and more "how do we bring many more to the language".

I am highly suspicious of this statement. I don't believe the addition of any specific language support will move this needle at all. It's not the language that matters, it's the ecosystem and frameworks around it.

I fully agree with this. There is either a discussion or an issue about this - I can't remember which one it is at the moment. I would definitely support that one being championed ahead of the exponent operator.

#2379

Also, with .NET 6.0 many of the Math functions can now participate in JIT constant folding, including Math.Pow:

https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/

@TahirAhmadov
Copy link

TahirAhmadov commented Nov 11, 2021

That's the thing - I think about this less in terms of "how often is it used today" and more "how do we bring many more to the language".

I am highly suspicious of this statement. I don't believe the addition of any specific language support will move this needle at all. It's not the language that matters, it's the ecosystem and frameworks around it.

It's all of the above; that's why I also mentioned REPL. I guess I hope the takeaway from this entire thread is not just that some people think exponents are valuable enough to warrant an operator, but that also there are other languages like Python which are inferior in a million ways to C# yet have a lead in the market, due to their simplicity in some facets, particularly for hobbyists and non-programmer scientists. I won't argue that this operator would close that gap all by itself, not even close; but we should think about ways to do so. Obviously C# is NOT a language for beginners, but I think we can move it in that direction with a few simple steps without sacrificing the quality that it has now.

Also, with .NET 6.0 many of the Math functions can now participate in JIT constant folding, including Math.Pow:

https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/

Hmmm doesn't this entirely undo the need for "constant expressions" in C#?

@HaloFour
Copy link
Contributor

@TahirAhmadov

I would argue that pyspark and scikit do infinitely more to advance Python's adoption in the data sciences space than anything the language does itself. Scala is also very popular in that field and IMO Scala is substantially more complicated than C#.

C# does have a REPL but I agree that improved tooling would help drive adoption. I'd like to see something akin to xcode Swift Playground that can combine features from C# Interactive, LINQPad and SharpLab into a very easy to use tool to write C# snippets and visualize their execution.

@tannergooding
Copy link
Member

That's the thing - I think about this less in terms of "how often is it used today" and more "how do we bring many more to the language".

An operator for exponents wouldn't be the thing to do this.

The main argument being given above is around constants and that applies to so many things that aren't Math.Pow. This would be a larger push around something like constexpr (#1028) support which would allow any supported method to be annotated as a constant expression and therefore be useable in constant expressions.

Additionally, the use of things like Pow as integers and in constants are particular are rare and limited. When they are used, they almost always float or double; otherwise they are powers of two or ten for int and rarely uint/long/ulong or other powers. Outside float/double the ranges that can be used are super limited and can actually be boiled down to a fairly small static table of valid inputs.

Its not uncommon to just right something like:

  • `const int Name = 1 << 10; // 1024
  • `const int Name = 1024; // 2^10
  • etc

or to have a test to validate the constant value vs the computed expression, etc. Which is the generally more usable scenario where you have some "magic number".

Hmmm doesn't this entirely undo the need for "constant expressions" in C#?

Yesn't. There are still differences between C# constants and JIT constants including where they can be used and not.

@NetMage
Copy link

NetMage commented Nov 12, 2021

Additionally, the use of things like Pow as integers and in constants are particular are rare and limited. When they are used, they almost always float or double; otherwise they are powers of two or ten for int and rarely uint/long/ulong or other powers. Outside float/double the ranges that can be used are super limited and can actually be boiled down to a fairly small static table of valid inputs.

Is that in C# or in other languages as well? It wouldn't be unsurprising that it isn't used much in C#, there isn't a constant supported operator for it.

@tannergooding
Copy link
Member

Is that in C# or in other languages as well? It wouldn't be unsurprising that it isn't used much in C#, there isn't a constant supported operator for it.

Even in other languages. C/C++ don't because pow/powf aren't constexpr either, namely because many of these functions have a side effect in setting errno.

Ignoring x^1 and 1^x (which are just x and 1, respectively), there are 48,036 valid powers that are between 0 and int.MaxValue.

Now that might sound like a lot, but lets actually break it down:

  • 2 goes up to the power of 30
  • 3 goes up to the power of 19
  • 4 goes up to the power of 15
  • 5 goes up to the power of 13
  • 6 and 7 go up to the power of 11
  • 8 goes up to the power of 10
  • 9 and 10 go up to the power of 9
  • 11 through 14 go up to the power of 8
  • 15 through 21 go up to the power of 7
  • 22 through 35 go up to the power of 6
  • 36 through 73 go up to the power of 5
  • 74 through 215 go up to the power of 4
  • 216 through 1290 go up to the power of 3
  • 1291 through 46340 go up to the power of 2

This means that there is a very very small window of valid integer values that can be represented as powers in the first place; most of which (~45,000 of them; which is ~93.7%) are only valid as a power of two (e.g. x * x).

These values are also very specific and representa very small portion of the overall 2.14 billion positive integers you can otherwise have. Their usage outside 2^x and 10^x are also going to be fairly limited, both of which have better alternatives that are perhaps more clear (e.g. 2 << x for powers of two or 10_000_000 or similar for powers of ten).

@tannergooding
Copy link
Member

You can do similar logic for other ranges, like uint or long or ulong and see that, similarly, the majority of valid powers are x^2.

For ulong for example the highest is 4294967295^2, then 2097152^3, 55108^4, 6208^5, and continuing to rapidly decrease in range from there; up to 2^63

@cordasfilip
Copy link

Something interesting to point out in regards to this is how Postgres uses Operators. To me an interesting way of solving this issue would be to allow people to override a bunch of symbols so they can use some strange operator syntax on classis they create. For example I don't see the need for anyone to use Pow(2,5) since you can calculate it before hand but something like x^2 could make sense for a custom math class. So if they could create a random operator on the class like |/ would be nice for a custom math number class or even a variable class.

@adamyakes
Copy link

Something I feel is missing from a lot of this discussion is just how ugly math can get if you have to use Math.Pow everywhere. Here's some Fortran that's an implementation of the Churchill correlation.

ReInv = 1.0 / Re
A = (2.457 * LOG(1.0 / ((7.0 * ReInv) ** 0.9 + 0.27 * e_over_d))) ** 16
B = (37530.0 * ReInv) ** 16

FanningFactor = 2.0 * ((8.0 * ReInv) ** 12.0 + 1.0 / (A + B) ** (1.5)) ** Exponent

That same code written in C#

ReInv = 1.0 / Re
A = Math.Pow(2.457 * Math.Log(1.0 / (Math.Pow(7.0 * ReInv, 0.9) + 0.27 * e_over_d))), 16)
B = Math.Pow(37530.0 * ReInv, 16)

FanningFactor = 2.0 * Math.Pow(Math.Pow(8.0 * ReInv, 12.0) + 1.0 / Math.Pow(A + B, 1.5), Exponent)

To me, the main issue is that Math.Pow is effectively a prefix notation. Mixing prefix and infix inside a formula makes it very difficult to parse. It also doesn't read well: x = y ** z reads "x is y to the power of z". How am I supposed to read z = Math.Pow(y, z)?

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Apr 12, 2024

To me, the main issue is that Math.Pow is effectively a prefix notation.

You could add your own extension method to make this infix. then you'd have x = y.Pow(z) which reads well to me.

You full example would be:

A = (2.457 * LOG(1.0 / ((7.0 * ReInv).Pow(0.9) + 0.27 * e_over_d))).Pow(16)
B = (37530.0 * ReInv).Pow(16)

FanningFactor = 2.0 * ((8.0 * ReInv).Pow(12.0) + 1.0 / (A + B).Pow(1.5)).Pow(Exponent)

Which also reads fine to me.

@tannergooding
Copy link
Member

There is also notably not a whole lot of difference between the two. You're basically trading ** for , and making the fact that you're doing exponentiation more clear by declaring it up front.

The entire expression as written (for either Fortran or C#) is pretty hard to read. If I saw that code in a PR to dotnet/runtime, I'd like flag it and ask for it to be explicitly broken apart so that it's easier to read and debug. I would likely also require an explanatory comment to exist which gives the mathematical algorithm it corresponds to and an explanation of how its accounting for binary floating-point inaccuracies.

@adamyakes
Copy link

To me, the main issue is that Math.Pow is effectively a prefix notation.

You could add your own extension method to make this infix. then you'd have x = y.Pow(z) which reads well to me.

That's a good idea - I think I'll write that extension method. It more or less solves the readability problem for me.

@heartacker
Copy link

heartacker commented Apr 12, 2024

pow *>
sqrt *<

@adamyakes
Copy link

There is also notably not a whole lot of difference between the two.

I'd have to disagree with this. Let's take a simpler example. c = a**2 + b**2 reads left to right as "c is a squared plus b squared", or "c is a to the power of two plus b to the power of two", whereas c = Math.Pow(a, 2) + Math.Pow(b, 2) reads "c is the power of (?) a and two plus the power of b and two". It doesn't really work.

You're basically trading ** for ,

This makes makes my point for me - you're trading a context-giving operator, **, for a generic argument separator. You lose the context of what operation is being done.

and making the fact that you're doing exponentiation more clear by declaring it up front.

Putting something up front doesn't make it clearer, especially when sub-expressions are involved. We read infix operators as infix operators. Clarity is lost, not gained, by rearranging standard math operators to fit syntax. Note, things like sin(x) don't break this because those are read "sin of x" already. Granted, a significant portion of this issue can be resolved by writing an extension method as suggested here, but it's bending over backwards to argue that the method syntax is better.

The entire expression as written (for either Fortran or C#) is pretty hard to read. If I saw that code in a PR to dotnet/runtime, I'd like flag it and ask for it to be explicitly broken apart so that it's easier to read and debug.

Math-heavy code generally seems difficult at first, but breaking down equations doesn't always make them easier to read. You're inventing new variables for sub-expressions that don't necessarily mean anything by themselves. What about the distance formula ($\sqrt{(x_1-x_2)^2 + (y_1-y_2)^2}$)? Since we have to write Math.Sqrt(Math.Pow(x1-x2, 2) + Math.Pow(y1-y2, 2)) instead of sqrt((x1-x2)**2 + (y1-y2)**2), what are we going to break it down to? Replace x1-x2 with delta_x? Or Math.Pow(x1-x2, 2) with x_delta_squared? Not every component has a name. You're in fact obfuscating the resulting expression by splitting it up.

I would likely also require an explanatory comment to exist which gives the mathematical algorithm it corresponds to and an explanation of how its accounting for binary floating-point inaccuracies.

Sure, but has nothing to do with whether Math.Pow or ** is used. The code in question is actually inside a function named after the equation it calculates and has documentation.

@CyrusNajmabadi
Copy link
Member

I'd have to disagree with this. Let's take a simpler example. c = a2 + b2 reads left to right as "c is a squared plus b squared", or "c is a to the power of two plus b to the power of two", whereas c = Math.Pow(a, 2) + Math.Pow(b, 2) reads "c is the power of (?) a and two plus the power of b and two". It doesn't really work.

c = a.Pow(2) + b.Pow(2) reads eft to right as "c is a to the power of two plus b to the power of two", and seems really clear to me.

but it's bending over backwards to argue that the method syntax is better.

I honestly disagree. This reads great to me. Maybe it's not 'better' per se. But it's easy and extremely clear. So there's little motivation to come up with an alternate given that.

Since we have to write Math.Sqrt(Math.Pow(x1-x2, 2) + Math.Pow(y1-y2, 2)) instead of sqrt((x1-x2)**2 + (y1-y2)**2)

Sqrt((x1-x2).Pow(2) + (y1-y2).Pow(2)) honestly seems great to me.

@tannergooding
Copy link
Member

tannergooding commented Apr 15, 2024

You lose the context of what operation is being done.

You have lost no context, the method name makes it very clear what you're doing.

In the case of a ** b you need to learn that this syntax, which is programming specific (and is not one of the universally agreed upon operators), means a to the power of b; Where-as Pow(a, b) makes it very clear that you're raising a to the power of b. You cannot program in superscript, so you cannot do ab and the more common text syntax of a^b is taken by exclusive-or, so you cannot do that either.

You then have to rationalize that ** is itself somewhat ambiguous when it comes to the overall context of the language because * is used to mean multiple and it's used to mean dereference. You then may have to rationalize what it means in context of other dual operators (++ means increment by one, for example).

Beyond that, you also have to rationalize all the problems and edge cases that exist with this operator. The fact that it's not efficient for integers, that it can trivially overflow for integers (as per a different comment above, you get around 45k valid combinations and of those, ~42.1k of them are x * x, or rather x2), that it requires a deterministic implementation for floating-point (which most runtimes don't actually provide today), etc.

Putting something up front doesn't make it clearer, especially when sub-expressions are involved.

Yes, it can make it harder to rationalize about, but so can very complex arbitrary expressions. That's why it's typical to break your code apart for readability/maintainability.

Math.Sqrt(Math.Pow(x1-x2, 2) + Math.Pow(y1-y2, 2))

This is a terrible way to implement distance. Not only is it incredibly inefficient (calling a function, Pow, for something that is x * x, which can prevent many optimizations) and so you wouldn't ever do this for such a common/core API, but it doesn't account for inaccuracies and so needlessly produces a less correct result.

Even if you were defining it naively and not accounting for precision, you would want to explicitly store x1 - x2 into a temporary so that you can just do:

double dX = x1 - x2;
double dy = y1 - y2;
return double.Sqrt((dX * dX) + (dy * dy))`

Even more ideally, you've appropriately exposed a Point or Vector2 type and exposed this via a helper API, rather than doing it inline, so all the user sees is Vector2.Distance(x, y) and the "complexity" of doing it efficiently or correctly is abstracted away (the BCL does define such an API), and so you get (naively):

public static Vector2 Distance(Vector2 x, Vector2 y)
{
    float sum = Vector2.Sum(x - y);
    return float.Sqrt(sum);
}

has documentation.

Documentation, which exists for the consumer of the API is not the same as code comments, which exists for the implementor/maintainer of an API. One is about the public surface area and the other is about the inner implementation details.

@CyrusNajmabadi
Copy link
Member

is itself somewhat ambiguous when it comes to the overall context of the language because * is used to mean multiple and it's used to mean dereference

Note, it's not somewhat ambiguous, It's very ambiguous. For example, this is legal today:

using System;
public class C {
    public unsafe void M(int b, int* a) {
        var x = b ** a;
    }
}

:D

@tannergooding
Copy link
Member

What might be more interesting to discuss would be if the language could allow for certain static methods exposed on the type to be accessed like instance methods.

That is to say, today you have instance methods (T M()) defined on T and so for T x you can do x.M(). You can have static methods defined on T (static T M(T value)) and can do T.M(x). You can then define static methods on another class U and define them as extension methods using the this keyword (static T M(this T value)). The extension methods can also be called as U.M(x) if a user desires.

There is, however, no way to allow for a static method defined on T to be considered as an extension method for T and thus while we've defined double.Pow(double x, double y) users are forced to call it via the static syntax.

Perhaps if the language opted to relax its lookup rules to be static class and static methods on T, then we could define double.Pow(this double x, double y) and users could choose if they prefer double.Pow(x, y) or x.Pow(y) (assuming API review also approved such support).

@adamyakes
Copy link

adamyakes commented Apr 15, 2024

I'd have to disagree with this. Let's take a simpler example. c = a2 + b2 reads left to right as "c is a squared plus b squared", or "c is a to the power of two plus b to the power of two", whereas c = Math.Pow(a, 2) + Math.Pow(b, 2) reads "c is the power of (?) a and two plus the power of b and two". It doesn't really work.

c = a.Pow(2) + b.Pow(2) reads eft to right as "c is a to the power of two plus b to the power of two", and seems really clear to me.

but it's bending over backwards to argue that the method syntax is better.

I honestly disagree. This reads great to me. Maybe it's not 'better' per se. But it's easy and extremely clear. So there's little motivation to come up with an alternate given that.

Since we have to write Math.Sqrt(Math.Pow(x1-x2, 2) + Math.Pow(y1-y2, 2)) instead of sqrt((x1-x2)**2 + (y1-y2)**2)

Sqrt((x1-x2).Pow(2) + (y1-y2).Pow(2)) honestly seems great to me.

@CyrusNajmabadi My comment was quoting from and responding to @tannergooding's comment - I actually replied here and also acknowledge in replying that the infix nature of an extension method as you suggested is much more readable.

@rummelsworth
Copy link

Math-heavy code generally seems difficult at first, but breaking down equations doesn't always make them easier to read. You're inventing new variables for sub-expressions that don't necessarily mean anything by themselves. [...] Not every component has a name. You're in fact obfuscating the resulting expression by splitting it up.

I work with a lot of old ancient mathematical code on a daily basis. Decomposing expressions, complex or not, is routine for easier debugging, readability, and sometimes discovering improvements. Sometimes there are decent names for the intermediate values (deltaX isn't terrible), sometimes there aren't (e.g. cse for "common subexpression"). In all cases, obfuscation due to decomposition is easy to handle by adding comments. Sometimes it's just a citation (including equation number, page number, section heading, etc) to the paper, book, or URL source in the literature. Oftentimes the citation can be supplemented with some math markup (e.g. plain KaTeX for copy-paste previewing in VS Code, or you can try an extension like TeX Comments 2022+). These comments and the markup can be at whatever granularity the code needs to achieve readability. With this, the code can take its best shape while still being firmly tethered to the formatted presentation/description of its specific logic.

It'd be nice if C# comments (including XML doc) had more convenient built-in support for Markdown-like and/or TeX-like syntaxes. Comments are at least as important as "real code", and it feels like they're just ignored for planning and feature development.

@adamyakes
Copy link

@tannergooding I think your reply really misses the mark on the whole point of this discussion: whether an exponentiation operator would be a useful addition to the C# language. Arguing about whether a specific example is optimal in terms of speed or accuracy doesn't have anything to do with whether an operator adds value. The purpose of my examples was to show the benefit of an operator.

Yes, code needs documentation (obviously). Yes, penny pinching CPU cycles could lead someone to trade the brevity of x**3 for x * x * x. Yes, floating point precision should be weighed and accounted for. But what does that have to do with adding an operator? To that end, I won't be addressing the criticism of my specific implementation of the distance formula nor the proposed API changes.

You lose the context of what operation is being done.

You have lost no context, the method name makes it very clear what you're doing.

Here's my original example

A = Math.Pow(2.457 * Math.Log(1.0 / (Math.Pow(7.0 * ReInv, 0.9) + 0.27 * e_over_d))), 16)
// ^ what does this comma mean?

This is the context to which I was referring: the mental ability to parse what's going on. Is all the information technically there for you? Sure. Should the code be broken down? Probably! But it's (subjectively) harder to read, and thus to me serves as a good example. I will reiterate from other replies: I think the extension method (expression).Pow(expression) solves a lot of the readability concerns. But it also serves to point out we've basically invented an infix operator.

In the case of a ** b you need to learn that this syntax, which is programming specific (and is not one of the universally agreed upon operators), means a to the power of b; Where-as Pow(a, b) makes it very clear that you're raising a to the power of b. You cannot program in superscript, so you cannot do ab and the more common text syntax of a^b is taken by exclusive-or, so you cannot do that either.

You then have to rationalize that ** is itself somewhat ambiguous when it comes to the overall context of the language because * is used to mean multiple and it's used to mean dereference. You then may have to rationalize what it means in context of other dual operators (++ means increment by one, for example).

I'm not advocating for ** specifically, just something. Of course Pow is a great, explicit name, but programming is filled syntax you need to learn. Fortran uses () instead of [] for arrays, Haskell uses instead of () for function application. We've got !=, <>, and /= for inequality between languages. (And C# uses /= for compound assignment!) C# has 46 contextual keywords, allows operator overloading, and has introduced something like four new different kinds of string syntax since I've been using it. Difficulties in disambiguating ** aside, "learning syntax" itself is not a concern I share.

@CyrusNajmabadi
Copy link
Member

But it also serves to point out we've basically invented an infix operator.

I don't really agree with this. The idea here is that we're not inventing anything. We're using what the language already has and supports. :) And our preference is that if the lang already has a good solution for a problem, we'd prefer to keep using that versus introducing a new solution (cuz now you have multiple :)). So we need strong reasons to add features. And that includes strong arguments for why the existing options available at the language level are not sufficient, and thus why something new does need to be "invented" :)

@tannergooding
Copy link
Member

But what does that have to do with adding an operator?

A lot.

If the operator is trivially rejected because it is the wrong thing nine times out of ten or has other super common problems, then that is a reason to not expose the operator.

If there is then a simple alternative, like defining an infix API, which solves the problem and introduces less complexity to the language, that then becomes a better direction. Especially if that also gives users more flexibility for the entire set of math APIs rather than being scoped to simply Pow

// ^ what does this comma mean?

You can trivially create complex expressions even with purely using operators that are hard to read/understand and even where users will commonly get the logic wrong because they misunderstand things like order of operations.

Consider, for example, the following which is only using multiplication and addition. It is part of the real algorithm used to implement double.Log(double x):

(((r04 * C20) + ((((r * C19) + C18) * r02) + ((r * C17) + C16))) * r16) + (((((((r * C15) + C14) * r02) + ((r * C13) + C12)) * r04) + ((((r * C11) + C10) * r02) + ((r * C09) + C08))) * r08) + (((((r * C07) + C06) * r02) + ((r * C05) + C04)) * r04) + ((((r * C03) + C02) * r02) + r)

Which is much more readable, but still very verbose and hard to parse/understand, if you break it apart like this (and it could be more readable/understandable with locals and/or the surrounding comments in the actual code):

     (((r04 * C20)
    + ((((r * C19) + C18) * r02)
      + ((r * C17) + C16))) * r16)
 + (((((((r * C15) + C14) * r02)
      + ((r * C13) + C12)) * r04)
    + ((((r * C11) + C10) * r02)
      + ((r * C09) + C08))) * r08)
   + (((((r * C07) + C06) * r02)
      + ((r * C05) + C04)) * r04)
    + ((((r * C03) + C02) * r02) + r)

Code has to be broken apart for readability/legibility. This is even true in real math and is the whole reason Algebra has named variables.

@GeirGrusom
Copy link

is itself somewhat ambiguous when it comes to the overall context of the language because * is used to mean multiple and it's used to mean dereference

Note, it's not somewhat ambiguous, It's very ambiguous. For example, this is legal today:

using System;
public class C {
    public unsafe void M(int b, int* a) {
        var x = b ** a;
    }
}

:D

The probably one (now two) people who wrote a line that looked like that can afford a compilation error I think :)

I have felt the few times I write exponentiation it does look ugly, and tears up expressions a bit, especially when I'm translating from a math formula. Not a hill I would die on, but I wouldn't mind an exponentiation operator either.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Approved Specification This issue needs an LDM-approved specification Proposal champion
Projects
None yet
Development

No branches or pull requests