I have this situation (drastically simplified):
interface IPoint<TPoint>
where TPoint:IPoint<TPoint>
{
//example method
TPoint Translate(TPoint offset);
}
interface IGrid<TPoint, TDualPoint>
where TPoint:IPoint<T
where TDualPoint:Ipoint
{
TDualPoint GetDualPoint(TPoint point, /* Parameter specifying direction */);
}
Here is typical implementation:
class HexPoint : IPoint<HexPoint> { ... }
class TriPoint : IPoint<TriPoint> { ... }
class HexGrid : IGrid<HexPoint, TriPoint> { ... }
class TriGrid : IGrid<TriPoint, HexPoint> { ... }
So on a HexGrid
, the client can make a call to get a point on a dual grid, with exactly the right type:
TriPoint dual = hexGrid.GetDualPoint(hexPoint, North);
So far so good; the client does not need to know anything about the type how the two points relate, all she needs to know is that on a HexGrid
the method GetDualPoint
returns a TriPoint
.
Except...
I have a class full of generic algorithms, that operate on IGrid
s, for example:
static List<TPoint> CalcShortestPath<TPoint, TDualPoint>(
IGrid<TPoint, TDualPoint> grid,
TPoint start,
TPoint goal)
{...}
Now, the client suddenly has to know the little detail that the dual point of a HexPoint
is a TriPoint
, and we need to specify it as part of the type parameter list, even though it does not strictly matter for this algorithm:
static List<TPoint> CalcShortestPath<TPoint, *>(
IGrid<TPoint, *> grid,
TPoint start,
TPoint goal)
{...}
Ideally, I would like to make DualPoint a "property" of the type IPoint
, so that HexPoint.DualPoint
is the type TriPoint
.
Something that allows IGrid to look like this:
interface IGrid<TPoint>
where TPoint:IPoint<TPoint>
//and TPoint has "property" DualPoint where DualPoint implements IPoint...
{
IGrid<TPoint.DualPoint> GetDualGrid();
}
and the function CalcShortestPath
like this
static List<TPoint> CalcShortestPath<TPoint>(
IGrid<TPoint> grid,
TPoint start,
TPoint goal)
{...}
Of course, this is not possible, as far as I know.
But is there a way I can change my design to mimic this somehow? So that
To give an indication of why this becomes a real problem: In my library IGrid
actually has 4 type parameters, IPoint
has 3, and both will potentially increase (up to 6 and 5). (Similar relations hold between most of these type parameters.)
Explicit overloads instead of generics for the algorithms are not practical: there are 9 concrete implementations of each of IGrid
and IPoint
. Some algorithms operate on two types of grids, and hence have a tonne of type parameters. (The declaration of many functions are longer than the function body!)
The mental burden was driven home when my IDE threw away all the type parameters during an automatic rename, and I had to put all the parameters back manually. It was not a mindless task; my brain was fried.
As requested by @Iridium, an example showing when type inference fails. Obviously, the code below does not do anything; it's just to illustrate compiler behaviour.
using System;
using System.Collections.Generic;
using System.Linq;
public interface IPoint<TPoint, TDualPoint>
where TPoint:IPoint<TPoint, TDualPoint>
where TDualPoint : IPoint<TDualPoint, TPoint>{}
interface IGrid<TPoint, TDualPoint>
where TPoint:IPoint<TPoint, TDualPoint>
where TDualPoint:IPoint<TDualPoint, TPoint>{}
class HexPoint : IPoint<HexPoint, TriPoint>
{
public HexPoint Rotate240(){ return new HexPoint();} //Normally you would rotate the point
}
class TriPoint : IPoint<TriPoint, HexPoint>{}
class HexGrid : IGrid<HexPoint, TriPoint>{}
static class Algorithms
{
public static IEnumerable<TPoint> TransformShape<TPoint, TDualPoint>(
IEnumerable<TPoint> shape,
Func<TPoint, TPoint> transform)
where TPoint : IPoint<TPoint, TDualPoint>
where TDualPoint : IPoint<TDualPoint, TPoint>
{
return
from TPoint point in shape
select transform(point);
}
public static IEnumerable<TPoint> TransformShape<TPoint, TDualPoint>(
IGrid<TPoint, TDualPoint> grid,
IEnumerable<TPoint> shape,
Func<TPoint, TPoint> transform)
where TPoint : IPoint<TPoint, TDualPoint>
where TDualPoint : IPoint<TDualPoint, TPoint>
{
return
from TPoint point in shape
//where transform(point) is in grid
select transform(point);
}
}
class UserCode
{
public static void UserMethod()
{
HexGrid hexGrid = new HexGrid();
List<HexPoint> hexPointShape = new List<HexPoint>(); //Add some items
//Compiles
var rotatedShape1 = Algorithms.TransformShape(
hexGrid,
hexPointShape,
point => point.Rotate240()).ToList();
//Compiles
var rotatedShape2 = Algorithms.TransformShape<HexPoint, TriPoint>(
hexPointShape,
point => point.Rotate240()).ToList();
//Does not compile
var rotatedShape3 = Algorithms.TransformShape(
hexPointShape,
point => point.Rotate240()).ToList();
}
}
So, gonna throw up an answer based on the throwaway idea I talked about in the comments...
The basic gist was "Define a type that conveys this concept of point duality, and use that in your relevant signatures so as to give the compiler the hints it needs"
One thing you should read whenever you hit the dreaded "Type cannot be inferred from usage" error: http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx
In that, Messr. Lippert spells out the harsh truth that only the parameters of the signature are checked during this inference stage, NOT the constraints. So we have to be a tiny bit more "specific" here.
First, let's define our "duality relationship" - I should note this is one way to set up these relationships, there are (in theory) an infinite variety of them.
public interface IDual<TPoint, TDualPoint>
where TPoint: IPoint<TPoint>, IDual<TPoint, TDualPoint>
where TDualPoint: IPoint<TDualPoint>, IDual<TDualPoint, TPoint>
{}
Now we go back and retrofit our existing signatures:
public interface IPoint<TPoint>
where TPoint:IPoint<TPoint>
{}
class TriPoint : IPoint<TriPoint>, IDual<TriPoint,HexPoint>
{}
class HexPoint : IPoint<HexPoint>, IDual<HexPoint,TriPoint>
{
// Normally you would rotate the point
public HexPoint Rotate240(){ return new HexPoint();}
}
And likewise on the "secondary types", the grids:
interface IGrid<TPoint, TDualPoint>
where TPoint: IPoint<TPoint>, IDual<TPoint, TDualPoint>
where TDualPoint : IPoint<TDualPoint>, IDual<TDualPoint, TPoint>
{
TDualPoint GetDualPoint(TPoint point);
}
class HexGrid : IGrid<HexPoint, TriPoint>
{
public TriPoint GetDualPoint(HexPoint point)
{
return new TriPoint();
}
}
class TriGrid : IGrid<TriPoint, HexPoint>
{
public HexPoint GetDualPoint(TriPoint point)
{
return new HexPoint();
}
}
And finally on our utility methods:
static class Algorithms
{
public static IEnumerable<TPoint> TransformShape<TPoint, TDualPoint>(
IEnumerable<IDual<TPoint, TDualPoint>> shape,
Func<TPoint, TPoint> transform)
where TPoint : IPoint<TPoint>, IDual<TPoint, TDualPoint>
where TDualPoint : IPoint<TDualPoint>, IDual<TDualPoint, TPoint>
{
return
from TPoint point in shape
select transform(point);
}
public static IEnumerable<TPoint> TransformShape<TPoint, TDualPoint>(
IGrid<TPoint, TDualPoint> grid,
IEnumerable<IDual<TPoint, TDualPoint>> shape,
Func<TPoint, TPoint> transform)
where TPoint : IPoint<TPoint>, IDual<TPoint, TDualPoint>
where TDualPoint : IPoint<TDualPoint>, IDual<TDualPoint, TPoint>
{
return
from TPoint point in shape
//where transform(point) is in grid
select transform(point);
}
}
Note the signature on the method - we are saying "Hey, this list of things we're giving you, it absolutely has dual points", which is what's going to allow code like so:
HexGrid hexGrid = new HexGrid();
List<HexPoint> hexPointShape = new List<HexPoint>(); //Add some items
//Compiles
var rotatedShape1 = Algorithms
.TransformShape(
hexGrid,
hexPointShape,
point => point.Rotate240())
.ToList();
//Compiles
var rotatedShape2 = Algorithms
.TransformShape<HexPoint, TriPoint>(
hexPointShape,
point => point.Rotate240())
.ToList();
//Did not compile, but does now!
var rotatedShape3 = Algorithms
.TransformShape(
hexPointShape,
point => point.Rotate240())
.ToList();