Skip to content

Thinking Functionally: Function Signatures

Paul Louth edited this page May 19, 2017 · 13 revisions

Declarative functions signatures are just good practice whether you're writing imperatively or functionally. On any suitably large project - with multiple developers - it's impossible to know everything that goes on within a function (and you shouldn't have to know). Documentation can only take you so far, and tends to rot over time. So we need the signature of the function to tell us what's what.

For example:

    Date CreateDate(int a, int b, int c);

Clearly that's not as declarative as:

    Date CreateDate(int year, int month, int day);

We're all used to using names to communicate with other devs about our intentions. Names are a pretty crude tool for constraining data. The compiler won't do anything if you provide a month with the number 13. Ideally we need this:

    Date CreateDate(Year year, Month month, Day day);

But creating a new-type for every usage of an int (or any unconstrained base-type) would be extremely tedious.

Language-ext provides three classes to help: NewType, NumType, FloatType. NewType is the most general case, NumType would be used for integer number types (int, short, long, BigInteger, etc.), and FloatType is used for floating point number types (float, double, decimal, etc.).

So let's create Year, Month, Day:

    public class Year : NumType<Year, TInt, int>
    {
        Year(int x) : base(x) { }
    }

    public class Month : NumType<Month, TInt, int>
    {
        Month(int x) : base(x) { }
    }

    public class Day : NumType<Day, TInt, int>
    {
        Day(int x) : base(x) { }
    }

We can now create typed values:

    var day = Day.New(1);
    var month = Month.New(1);
    var year = Year.New(2000);

But you can't assign a Day to a Month, or a Month to a Year, because they're not the same types.

What about constraints. We can just use constructors:

    public class Year : NumType<Year, TInt, int>
    {
        Year(int x) : base(x) 
        { 
           if(x < 1970 || x > 2050) throw new ArgumentException("Invalid year");
        }
    }

    public class Month : NumType<Month, TInt, int>
    {
        Month(int x) : base(x)
        { 
           if(x < 1 || x > 12) throw new ArgumentException("Invalid month");
        }
    }

    public class Day : NumType<Day, TInt, int>
    {
        Day(int x) : base(x)
        { 
           if(x < 1 || x > 31) throw new ArgumentException("Invalid day");
        }
    }

Or we can do it declaratively:

    using LanguageExt.ClassInstances;
    using LanguageExt.ClassInstances.Const;
    using LanguageExt.ClassInstances.Pred;

    public class Year : NumType<Year, TInt, int, Range<TInt, int, I1970, I2050>>
    {
        Year(int x) : base(x) { }
    }

    public class Month : NumType<Month, TInt, int, Range<TInt, int, I1, I12>>
    {
        Month(int x) : base(x) { }
    }

    public class Day : NumType<Day, TInt, int, Range<TInt, int, I1, I31>>
    {
        Day(int x) : base(x) { }
    }