4

I write simple parser and want to implement next two interfaces:

public interface IResult<TValue, TToken> 
    where TToken : ITokenizer<IResult<TValue, TToken>, TValue>
{
    TToken Tokenizer { get; }
    TValue Value { get; }
}

public interface ITokenizer<TResult, TValue> 
    where TResult : IResult<TValue, ITokenizer<TResult, TValue>>
{
    TResult Advance();
}

It has next purpose: ITokenizer is an immutable class for splitting the string by tokens. We can call Advance method and get Result: next token and next tokenizer. So, I want store token and tokenizer in Result class and want add compile-time constraint for this.

Now I have a compile-time error during construct this two interfaces.

I thought that next classes can implement interfaces with all constraints:

public class Result : IResult<string, Tokenizer>
{ /* implement interface */}

public class Tokenizer : ITokenizer<Result, string>
{ /* implement interface */}

Can anyone explain what's wrong? Maybe why it's impossible or how make this code correct?

P.S. For my task I can simply use IResult<TValue, TToken> interface without any constraints, but can I implement this without losing constraints?

Compiler errors:

(3:22) The type 'Test.IResult<TValue,TToken>' cannot be used as type parameter 'TResult' in the generic type or method 'Test.ITokenizer<TResult,TValue>'. 
There is no implicit reference conversion from 'Test.IResult<TValue,TToken>' to 
'Test.IResult<TValue,Test.ITokenizer<Test.IResult<TValue,TToken>,TValue>>'.
(10:22) The type 'Test.ITokenizer<TResult,TValue>' cannot be used as type parameter 'TToken' in the generic type or method 'Test.IResult<TValue,TToken>'. 
There is no implicit reference conversion from 'Test.ITokenizer<TResult,TValue>' to 
'Test.ITokenizer<Test.IResult<TValue,Test.ITokenizer<TResult,TValue>>,TValue>'.
Nikita Sivukhin
  • 2,370
  • 3
  • 16
  • 33
  • 2 things: Please add the compile error to your post so we know what it is and secondly perhaps tell us what you are trying to do so we know why you chose this solution. There may be a better solution and you will get some more ideas. – CodingYoshi Nov 19 '16 at 15:36
  • @CodingYoshi I don't want to go deep into the situation because I want to understand why this code doesn't compile. I think there is an underlying basic reason for it, that I don't understand now. – Nikita Sivukhin Nov 19 '16 at 15:57
  • But you have circular reference there: IResult type constraints depend on ITokenizer and vica versa. – Evk Nov 19 '16 at 16:13
  • The error is clear and it is saying it cannot convert IResult to IResult, TValue>>. I am not sure what else you want to know? – CodingYoshi Nov 19 '16 at 16:13
  • @Evk but why simple circular reference like `public interface Circular where T : Circular` is allowed and works? – Nikita Sivukhin Nov 19 '16 at 16:14
  • @CodingYoshi but why it can't convert? I can simply deduce, that `IResult` is `IResult, TValue>` from constraints – Nikita Sivukhin Nov 19 '16 at 16:19
  • @NikitaSivukhin may be you should use [covariance](https://msdn.microsoft.com/en-us/library/dd799517) here? – Pavel Voronin Nov 19 '16 at 16:29
  • @PavelVoronin yes, I think. But if I simply mark type parameters with `out` keyword there still compile errors... Or you suggest something different? – Nikita Sivukhin Nov 19 '16 at 16:31
  • @NikitaSivukhin nope, nothing more. – Pavel Voronin Nov 19 '16 at 16:37

2 Answers2

5

You can try to add one more type constraint to both interfaces, like this:

public interface IResult<TValue, TToken, TResult>
    where TToken : ITokenizer<TResult, TValue, TToken>
    where TResult : IResult<TValue, TToken, TResult> {
    TToken Tokenizer { get; }
    TValue Value { get; }
}

public interface ITokenizer<TResult, TValue, TTokenizer>
    where TResult : IResult<TValue, TTokenizer, TResult>
    where TTokenizer : ITokenizer<TResult, TValue, TTokenizer> {
    TResult Advance();
}

It is a bit more ugly, but I think will work for your goal:

public class Result : IResult<string, Tokenizer, Result>
{

}

public class Tokenizer : ITokenizer<Result, string, Tokenizer> {

}

I think the main problem is not circular references, but just the fact compiler cannot deduce implicit conversion between your generic types until you help it a bit.

UPDATE: I think your interfaces lack strong relation between Tokenizer and Result. IResult interface says that TToken can be any tokenizer, I mean related to any Result. So it can be ITokenizer<Result1>, ITokenizer<Result2> and so on. But you cannot assign ITokenizer<Result1> to ITokenizer<Result2> (even though results implement the same interface) - that's different types. Same is true for tokenizer interface. When you change interface like above, it is now clear that TToken is tokenizer of TResult, and at the same time TResult is result of TTokenizer (now those are two concrete types, not interfaces, with strong relation between them).

Evk
  • 98,527
  • 8
  • 141
  • 191
  • @NikitaSivukhin see also my thoughts about the reasons (I think that it's not compiler cannot deduce, but those definitions are really wrong and should not compile). – Evk Nov 19 '16 at 17:46
0

Update Please disregard this answer since Evk's answer disproved this answer. However, I am still leaving this answer here because if someone else thinks it has to do with circular reference, it will help to explain that it clearly does not.

The issue is when the compiler is trying to compile the first interface, it needs to compile the second one but to compile the second one, it needs to compile the first one. Therefore, it cannot do so since it cannot arrive at a conclusion. To make things simpler, this code will get the same error as yours:

public interface IFirst<TFirst>
    where TFirst : ISecond<IFirst<TFirst>>
{

}

public interface ISecond<TSecond>
    where TSecond : IFirst<ISecond<TSecond>>
{ }

But the code below will not get errors since there is no circular reference and the compiler can arrive at a conclusion:

public interface IFirst<TFirst>
    where TFirst : ISecond<IFirst<TFirst>>
{

}

public interface ISecond<TSecond>
    //where TSecond : IFirst<ISecond<TSecond>>
{ }
CodingYoshi
  • 25,467
  • 4
  • 62
  • 64