Skip to content

Thinking Functionally: Function Signatures

taserian edited this page Jan 9, 2020 · 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:

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

Clearly that's not as declarative as:

    DateTime 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:

    DateTime 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.

Constrained types

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) { }
    }

It's a little awkward to use the declarative style, so I would understand if you prefer the constructor approach. To see what predicates are available for validation check the: LanguageExt.ClassInstances.Pred namespace and the constants are in LanguageExt.ClassInstances.Const. You can build your own predicates by deriving from Pred<A> and your own constants by deriving from Const<A>.

The benefits however are:

  • You can create re-usable validators
  • When you find a type in your source code and select 'Go To Definition' you can see the predicate that's being used. No need to look at the source

The end result is a function that looks like this:

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

And that communicates so much to the consumer of that function it's hard to go wrong, but even if you do, the type system picks it up.

You may have noticed that you could have a date which is 31st Feb. This doesn't deal with composite validation, so you still need to do that.

There are other areas these predicates in the generics can be used. For example:

    int DoSomethingWithAList(Lst<int> list)
    {
        if(list.IsEmpty) throw new ArgumentException("List must not be empty");
        ...
    }

The range on a list is another example where we can't tell from the signature what's going on.

Try this:

    int DoSomethingWithAList(Lst<NonEmpty, int> list)
    {
        // List can't ever be empty here
    }

The predicate enforces the rule and it's embedded in the type. So wherever you work with a list the NonEmpty attribute is following it around. The type system is communicating with you.

You could also do:

    int DoSomethingWithAList(Lst<MaxCount<I100>, int> list)
    {
        // List must have 100 or fewer items
    }

    int DoSomethingWithAList(Lst<CountRange<I1, I100>, int> list)
    {
        // List must have 1 - 100 items.
    }

By the time you get to a function that takes a list or something like that, you could be many methods and stack frames away from the source of the error. By using declarative types then you catch the issues and declare your intent when you create the list.

Signatures that talk

Let's look again at our CreateDate example. What happens when I pass '31st Feb' as a date? We must assume that it throws an exception.

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

What about in this situation?

    string CreateDateString(Year year, Month month, Day day);

This is a function that appears to create a formatted date string based on the parameters. If we give an invalid date, do we:

  • Get a null return value?
  • Get an exception thrown?

The only way to know is from documentation or source code. We don't want to have to read the source code for every function we work with, so we need the signature to talk to us.

In general in functional programming it's better to avoid throwing exceptions (because exceptions break the whole idea of mathematical expressions and send us back to goto land), and instead pass back a result that indicates failure. In language-ext there are a number of types that do this:

  • Option<A>
  • OptionUnsafe<A>
  • OptionAsync<A>
  • Either<L, R>
  • EitherUnsafe<L, R>
  • Try<A>
  • TryOption<A>
  • TryAsync<A>
  • TryOptionAsync<A>

So how do these work? Let's start with the CreateDateString example:

    Try<string> CreateDateString(Year year, Month month, Day day) => () =>
        new DateTime(year.Value, month.Value, day.Value).ToShortDateString();

What we're doing here is no validation ourselves, but relying on the validation of DateTime. DateTime however throws exceptions, which we don't want. So instead we return a Try<string>. Try<A> is a delegate type, and so we wrap this: new DateTime(year.Value, month.Value, day.Value).ToShortDateString() in a lambda (notice the => () => at the end of the first line.

Next we make use of the extension methods for Try<string> to work with the value:

    Try<string> date = CreateDateString(Year.New(2000), Month.New(2), Day.New(30));

    string result = date.IfFail("Invalid date");

This is great, we can now handle the failure gracefully, and at the point where we know more about it. We can also pattern match on the result. The result has two possible states Succ for success, and Fail for failure:

    Try<string> date = CreateDateString(Year.New(2000), Month.New(2), Day.New(30));

    string result = date.Match(
        Succ: dt => $"The date is {dt}",
        Fail: ex => $"Invalid date ({ex.Message})"
        );

This is using named arguments to specify the handlers for the patterns. We can now post-process the value when it succeeds and craft a useful default when it fails.

The most important thing about these types is that you can't physically get at the string value until you've confirmed that it exists. So there's no null reference exceptions and no doubt in the programmers mind whether there are bugs in the code they're writing.

What about working with multiple functions that could fail? Let's change our CreateDateString example to the original CreateDate example. In fact, why not get creative with this... let's make it an extension method for the tuple (Year, Month, Day).

    public static class TupleExt
    {
        public static Try<DateTime> ToDate(this (Year year, Month month, Day day) date) => () =>
           new DateTime(date.year.Value, date.month.Value, date.day.Value);
    }

So now we have an extension method that will turn a tuple into a Try<DateTime>:

Let's try working with two Try<DateTime> by getting the TimeSpan between them:

    Try<DateTime> fromDate = (Year.New(2000), Month.New(1), Day.New(1)).ToDate();
    Try<DateTime> toDate = (Year.New(2017), Month.New(5), Day.New(20)).ToDate();

    TimeSpan result = fromDate.Match(
        Succ: begin =>
            toDate.Match(
                Succ: end => end - begin,
                Fail: ex  => TimeSpan.Zero),
        Fail: ex => TimeSpan.Zero);

Euurggh! Ugly right!? Nobody want to do this. And what if we had more than two items? It would very quickly become a mess of nested lambdas.

It just so happens that the behaviour from the example above is also an example of monadic binding. Which means we can use LINQ to achieve the same ends (but in a much more attractive manner):

    var result = from begin in fromDate
                 from end in toDate
                 select end - begin;

So what is the result here? It's a Try<TimeSpan>. We're still in our Try context. So if we want to invoke the expression to get the value we can still do:

    TimeSpan span = result.IfFail(TimeSpan.Zero);

In general it's better to stay in context when working with monadic types. Using Match, IfNone, IfFail, etc. should be a last resort when you need to get a concrete value out. Even then it can be better to call Iter(x => ...) where x is the bound value and ... is the code that works on it. Or Map(x => ...) which is the same as Iter but expects a return value (which will be re-bound in the monadic type).

Try<A> is to handle exceptional behaviour (code that might throw exceptions), but not all error values are exceptional. Some are known:

    int ParseInt(string value) =>
        Int32.TryParse(value, out var result)
            ? result
            : 0;

That code will never throw an exception, but returning 0 as a default isn't a great solution. So let's move on to Option<A>.

    using static LanguageExt.Prelude;

    Option<int> ParseInt(string value) =>
        Int32.TryParse(value, out var result)
            ? Some(result)
            : None;

Anyone looking at this signature can't fail to see that the result is optional:

    Option<int> ParseInt(string value)

It also indicates that the function is not likely to throw exceptions (if it does, then it will be truly exceptional!).

Usage follows a similar path to Try<A>:

    Option<int> result = from a in ParseInt("10")
                         from b in ParseInt("20")
                         from c in ParseInt("30")
                         select a + b + c;

    // Some(60)

TBC

NEXT: What is LINQ really?