Adapters - or post-definition non-contractual "extension" interfaces

Topics: C# Language Design
May 27, 2014 at 10:44 PM
Edited May 27, 2014 at 10:47 PM
I've seen the stirrings of some sort of pattern matching support lately. While it's not pattern matching per se, I have always wanted to scratch my duck typing itch.

Across a program, I use multiple types, working slightly differently to do the same thing because they need to work differently in each place. However, I'd like to work with them as if they were the same thing.

If they're my own inventions, I can use interfaces. However, if I want something I don't control to be part of these related types (be it a NuGet dependency or some framework), I can't. I'd have to create wrapper classes or extension methods.

Here's my idea about "adapters" - interfaces that are interfaces, but which also contain logic to map various other types to the same pattern, logic which is inlined during compilation instead of creating wrapper objects.

First, here's how a "readable collection" could look like.
interface IReadableCollection<in T> : IEnumerable<T> {
    int Count { get; }
    T this[int idx] { get; }

    // So far, this is just a simple interface.
    
    // "adapt" means that something conforming to this type
    // can be treated as a compatible instance.

    // This is a simple declaration. All the interfaces required and the members declared
    // are already available verbatim.
    adapt IList<T>;
    // The above is shorthand for...
    adapt IList<T> {
        int Count {
            // "that" is used instead of "this" to refer to the original IList.
            // Like the "this"-prefixed parameter in extension methods
            // (and all other code), it can only see internally or publicly visible members.
            get { return that.Count; } 
        }
        T this[int idx] {
            get { return that[idx]; }
        }
    }
    
    adapt T[] { // not shorthand, Length != Count.
        int Count {
            get { return that.Length; }
        }
        
        T this[int idx] {
            get { return that[idx]; }
        }
    }
    
    // It turns out that a Tuple with two elements of the same
    // type also would fit. However, it requires a bit more work.
    adapt Tuple<T1, T2> 
        where T1 : T
        where T2 : T {
        
        int Count {
            get { return 2; }
        }
        
        T this[int idx] {
            get {
                if (idx > 1 || idx < 0) throw new ArgumentOutOfRange("idx");
                return (idx == 0 ? that.Item1 : that.Item2);
            }
        }
        
        // GetEnumerator has to be implemented manually
        IEnumerable<T> IEnumerable<T>.GetEnumerator() {
            yield that.Item1;
            yield that.Item2;
        }
        
        IEnumerable IEnumerable.GetEnumerator() {
            yield that.Item1;
            yield that.Item2;
        }
    }
}

// Like partial classes and extension method classes,
// adapters can live outside the interface.
adapt interface IReadableCollection<in T> {
    adapt IReadOnlyList<T>;
}
Here's another example, showing how two different concrete implementations of the same thing could be bridged, and the interface derived from as usual:
interface IColor {
    string HexColor { get; }
    System.Windows.Media.Color WpfColor { get; }
    System.Windows.Media.SolidColorBrush WpfBrush { get; }
    
    adapt System.Windows.Media.Color {
        string HexColor { 
            get {
                return string.Format("#{0:X2}{1:X2}{2:X2}", this.R, this.G, this.B);
            }
        }
        
        System.Windows.Media.Color WpfColor { get { return this; } }
        System.Windows.Media.SolidColorBrush WpfBrush { get { return new SolidColorBrush(this); } }
    }
    
    adapt System.Drawing.Color {
        string HexColor { 
            get {
                return string.Format("#{0:X2}{1:X2}{2:X2}", this.R, this.G, this.B);
            }
        }
        
        System.Windows.Media.Color WpfColor { get { return System.Windows.Media.Color.FromArgb(this.A, this.R, this.G, this.B); } }
        System.Windows.Media.SolidColorBrush WpfBrush { get { return new SolidColorBrush(this.WpfColor); } }
    }
}

class StrungColor : IColor {
    static readonly Regex _matching = new Regex(@"^#?(?<r>[0-9a-fA-F]{2})(?<g>[0-9a-fA-F]{2})(?<b>[0-9a-fA-F]{2})$");
    
    public static IColor FromString(string hex) {
        var match = _matching.Match(hex);
        if (match.IsMatch) {
            return new StrungColor(hex, Convert.ToByte(match.Groups["r"].Value), Convert.ToByte(match.Groups["g"].Value), Convert.ToByte(match.Groups["b"].Value));
        }
        return null;
    }
    
    System.Windows.Media.Color _color;
    string _hex;
    
    StrungColor(string hex, System.Windows.Media.Color color) {
        _hex = hex;
        _color = color;
    }
    
    public string HexColor { get { return _hex; } }
    
    public System.Windows.Media.Color WpfColor { get { return _color; } }
    
    System.Windows.Media.SolidColorBrush _brush;
    public System.Windows.Media.SolidColorBrush WpfBrush {
        get {
            return LazyInitializer.EnsureInitialized(ref _brush, () => new SolidColorBrush(_color));
        }
    }
}
And finally, while there are thousands of other subproblems (like operators) it won't solve, how about being able to begin chipping away at a very old problem:
interface INumeric {
    bool IsInteger { get; }
    INumeric Minus(INumeric n);
    // every other operator...
    
    adapt long {
        bool IsInteger { get { return true; } }
        INumeric Minus(INumeric n) {
            if (n is long) {
                return (long)n - that;
            }
            if (n is int) {
                return (int)n - that;
            }
            if (n is short) {
                return (short)n - that;
            }
            if (n is byte) {
                return (byte)n - that;
            }
            // every other case...
        }
        // ...
    }
    
    // every other adapter...
}
There are probably tons of ways this is untenable that I'm not seeing - has anything like this to bring up-front-specified duck-typing (and not just dynamic which is more or less duck typing by exact usage) been discussed before?