Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Functions as Data: Functional Programming in C#

DZone's Guide to

Functions as Data: Functional Programming in C#

Functions in C# are often thought of as static statements that our app can use to change the state of data in the system. But there are other ways to think about it.

· Big Data Zone ·
Free Resource

Hortonworks Sandbox for HDP and HDF is your chance to get started on learning, developing, testing and trying out new features. Each download comes preconfigured with interactive tutorials, sample data and developments from the Apache community.

By realigning your thinking about functions as data, you can uncover new solutions to problems in OOP. Let's look at an example of 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 that 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 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 of 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, thescore 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.
  6. public HandRank GetScore(Hand hand) => 
        HasRoyalFlush(hand.Cards) ? HandRank.RoyalFlush :
         ...
        HasPair(hand.Cards) ? HandRank.Pair :
        HandRank.HighCard;

In all of the previous examples, the order of operation is crucial. If we decide to add new rules to this scoring function, then we'll need to ensure 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 as 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 dataset 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 and map-based evaluation. This simple change expands the functionality of the application and reduces friction 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.

Hortonworks Community Connection (HCC) is an online collaboration destination for developers, DevOps, customers and partners to get answers to questions, collaborate on technical articles and share code examples from GitHub.  Join the discussion.

Topics:
big data ,functional programming ,c# ,oop ,linq ,tutorial ,data analytics

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}