Language-ext: Is a newtype equivalent possible?

Created on 8 Jun 2016  路  4Comments  路  Source: louthy/language-ext

Is it possible to have a haskell newtype equivalent in lang-ext?

IMO the most important feature would be safety, e.g. allowing you to ensure that you never accidentally mix two int values that mean different things.

I think you'd have to consider a few things

  • Equality: newtype must not automatically equal newtype, you specifically want to disallow assignment of a newtype with the same underlying type, unless they are the same newtype
  • Pattern matching: I'm not sure if this is feasible, perhaps just a .Value getter will work well enough?
  • Runtime performance: In haskell the runtime cost of a newtype is removed at compile time, IMO thats nice but not crucial, at least not for me...
  • Single vs multiple type params: Just have one T?

Thanks

examples / documentation

Most helpful comment

@andrevdm I thought about it, and have decided to add it. I've super-charged it somewhat compared to the example above: https://github.com/louthy/language-ext/blob/master/LanguageExt.Core/NewType.cs

Types derived from NewType<T> are:

  • Equatable
  • Comparable
  • Appendable
  • Subtractable
  • Multiplicable
  • Divisable
  • Foldable
  • Functors (with limitations)
  • Monadic (with limitations)

Unit tests are here: https://github.com/louthy/language-ext/blob/master/LanguageExt.Tests/NewTypeTests.cs

In certain places the C# type system isn't strong enough to do compile-time checks. And behaviours on NewType like Map must return NewType<T> rather than the sub-class. So certain operations will throw an exception at runtime if you mix types - this is a probably more 'dynamic' than you'd like, but at least the type checking is there - it's definitely an improvement on working with int:

  • Append and the + operator (works with numeric values and collections), performs runtime checks
  • Subtract and the - operator (works with numeric values and collections), performs runtime checks
  • Divide and the / operator (works with numeric values), performs runtime checks
  • Multiply and the * operator (works with numeric values and collections), performs runtime checks
  • The comparison operators: <, >, <=, >=, perform runtime checks
  • SelectMany - monadic binding, performs runtime checks

Map and Select take a NewType<T> and return a NewType<T>, so you can't change the generic type by mapping - this seems like a sensible limitation. This is also true for SelectMany. However, internally it relies on derived types having one constructor with one argument: this allows NewType<T> to construct the sub-types with reflection and always maintain the correct sub-type even though it's the base-class performing the operations.

So for example:

``` c#
// Declare a new-type called Metres, that only accepts double
class Metres : NewType { public Metres(double value) : base(value) }

// Instantiate a new Metres value
var v = new Metres(1);

// Map must return a NewType<double> because Map is in the base-class
NewType<double> res = v.Map(x => x + 1);

// true -- so even though res is NewType<double>, it is still a Metres type 
// and will maintain the same runtime checks
Debug.Assert( res is Metres );
You can cast the type if you need it back as Metres:

``` c#
    Metres res = v.Map(x => x + 1).As<Metres>();

Or use the built in as operator:

``` c#
Metres res = v.Map(x => x + 1) as Metres;

Because they're monadic you can also LINQ:

``` c#
var m1 = new Metres(100);
var m2 = new Metres(100);

var res = (from x in m1
           from y in m2
           select x + y) as Metres;

NOTE: Because of the runtime checks, and reflection based invocation of constructors, this will be slower than working with manually built types.

All 4 comments

@andrevdm

I've gone through in my head of the options. The only thing I can think of (without reflection or emit gymnastics) would be:

``` c#
public class NewType : IEquatable>
{
public readonly T Value;
public NewType(T value)
{
Value = value;
}

    public bool Equals(NewType<T> other) =>
        !Object.ReferenceEquals(null, other) &&
        this.GetType() == other.GetType() &&
        Value.Equals(other.Value);

    public override bool Equals(object obj) =>
        !Object.ReferenceEquals(null, obj) &&
        Equals((NewType<T>)obj);

    public override int GetHashCode() =>
        Value == null ? 0 : Value.GetHashCode();

    public static bool operator ==(NewType<T> lhs, NewType<T> rhs) =>
        lhs.Equals(rhs);

    public static bool operator !=(NewType<T> lhs, NewType<T> rhs) =>
        !lhs.Equals(rhs);

}

public class Metres : NewType<int>
{
    public Metres(int value) : base(value)
    {
    }
}

public class Hours : NewType<int>
{
    public Hours(int value) : base(value)
    {
    }
}
That has the one constructor/value constraint required of `newtype`, the derived types (`Hours` and `Metres`) aren't the same, even though they both wrap `int`.

``` c#
            var m1 = new Metres(1);
            var m2 = new Metres(1);
            var m3 = new Metres(2);

            var h1 = new Hours(1);
            var h2 = new Hours(1);
            var h3 = new Hours(2);

            var r1 = m1 == m2;    // true
            var r2 = m1 == m3;    // false
            var r3 = m1 == h1;    // This won't compile because Hours and Metres are different

