Functions as Data: Functional Programming in C#

In Object Oriented Programming (OOP), we’re used to using collections of objects or simple data types. We often sort and filter these collections using LINQ as part of business logic behaviors or for data transformation. While these are useful tasks we frequently perform, it can be easy to forget that functions in C# can be treated as data. If we realign our thinking around functions as data, it enables us to discover alternative solutions to standard problems in OOP.

In this article, we’ll look at an example from my C# Functional Programming workshop. The scenario outlines a solution used to score a poker hand. We’ll examine an alternative pattern to a solution that utilizes functions as data. Through this new pattern, we’ll provide flexibility to the scoring mechanic of the game.

Scoring Criteria

First, let’s take a look at the individual scoring functions that are used to produce the final score. Each function is a rule that determines if the hand of cards meets a criteria.

private bool HasFlush(IEnumerable<Card> cards) => ...;
private bool HasRoyalFlush(IEnumerable<Card> cards) => ...;
private bool HasPair(IEnumerable<Card> cards) => ...;
private bool HasThreeOfAKind(IEnumerable<Card> cards) => ...;
private bool HasFourOfAKind(IEnumerable<Card> cards) => ...;
private bool HasFullHouse(IEnumerable<Card> cards) => ...;
private bool HasStraightFlush(IEnumerable<Card> cards) => ...;
private bool HasStraight(IEnumerable<Card> cards) => ...;

The diagram below illustrates the rules of which the game is scored by. While the functions tell if the hand meets the criteria, they don’t directly impact the final score of the hand. We need to arrange the rules and evaluate them in order of importance to produce a score and assign it to an enumerator of HandRank.

 

Determining the Score

Using the rules, we can determine the final score value in a few different ways. Each of the following examples is technically correct and offers its own level of readability and simplicity. The negative aspect to each approach is that the order in which the rules execute is “hard coded”.

  1. Maintain a state
    This way of evaluating the score uses a temporary placeholder value to keep track of the score. As each evaluation takes place, the score is updated with the best HandRank available. This method is very explicit, but involves extra code and variables that arenโ€™t necessary to complete the task.
  2. public HandRank GetScore(Hand hand)
    {
        var score = HandRank.HighCard;
        if (HasPair(hand.Cards)) { score = HandRank.Pair; }
         ... 
        if (HasRoyalFlush(hand.Cards)) { score = HandRank.RoyalFlush; }
        return score;
    }
    
  3. Return early
    Using a return early pattern allows us to write intuitive code that returns the best HandRank by returning immediately from the function when an evaluation is found to be true. This method is easy to read and fairly easy to modify as new rules are required by the application.
  4. public HandRank GetScore(Hand hand)
    {
        if (HasRoyalFlush()) return HandRank.RoyalFlush;
         ...
        if (HasPair()) return HandRank.Pair;
        return HandRank.HighCard;
    }
    
  5. Ternary Expression
    The function can be written as a single expression using a ternary operator. This has a similar effect as the return early method, but with even less code. Readability for this method may be easier for some than others.
public HandRank GetScore(Hand hand) => 
    HasRoyalFlush(hand.Cards) ? HandRank.RoyalFlush :
     ...
    HasPair(hand.Cards) ? HandRank.Pair :
    HandRank.HighCard;

In all of the previous examples order of operation is crucial. If we decide to add new rules to this scoring function, then we’ll need to insure they are inserted in the correct order to determine the proper score.

Thinking Functional

The GetScore operation is stepping through criteria evaluations and matching the first rule that results to true and returning the matching HandRank. Instead of evaluating the functions as individual statements we, can approach the problem from a functional programming mindset. Let’s change the way we look at the problem by thinking of the functions as data.
If we look at the individual scoring functions as data, we can identify a pattern. Consider the signature for the following scoring functions.

private bool HasFlush(IEnumerable<Card> cards) => ...;
private bool HasRoyalFlush(IEnumerable<Card> cards) => ...;
private bool HasPair(IEnumerable<Card> cards) => ...;
private bool HasThreeOfAKind(IEnumerable<Card> cards) => ...;
private bool HasFourOfAKind(IEnumerable<Card> cards) => ...;
private bool HasFullHouse(IEnumerable<Card> cards) => ...;
private bool HasStraightFlush(IEnumerable<Card> cards) => ...;
private bool HasStraight(IEnumerable<Card> cards) => ...;

Each function is of the same type, Func<IEnumerable<Card>, bool>. Since we have many pieces of data of the same type, we can arrange them in a collection or array. Next, we’ll need to match each function with the HandRank it represents. For example: HasPair will result in a score of HandRank.Pair. Using Tuples we can easily create this mapping without the need for a specialized class. In C# 7.1, we can create a tuple by simply enclosing multiple values in parenthesis. Using the function and its mapped enumerator, we can build the collection.

