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

Issue: conditional operator ?: cannot resolve implicit conversion to one another #563

Closed
lachbaer opened this issue May 10, 2017 · 16 comments

Comments

@lachbaer
Copy link
Contributor

lachbaer commented May 10, 2017

While trying something I ran into the following phenomenon:

struct NNDouble {
    double Value;
    [ ... ]
    public static implicit operator double(NNDouble value) => Validate(value.value);
    public static implicit operator NNDouble(double value) => new NNDouble(value);
    public static bool operator !=(NNDouble left, NNDouble right)
        => (left.IsValid == right.IsValid) && (left.value != right.value);
}
struct Fraction {
    [ ... ]
    private NNDouble _denominator;
    public double Denominator
        => (_denominator != null) ? _denominator : 1d; // Error here
}

The marked line gets "Error CS0172: Type of conditional expression cannot be determined because 'NNDouble' and 'double' implicitly convert to one another".

Of course this could be helped by explicity converting one of the operands.

But from a naive point of view, shouldn't the compiler try to implicitly convert the second operand to the result type first, making that the target type of the whole expression? Or in case there is no available return type (var, dynamic) try to convert the third operand to the second one first?

Examples:

// `_denominator` can be implicitly converted to `double`, the result is `double`;
double result = (_denominator != null) ? _denominator : 1d; 

// `_denominator` has result type, `1d` can be implicitly converted to `NNDouble`, 
// the result is `NNDouble`;
var result = (_denominator != null) ? _denominator : 1d; 

At least we are reading this language left-to-right, and using the second operand to evaluate the whole result type seems logical to me this way.

(Edit: changed "first" and "second" operand to "second" and "third" operand, according to the specifications.)

@lachbaer
Copy link
Contributor Author

Here's what the specs say:

  • If X and Y are the same type, then this is the type of the conditional expression.
  • Otherwise, if an implicit conversion (Section 6.1) exists from X to Y, but not from Y to X, then Y is the type of the conditional expression.
  • Otherwise, if an implicit conversion (Section 6.1) exists from Y to X, but not from X to Y, then X is the type of the conditional expression.
  • Otherwise, no expression type can be determined, and a compile-time error occurs.

In terms of the specs the produced error is in order. But I would like to know the objective reason behind this definition. It somehow feels weired without knowing the background.

@jnm2
Copy link
Contributor

jnm2 commented May 10, 2017

As to the background, I think the general answer is that C# avoids target-typing (outside-in). If everything is inferred inside-out it keeps inference straightforward and unidirectional, which is a benefit both to C# language designers and consumers.

@lachbaer
Copy link
Contributor Author

But even if that reason is true it won't argue against converting the third operand to the second first. Btw, a complex type balancing is done at the null-coalescing operator ?? where the position of the operand/type is taken into consideration.
(As soon as default; (without type) is introduced there will be a kind of outside-in. )

@HaloFour
Copy link
Contributor

Might be nice if there were some kind of rules regarding precedence/inference of the operands, at least in cases of ambiguity. For example in the cases above I'd kind of expect that if the compiler couldn't determine the type of expressions between two types that are implicitly castable to each other that it might favor the type of the first operand.

When it comes to expressions of null or default I think the rules would be slightly different in that the compiler would always use the type of the other operand:

var value1 = (condition) ? 123 : default;
var value2 = (condition) ? default : 123;

What I wouldn't expect is for the target type to play into that inference. For example:

double result = (_denominator != null) ? _denominator : 1d; 

I'd expect the conditional expression to evaluate to type NNDouble which is then implicitly cast to double.

@HaloFour
Copy link
Contributor

@lachbaer

In terms of the specs the produced error is in order. But I would like to know the objective reason behind this definition. It somehow feels weired without knowing the background.

I'm going to assume that this is because looking at said expression you might not immediately understand what it's going to do and you might make the wrong assumption. You can make the case that it should immediately cast _denominator to double and I can make the case that it should immediately cast 1d to NNDouble and then cast the result of the expression to double. Neither are inherently right and depending on the exact code and order of operands you may accidentally read it either way. Preventing such ambiguity requires the developer to explicitly cast which would eliminate all doubt.

@lachbaer
Copy link
Contributor Author

@HaloFour Because we read this language from left-to-right I just find it logical if the second operand's type had precedence over the third's. If operators (+, *) have the same precedence they are processed left-to-right.

@jnm2
Copy link
Contributor

jnm2 commented May 10, 2017

It's not intuitive to me that one ternary operand should be preferred over the other. I mean sure, I'd live with it, but it feels like a wart. I like knowing that the order of the two operands is just about readability and not about importance.

@bondsbw
Copy link

bondsbw commented May 10, 2017

Perhaps ternary expressions could be treated as a union type which is reduced by the target type. So for this example:

double result = (_denominator != null) ? _denominator : 1d;

the following would be pseudo steps for evaluating the type:

  1. Expression is ternary, so evaluate both inputs:
    a. _denominator is NNDouble
    b. 1d is double
  2. Ternary expression types are not equal, so the preliminary expression type is the union type NNDouble | double
  3. Target type is double, so attempt to use it to reduce the components of the union type from the ternary expression:
    a. _denominator is implicitly convertible to double
    b. 1d is double
  4. Ternary expression types are equal (double), so the final ternary expression type is the resolved type double
  5. RHS double is directly assignable to LHS double; type conversion succeeds

(In this context, the union type is resolved prior to assignment. This does not cover the topic of unresolved union types, i.e. var result = (_denominator != null) ? _denominator : 1d;.)

@lachbaer
Copy link
Contributor Author

so the preliminary expression type is the union type NNDouble | double

That will be 'ValueType'.

@bondsbw
Copy link

bondsbw commented May 10, 2017

@lachbaer I'm not sure what you mean. A union type represents an "or" between two or more types.

@lachbaer
Copy link
Contributor Author

Isn't it the common base type? Otherwise you get the same ambiguity again, shall the common type be A or B when they both can implicitly converted to one another?

@bondsbw
Copy link

bondsbw commented May 10, 2017

@lachbaer The ambiguity exists at step 2. But a target type was specified, so the ambiguity is resolved in steps 3-4. There is no need to find a base type because the implicit conversion to double already exists (step 3).

Now if we write var result = ..., the ambiguity would still exist because there is no target type. Steps 3+ would not complete. As I said, that case isn't covered here. It might result in an error as it does today, or perhaps the discussions on union/intersection types (such as #399) would kick in.

@svick
Copy link
Contributor

svick commented May 11, 2017

I think I would find it pretty confusing if a ? b : c gave different result than !a ? c : b.

@taori
Copy link

taori commented May 3, 2018

Old issue - i know:

I see why people argue for this to be confusing in general assignment scenarios - but what about scenarios where there is no confusion, because the type casting is deterministic if a type is used instead of using var.

Is the idea of target typed implicit casting still off the table currently? Personally i always think of ternary expressions as a nice way to write code in a more compact way, if the expression contents are short.

Having to write code like this

grafik

instead of being able to use ternary expression seems unnecessarily verbose.

@alrz
Copy link
Member

alrz commented May 3, 2018

I think #877 can resolve this since there is a target-type in all examples (except for var usages).

@YairHalberstadt
Copy link
Contributor

I'm closing this as a 'duplicate' of the championed issue #2460. Also relevant is Gafter's proposal at #2823

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

8 participants