It's definitely not as robust as Haskell, but it gets some of the way I think. Ultimately you will always need to declare an actual type if you want compile-time checking.

In terms of pattern-matching. Well if you have a function that only takes a Metres type, then the work is done. So having .Value access is probably enough. If you have something that takes a NewType<T> then you'd need to us if( x is Metres ) ... and cast. That could definitely be wrapped up in a match or map function.

In terms of multiple type-params, you'd need to create a NewType<A,B>, NewType<A,B,C> etc, and update the Equals operators to do compare all values.

Not 100% sure this is a great fit for lang-ext right now. But I'll give it some thought.

@louthy that is fantastic. That is pretty much all I was thinking of. Sure there is a lot more you could add but I think that covers the 80% and definitely gives the type safety I was after. If you don't add to lang-ext I'll probably steal that code and start from there.

I think single type param is perfect, having more does not really make sense. Also agree that functions would almost always take the exact type, no need for matching then

Thanks for taking the time to look at this

@andrevdm I thought about it, and have decided to add it. I've super-charged it somewhat compared to the example above: https://github.com/louthy/language-ext/blob/master/LanguageExt.Core/NewType.cs

Types derived from NewType<T> are:

  • Equatable
  • Comparable
  • Appendable
  • Subtractable
  • Multiplicable
  • Divisable
  • Foldable
  • Functors (with limitations)
  • Monadic (with limitations)

Unit tests are here: https://github.com/louthy/language-ext/blob/master/LanguageExt.Tests/NewTypeTests.cs

In certain places the C# type system isn't strong enough to do compile-time checks. And behaviours on NewType like Map must return NewType<T> rather than the sub-class. So certain operations will throw an exception at runtime if you mix types - this is a probably more 'dynamic' than you'd like, but at least the type checking is there - it's definitely an improvement on working with int:

  • Append and the + operator (works with numeric values and collections), performs runtime checks
  • Subtract and the - operator (works with numeric values and collections), performs runtime checks
  • Divide and the / operator (works with numeric values), performs runtime checks
  • Multiply and the * operator (works with numeric values and collections), performs runtime checks
  • The comparison operators: <, >, <=, >=, perform runtime checks
  • SelectMany - monadic binding, performs runtime checks

Map and Select take a NewType<T> and return a NewType<T>, so you can't change the generic type by mapping - this seems like a sensible limitation. This is also true for SelectMany. However, internally it relies on derived types having one constructor with one argument: this allows NewType<T> to construct the sub-types with reflection and always maintain the correct sub-type even though it's the base-class performing the operations.

So for example:

``` c#
// Declare a new-type called Metres, that only accepts double
class Metres : NewType { public Metres(double value) : base(value) }

// Instantiate a new Metres value
var v = new Metres(1);

// Map must return a NewType<double> because Map is in the base-class
NewType<double> res = v.Map(x => x + 1);

// true -- so even though res is NewType<double>, it is still a Metres type 
// and will maintain the same runtime checks
Debug.Assert( res is Metres );
You can cast the type if you need it back as Metres:

``` c#
    Metres res = v.Map(x => x + 1).As<Metres>();

Or use the built in as operator:

``` c#
Metres res = v.Map(x => x + 1) as Metres;

Because they're monadic you can also LINQ:

``` c#
var m1 = new Metres(100);
var m2 = new Metres(100);

var res = (from x in m1
           from y in m2
           select x + y) as Metres;

NOTE: Because of the runtime checks, and reflection based invocation of constructors, this will be slower than working with manually built types.

very, very nice! Thanks so much. I'm adding it to my project right now :)

Was this page helpful?
0 / 5 - 0 ratings