private List<(Func<IEnumerable<Card>, bool> eval, HandRank rank)> GameRules() =>
   new List<(Func<IEnumerable<Card>, bool> eval, HandRank rank)>
   {
               (cards => HasRoyalFlush(cards), HandRank.RoyalFlush),
               (cards => HasStraightFlush(cards), HandRank.StraightFlush),
               (cards => HasFourOfAKind(cards), HandRank.FourOfAKind),
               (cards => HasFullHouse(cards), HandRank.FullHouse),
               (cards => HasFlush(cards), HandRank.Flush),
               (cards => HasStraight(cards), HandRank.Straight),
               (cards => HasThreeOfAKind(cards), HandRank.ThreeOfAKind),
               (cards => HasPair(cards), HandRank.Pair),
               (cards => true, HandRank.HighCard),
   };

To keep things tidy, we’ll wrap the construction of the collection in a single function called GameRules. We can later use this as an extensible point for additional game rules. By moving the ranking system outside of the GetScore method it can be modified or replaced with new evaluations and ranks. For the lowest rank possible, we’ll simply use true to represent the default evaluation.

Refactoring with LINQ

Now we’ll rewrite the GetScore method using LINQ to evaluate the list. By treating the items in the list as data, we can utilize sorting to ensure they are executed in the proper order. We no longer have to worry about the “hard coded” execution order. We can use .OrderByDescending(card => card.rank) to sort the evaluations from strongest rank to weakest since HandRank.RoyalFlush is of the highest value.

public HandRank GetScore(Hand hand) => GameRules()
                    .OrderByDescending(rule => rule.rank)
                    .First(rule => rule.eval(hand.Cards)).rank;

Finally, to get the result we’ll perform our evaluation. The most efficient way to do this is by using the First LINQ method. Since First is a short-circuit operator, it will stop evaluating the items as soon as it finds the first item which returns true. When the first item evaluates to true we’ll take the rank value of the tuple from the data set and return it. The rank value is our final hand score.

Conclusion

Functions in C# are often thought of as static statements that our application can use to change the state of data within the system. By turning our perspective from imperative to functional, we can find alternative solutions. One way of bringing a functional mindset to the problem is by remembering that functions are also data and conform to many of the same rules as other data types in C# do. In this example, we saw how a functional approach changed a hard-coded statement-based evaluation to a flexible sort & map-based evaluation. This simple change expands the functionality of the application and reduces fritcion when adding new criteria as no order of operation is predefined.

To add more functional thinking to your mental toolbox, download the free Functional Programming cheat sheet and watch the video Functional Programming in C# on Channel 9.

Comments

  • BillWoo

    What bothers me about this example is the apparent lack of optimization of determining the HandRank: calculating the number of cards of the same suit in the hand, and/or the number of cards with the same face value, gives you a quick way to reduce/partition the search space.

    While encompassing standard poker rules may not be your goal here, an example that doesn’t take into account the comparison of hands with the same rank, such as pair, seems incomplete: a pair of jacks beats a pair of eights, etc.

    thanks, Bill

    • Wil Paulk

      This example does not appear to be incomplete to me. My interpretation of the author’s intent was to show how one could use functions as data and to show a coding pattern that uses that concept. Examples need to be succinct and focused. It would not improve the example to complicate it with more complex rules. The complex rules are not the point. The pattern is.

      • BillWoo

        I have added some content to the first post. You will probably continue to see what you want to see ๐Ÿ™‚

      • Ed Charbeneau

        Well said Wil. It’s difficult as an author to communicate a concept without introducing too much complexity or abstractions. Thanks!

    • Ed Charbeneau

      Bill, I appreciate your passion for the craft…

      1) This is probably a bit to literal for the demo code. The beauty of a poker Kata is that many of the challenges simulate things I’ve found in writing business logic: filter, map, reduce, repeat. This example isn’t by any means an opportunity to demonstrate evaluation based on poker odds.

      2) What’s old is new again. Can you believe that people are putting HTML, JavaScript and CSS in the same files again. I sure can’t. ๐Ÿ™‚ I work daily with the dev community at large. There’s a definite skill level curve you have remember exits. New devs join this career journey every day, many have not even grasped the basics of generics, func, expressions, etc… Not all of my readers have the same number of years dev experience.

      3) There’s a lot more ground to cover, but going too far down the FP rabbit hole can be counter productive. The scope of the article was to remind devs that we can get blinders on writing the same old CRUD day after day.

      I couldn’t agree more about the optimization comment. Yes, there’s not a single bit of optimization here, if you think this is bad, you should see the source code. With that said, it’s just a demo, and the point is to open a discussion around solving problems. Optimization is a necessary task that is a whole different discussion all together.

      Thanks for the detailed response. I’ll take the discussion on absurdity over an unread article any day.

  • Wil Paulk

    To make the GetScore function more reusable, could we pass in the GameRules? I guess the List<(Func<IEnumerable, bool> eval, HandRank rank)> signature might be a bit messy looking. This is where automatic typing that languages like F# have come in handy.

    public HandRank GetScore(List<(Func<IEnumerable, bool> eval, HandRank rank)> rules, Hand hand) =>
    rules()
    .OrderByDescending(rule => rule.rank)
    .First(rule => rule.eval(hand.Cards)).rank;

    Thanks for the article.

    • Ed Charbeneau

      Great feedback. Yes you could externalize the GameRules. As others pointed out, you could use interfaces and dependency injection to expand upon the examples given in the article.

  • Jay French

    Your numbered points under “Determining the Score” are all “1”. ๐Ÿ™‚

    • Good catch ๐Ÿ™‚ Thanks, Jay, for reading in detail! Fixed it.

  • kburgoyne

    Good write-up, but some optimizations and safer code. Granted the author wanted to explain things, not necessarily clutter the code with extra details.

    #1. GameRules should be static. There is no need to be creating multiple instances of it. This might not be true for some games, but for poker and most games it will be.

    #2. If the game does not dynamically alter the contents of GameRules — poker wouldn’t, but it is easily envisioned some games might — then GameRules should be readonly and IReadOnlyList. Most likely IEnumerable since the list seems to always be accessed in sequential order.

    #3. GameRules should be pre-sorted once by either a static constructor or possibly using a LINQ expression in the variable declaration initialization.

    #4. If GameRules can be dynamically changed, then either a binary search insert/delete or a re-sort should be perform at the time of change. Assuming inserts/deletes are infrequent (possibly only performed at game start based on game config settings), and the rules are a pretty short list, just doing a re-sort is probably more than sufficient rather than bothering with the added complexity of binary insert/delete. Besides sorts usually just do a lot of swapping of array (list) entries without doing inserts/deletes. Insert/deletes can involve a bit of overhead as array elements following the insert or delete position get moved.

    • Ed Charbeneau

      Great stuff. I’m glad to see you taking it to the next level. The scope of the article was to show some basic building blocks for those who might not be utilizing Func to it’s full potential. Thanks for taking the time to share your detailed thoughts ๐Ÿ™‚

  • marayfirth

    I got the point of functionnal programming here but you don’t make the real comparison with OOP. If I had to do it, I would have done like below and the results are almost the same. The pure functionnal definitions are less readable to me, but pure OOP is a bit less flexible.

    public interface IHandRule
    {
    HandRank { get; }
    bool Evaluate(IEnumerable cards);
    }
    public class PairHandRule : IHandRule
    {
    public HandRank => HandRank.Pair;

    public bool Evaluate(IEnumerable cards)
    {

    }
    }
    so the rule list :
    private IEnumerable Rules => new List
    {
    new PairHandRule(),
    new FlushHandRule(),

    }
    The scoring function:
    public HandRank GetScore(Hand hand) =>
    Rules.OrderByDescending(r => r.Rank).First(r => r.Evaluate(hand.Cards)).Rank;

  • Christopher Hayes

    I like this article and I do the exact same thing BUT instead of putting the functions in a list (which i’ve done before) I now put them behind an interface. Why? because it allows me to EXTEND functionality without MODIFYING the original class thus following the Open Closed Principle https://en.wikipedia.org/wiki/Open/closed_principle
    Some may say this breaks the part about ‘Functional Programming’ and I agree BUT there would be a benefit to returning a function instead of just the bool and thus have delayed execution.

    • Ed Charbeneau

      Indeed, interfaces can be combined with the example given. The ideas outlined aren’t mutually exclusive. Thanks for the feedback!

  • Jason Willhite

    Just an FYI, the link at the top of the article that reads “C# Functional Programming workshop” links to “c:UsersedchaOneDriveWritingsedweb.me3s”

  • LamiaLove

    The functionality changes you suggest doing to the GetScore() method are needed improvements if you ask me.

    BUT

    1. You are confusing “functional programming” with “first-class functions”.
    These are not one and the same thing.
    While first-class functions are an essential characteristic of fictional programming, they can very much be used in a procedural way was well.
    I see this confusion a lot in articles on the web.
    For some reason, people also seem to think LINQ is somehow linked to functional programming.
    A lot of confusion on the web when it comes to functional programming.

    2. LINQ is horrible to read.
    So I would rather write a few extra lines in a more standard procedural way to greatly increase the readability of the code instead of chaining ad-hoc methods on and on, like LINQ does it.

    • Roger Alsing

      And you are confusing opinions with facts ๐Ÿ™‚