Status of C# 8.0 functional features with a comparison to Haskell

-

Abstract

Writing C# func­tional code has be­come eas­ier with each new re­lease of the lan­guage (i.e. nul­lable ref types, tu­ples, switch expr, …). This doc­u­ment pre­sents a re­view of the cur­rent sta­tus of ba­sic func­tional fea­tures for C# 8.0. It fo­cuses mostly on syn­tax and aims to achieve its goal us­ing code ex­am­ples. It does­n’t touches on more ad­vanced top­ics as Monad, Functors, etc …

Haskell has been cho­sen as the comparison’ lan­guage (using few ex­am­ples from here and else­where). This is not in­tended as a state­ment of value of one lan­guage vs the other. The lan­guages are pro­foundly dif­fer­ent in un­der­ly­ing philoso­phies. They both do much more than what is pre­sented here (i.e. C# sup­ports OO and im­per­a­tive style, while Haskell goes much deeper in type sys­tem power etc…).

I also pre­sent sam­ples of us­age of lan­guage-ext to show what can be achieved in C# us­ing a func­tional li­brary.

Using a library of functions

In C# you can use sta­tic func­tions as if they were floating’ by im­port­ing the sta­tic class they are de­fined to with the syn­tax using static CLASSNAME. Below an ex­am­ple of both im­port­ing Console.WriteLine and us­ing it.

using System;
using System.IO;

using LanguageExt;
using LanguageExt.TypeClasses;
using LanguageExt.ClassInstances;
using System.Collections.Generic;

using static System.Console;
using static System.Linq.Enumerable;
using static System.Math;
using static LanguageExt.Prelude;

public static class Core {

    static void UseFunc() => WriteLine("System.WriteLine as a floating function");

Writing simple functions

In Haskell, a sim­ple func­tion might be writ­ten as:

   square :: Num a => a -> a
   square x = x * x

Which in C# looks like the be­low.

    static int Square(int x) => x * x; 

As an aside, note that we lose the gen­er­al­ity of the func­tion (i.e. we need a dif­fer­ent one for dou­bles).This is due to the lack of ad-hoc poly­mor­phism in C#. By us­ing lan­guage-ext, you can fake it so that it looks like this:

    static A Square<NumA, A>(A x) where NumA : struct, Num<A> => default(NumA).Product(x, x);

    static void SquarePoly() {
        WriteLine(Square<TInt, int>(2));
        WriteLine(Square<TDouble, double>(2.5));
    }

In Haskell, it is con­ven­tional to write the type of a func­tion be­fore the func­tion it­self. In C# you can use Func and Action to achieve a sim­i­lar goal (aka see­ing the types sep­a­rately from the func­tion im­ple­men­ta­tion), de­spite with more ver­bose syn­tax, as be­low:

    static Func<int, int> SquareF = x => x * x;

Pattern matching

Here is an ex­am­ple in Haskell:

    lucky :: (Integral a) => a -> String  
    lucky 7 = "LUCKY NUMBER SEVEN!"  
    lucky x = show x

In C#, you can write it in a few ways. Either as a func­tion sta­tic prop­erty …

    static Func<int, string> Lucky = x => x switch {
            7 => "LUCKY NUMBER SEVEN!",
            _ => x.ToString()
        };

Or with a nor­mal func­tion:

    static string Lucky1(int x) => x switch {
            7 => "LUCKY NUMBER SEVEN!",
            _ => x.ToString()
    };

Or even us­ing the ternary op­er­a­tor (a peren­nial fa­vorite of mine).

    static string Lucky2(int x) => x == 7 ? "LUCKY NUMBER SEVEN!"
                                          : x.ToString();

Pattern match­ing on tu­ples works with sim­i­lar syn­tax:

    static bool And(bool x, bool y) =>
        (x, y) switch
        {
            (true, true) => true,
            _            => false
        };

    

In Haskell you can match on lists as fol­lows:

    sum :: Num a => [a] -> a
    sum [] = 0
    sum (x:xs) = x + sum xs

Writing this in stan­dard C# looks like this:

    static int Sum(IEnumerable<int> l) => l.Count() == 0
                                            ? 0
                                            : l.First() + Sum(l.Skip(1));

    static int Sum1(IEnumerable<int> l) => l.Count() switch {
        0 => 0,
        _ => l.First() + Sum(l.Skip(1))
    };

Language-ext gives you a sim­pler syn­tax, with more flex­i­bil­ity on what you can match against:

    static int Sum2(Seq<int> l) =>
        match(l,
               () => 0,
               (x, xs) => x + Sum1(xs));

Obviously you al­ways have to be care­ful with re­cur­sion in C# (here). Better use the var­i­ous meth­ods on Enumerable.

Guards (and case expressions)

Let’s ex­plore guards. Case ex­pres­sions have an iden­ti­cal trans­la­tion in C#.

In Haskell guards are used as be­low:

    bmiTell :: (RealFloat a) => a -> a -> String  
    bmiTell weight height  
            | weight / height ^ 2 <= 18.5 = "Under"  
            | weight / height ^ 2 <= 25.0 = "Normal"  
            | weight / height ^ 2 <= 30.0 = "Over"  
            | otherwise                   = "Way over"

Which can be coded in C# as:

    static string BmiTell(double weight, double height) =>
        (weight, height) switch
        {
            _ when Pow(weight / height, 2) <= 18.5 => "Under",
            _ when Pow(weight / height, 2) <= 25.0 => "Normal",
            _ when Pow(weight / height, 2) <= 30.0 => "Over",
            _                                      => "Way over"
        };

Obviously this is quite bad. You would like some­thing more like:

    bmiTell :: (RealFloat a) => a -> a -> String  
    bmiTell weight height  
            | bmi <= skinny = "Under"  
            | bmi <= normal = "Normal"  
            | bmi <= fat    = "Over"  
            | otherwise     = "Way Over"  
            where bmi = weight / height ^ 2  
                  skinny = 18.5  
                  normal = 25.0  
                  fat = 30.0 

But it is not triv­ial in C# to de­clare vari­ables in ex­pres­sion bod­ied mem­bers. You can ei­ther move it to a nor­mal method or abuse the LINQ query syn­tax. Both shown be­low. No­tice that this is more sim­i­lar to let ex­pres­sions in Haskell, as they come be­fore the ex­pres­sion, not af­ter.

    static string BmiTell1(double weight, double height) {
        double bmi = Pow(weight / height, 2),
               skinny = 18.5,
               normal = 25.0,
               fat = 30;

        return bmi switch
        {
            _ when bmi <= skinny => "Under",
            _ when bmi <= normal => "Normal",
            _ when bmi <= fat    => "Over",
            _                    => "Way over"
        };
    }

    static string BmiTell2(double weight, double height) =>
        (   from _ in "x"
            let bmi     = Pow(weight / height, 2)
            let skinny  = 18.5
            let normal  = 25.0
            let fat     = 30
            select   bmi <= skinny ? "Under"
                   : bmi <= normal ? "Normal"
                   : bmi <= fat    ? "Over"
                   :                 "Way over").First();

Product types (aka Records)

In Haskell you de­fine a prod­uct type as be­low:

    data Person = Person { firstName :: String  
                             , lastName :: String  
                             , age :: Int
                             } deriving (Show)

In C#, it is cur­rently com­pli­cated to de­fine an im­mutable prod­uct type with struc­tural equal­ity, struc­tural or­der­ing and ef­fi­cient hash­ing.

In essence, you have to im­ple­ment a bunch of in­ter­faces and op­er­a­tors, some­how sim­i­lar to be­low (and I am not im­ple­ment­ing or­der­ing, and it is prob­a­bly not too ef­fi­cient ei­ther).

    public readonly struct PersonData: IEquatable<PersonData> {
        public readonly string FirstName;
        public readonly string LastName;
        public readonly int Age;

        public PersonData(string first, string last, int age) => (LastName, FirstName, Age) = (last, first, age);

        public override int GetHashCode() => (FirstName, LastName, Age).GetHashCode();
        public override bool Equals(object other) => other is PersonData l && Equals(l);
        public bool Equals(PersonData oth) => LastName == oth.LastName && FirstName == oth.FirstName && Age == oth.Age;
        public static bool operator ==(PersonData lhs, PersonData rhs) => lhs.Equals(rhs);
        public static bool operator !=(PersonData lhs, PersonData rhs) => !(lhs == rhs);
    }

If you use a struct, you are still open to some­one sim­ply new­ing it us­ing the de­fault con­struc­tor, that you can’t make private.

Using a class (as be­low) avoids that, but loses the pass by value se­man­tic.

    public class PersonData1 : IEquatable<PersonData1> {
        public readonly string FirstName;
        public readonly string LastName;
        public readonly int Age;

        public PersonData1(string first, string last, int age) => (LastName, FirstName, Age) = (last, first, age);

        public override int GetHashCode() => (FirstName, LastName, Age).GetHashCode();
        public override bool Equals(object oth) => oth is PersonData l && Equals(l);
        public bool Equals(PersonData1 other) => LastName == other.LastName && FirstName == other.FirstName && Age == other.Age;
        public static bool operator ==(PersonData1 lhs, PersonData1 rhs) => lhs.Equals(rhs);
        public static bool operator !=(PersonData1 lhs, PersonData1 rhs) => !(lhs == rhs);
    }

So, there is no easy fix. Using Language-ext you can do it much more sim­ply, by in­her­i­tance. But ob­vi­ously it uses IL gen­er­a­tion that is slow the first time around. Try run­ning the code and no­tice the de­lay when IL gen­er­at­ing.

    public class PersonData2 : Record<PersonData2> {
        public readonly string FirstName;
        public readonly string LastName;
        public readonly int Age;

        public PersonData2(string first, string last, int age) => (LastName, FirstName, Age) = (last, first, age);

    }

Sum types (aka Discriminated Union)

In Haskell you write:

    data Shape =
                Circle Float Float Float
            | Rectangle Float Float Float Float
            | NoShape
            deriving (Show)  

There is no ob­vi­ous equiv­a­lent in C#, and dif­fer­ent li­braries has sprung up to pro­pose pos­si­ble so­lu­tions (but not lan­guage-ext) (i.e. here or here).

One pos­si­ble pure lan­guage’ im­ple­men­ta­tion, not con­sid­er­ing struc­tural equal­ity/​or­der­ing/​hash, fol­lows:

    abstract class Shape {

        public sealed class NoShape : Shape { }
        public sealed class Circle : Shape {
            internal Circle(double r) => Radius = r;
            public readonly double Radius;
        }
        public sealed class Rectangle : Shape {
            internal Rectangle(double height, double width) => (Height, Width) = (height, width);
            public readonly double Height;
            public readonly double Width;
        }
    }

    static Shape.Circle Circle(double x)                 => new Shape.Circle(x);
    static Shape.Rectangle Rectangle(double x, double y) => new Shape.Rectangle(x,y);
    static Shape.NoShape NoShape()                       => new Shape.NoShape();

You can then pat­tern match on it in var­i­ous ob­vi­ous ways:

    static double Area(Shape s) => s switch
    {
        Shape.NoShape _                  => 0,
        Shape.Circle {Radius: var r }    => Pow(r, 2) * PI,
        Shape.Rectangle r                => r.Height * r.Width,
        _                                => throw new Exception("No known shape")
    };

    static void CalcAreas() {
        var c = Circle(10);
        var r = Rectangle(10, 3);
        var n = NoShape();

        WriteLine(Area(c));
        WriteLine(Area(r));
        WriteLine(Area(n));
    }

Maybe (Or Option) type

In Haskell you write:

     f::Int -> Maybe Int
     f 0 = Nothing
     f x = Just x

     g::Maybe Int -> Int
     g Nothing  = 0
     g (Just x) = x

In C#, this eas­ily trans­lates to Nullable value and ref­er­ence types. Assume you have Nullable = enabled in your pro­ject

    static int? F(int i) => i switch {
        0 => new Nullable<int>(),
        _ => i
    };

    static int G(int? i) => i ?? 0;

Main Method

Let’s wrap all the sam­ples with a Main func­tion and then write a full pro­gram.

    static void Main() {
        UseFunc();
        SquarePoly();
        WriteLine(Square(2) == SquareF(2));
        WriteLine(Sum(new[] { 1, 2, 3, 4 })  == Sum1(new[] { 1, 2, 3, 4 }));
        WriteLine(Sum1(new[] { 1, 2, 3, 4 }) == Sum2(new[] { 1, 2, 3, 4 }.ToSeq()));
        WriteLine(BmiTell(80, 100) == BmiTell1(80, 100));
        WriteLine(BmiTell(80, 100) == BmiTell2(80, 100));

        WriteLine(new PersonData("Bob", "Blake", 40) == new PersonData("Bob", "Blake", 40));
        WriteLine(new PersonData1("Bob", "Blake", 40) == new PersonData1("Bob", "Blake", 40));
        WriteLine("Before IL gen");
        WriteLine(new PersonData2("Bob", "Blake", 40) == new PersonData2("Bob", "Blake", 40));
        WriteLine(new PersonData2("Alphie", "Blake", 40) <= new PersonData2("Bob", "Blake", 40));
        WriteLine("Already genned");
        WriteLine(new PersonData2("Bob", "Blake", 40) == new PersonData2("Bob", "Blake", 40));
        WriteLine(new PersonData2("Alphie", "Blake", 40) <= new PersonData2("Bob", "Blake", 40));

        CalcAreas();

        Hangman.Core.MainHangman();

    }
}

Full program

Let’s fin­ish with a semi-work­ing ver­sion of Hangman, from an ex­er­cise in Haskell pro­gram­ming from first prin­ci­ples, just to get an over­all im­pres­sion of how the two lan­guages look for big­ger things.

The ex­er­cise was a fill-in-the-blanks kind of thing with the func­tion names and types given, so I don’t think I butcher it too badly, but maybe not.

The Haskell code is:

module Main where

import Control.Monad (forever) -- [1]
import Data.Char (toLower) -- [2]
import Data.Maybe (isJust) -- [3]
import Data.List (intersperse) -- [4]
import System.Exit (exitSuccess) -- [5]
import System.Random (randomRIO) -- [6]

type WordList = [String]

allWords :: IO WordList
allWords = do
      dict <- readFile "data/dict.txt"
      return (lines dict)

minWordLength :: Int
minWordLength = 5

maxWordLength :: Int
maxWordLength = 9

gameWords :: IO WordList
gameWords = do
      aw <- allWords
      return (filter gameLength aw)
      where gameLength w =
              let l = length (w :: String)
              in l > minWordLength && l < maxWordLength

randomWord :: WordList -> IO String
randomWord wl = do
      randomIndex <- randomRIO ( 0, length wl - 1)
      return $ wl !! randomIndex

randomWord' :: IO String
randomWord' = gameWords >>= randomWord

data Puzzle = Puzzle String [Maybe Char] [Char]

instance Show Puzzle where
      show (Puzzle _ discovered guessed) =
        (intersperse ' ' $ fmap renderPuzzleChar discovered)
        ++ " Guessed so far: " ++ guessed

freshPuzzle :: String -> Puzzle
freshPuzzle s = Puzzle s (map (const Nothing) s) []

charInWord :: Puzzle -> Char -> Bool
charInWord (Puzzle s _ _) c = c `elem` s

alreadyGuessed :: Puzzle -> Char -> Bool
alreadyGuessed (Puzzle _ _ s) c = c `elem` s

renderPuzzleChar :: Maybe Char -> Char
renderPuzzleChar Nothing  = '_'
renderPuzzleChar (Just c) = c

fillInCharacter :: Puzzle -> Char -> Puzzle
fillInCharacter (Puzzle word filledInSoFar s) c =
      Puzzle word newFilledInSoFar (c : s)
      where zipper guessed wordChar guessChar =
              if wordChar == guessed
                then Just wordChar
                else guessChar
            newFilledInSoFar =
              zipWith (zipper c) word filledInSoFar

handleGuess :: Puzzle -> Char -> IO Puzzle
handleGuess puzzle guess = do
      putStrLn $ "Your guess was: " ++ [guess]
      case (charInWord puzzle guess, alreadyGuessed puzzle guess) of
        (_, True) -> do
          putStrLn "You already guessed that character, pick something else!"
          return puzzle
        (True, _) -> do
          putStrLn "This character was in the word,filling in the word accordingly"
          return (fillInCharacter puzzle guess)
        (False, _) -> do
          putStrLn "This character wasn't in the word, try again."
          return (fillInCharacter puzzle guess)

gameOver :: Puzzle -> IO ()
gameOver (Puzzle wordToGuess _ guessed) =
      if (length guessed) > 7 then do
          putStrLn "You lose!"
          putStrLn $ "The word was: " ++ wordToGuess
          exitSuccess
      else return ()

gameWin :: Puzzle -> IO ()
gameWin (Puzzle _ filledInSoFar _) =
      if all isJust filledInSoFar then do
        putStrLn "You win!"
        exitSuccess
      else return ()

runGame :: Puzzle -> IO ()
runGame puzzle = forever $ do
      gameOver puzzle
      gameWin puzzle
      putStrLn $ "Current puzzle is: " ++ show puzzle
      putStr "Guess a letter: "
      guess <- getLine
      case guess of
        [c] -> handleGuess puzzle c >>= runGame
        _ -> putStrLn "Your guess must be a single character"


main :: IO ()
main = do
      word <- randomWord'
      let puzzle = freshPuzzle (fmap toLower word)
      runGame puzzle

Which loosely trans­late to the code be­low (pure C#, no lan­guage-ext) . A few com­ments:

  1. I tried to keep the translation as 1:1 as possible.
  2. I used expression bodied members for everything except IO returning function. That’s a pleasing convention to me.
  3. Things translate rather straightforwardly except for:
    1. Cheated using a simple struct instead of a Record, but often it is ok to do so.
    2. Needed to use LINQ query syntax to translate more complex expressions, but cannot have lambadas in it.
    3. Needed to do manual currying (language-ext would have beautified that).
  4. The line count for this example is roughly similar. I think that’s random. Also the C# code ‘extends to the right’ more.
  5. Notably absent from the code are sum types, which would have been verbose to implement in C#.
namespace Hangman {
    using WordList = IEnumerable<String>;
    using static System.Linq.Enumerable;

    static class Core {

        const int MinWordLength = 5;
        const int MaxWordLength = 9;

        static WordList AllWords => File.ReadAllLines("data/dict.txt");

        static WordList GameWords =>
            AllWords.Where(w => w.Length > MinWordLength && w.Length < MaxWordLength);

        static Random r = new Random();

        static string RandomWord(WordList wl) => GameWords.ElementAt(r.Next(0, wl.Length()));

        static string RandomWord1 => RandomWord(GameWords);

        static char RenderPuzzleChar(char? c) => c ?? '_';

        struct Puzzle { // Not implemented Eq and Ord because not needed in this program
            public string Word;
            public IEnumerable<char?> Discovered;
            public string Guessed;

            public override string ToString() =>
                $"{string.Join(" ", Discovered.Select(RenderPuzzleChar))}"
                    + " Guessed so far: " + Guessed;
        }

        static Puzzle FreshPuzzle(string s) => new Puzzle {
            Word       = s,
            Discovered = s.Select(_ => new Nullable<char>()),
            Guessed    = ""
        };

        static bool CharInWord(Puzzle p, char c)     => p.Word.Contains(c);
        static bool AlreadyGuessed(Puzzle p, char c) => p.Guessed.Contains(c);

        // Can't assign lambda expression to range variable with let, hence separate function
        static char? Zipper(char guessed, char wordChar, char? guessChar) =>
            wordChar == guessed ? wordChar : guessChar;

        // Manual curry. Could use language-ext to make it more beautiful.
        static Func<char, char?, char?> Zipper1(char c) => (c1, c2) => Zipper(c, c1, c2);

        static Puzzle FillInCharacter(Puzzle p, char c) =>
            (from _ in "x"
             let newFilledInSoFar = System.Linq.Enumerable.Zip<char, char?, char?>(p.Word, p.Discovered, Zipper1(c))
             select new Puzzle {
                Word = p.Word,
                Discovered = newFilledInSoFar,
                Guessed = c + p.Guessed
            }).First();

        static Puzzle HandleGuess(Puzzle puzzle, char guess) {// Braces means IO ...
            WriteLine($"Your guess was {guess}");
            switch (CharInWord(puzzle, guess), AlreadyGuessed(puzzle, guess)) {
                case (_, true):
                    WriteLine("You already guessed that character, pick something else!");
                    return puzzle;
                case (true, _):
                    WriteLine("This character was in the word,filling in the word accordingly");
                    return FillInCharacter(puzzle, guess);
                case (false, _):
                    WriteLine("This character wasn't in the word, try again.");
                    return FillInCharacter(puzzle, guess);
            }
        }

        static void GameOver(Puzzle p) {
            if(p.Guessed.Length > 7) {
                WriteLine("You lose!");
                WriteLine($"The word was {p.Word}");
                Environment.Exit(0);
            }
        }

        static void GameWin(Puzzle p) {
            if(p.Discovered.All(c => c.HasValue)) {
                WriteLine("You win!");
                Environment.Exit(0);
            }
        }

        static void RunGame(Puzzle puzzle) {
            while(true) {
                GameOver(puzzle);
                GameWin(puzzle);
                WriteLine($"Current puzzle is: {puzzle}");
                WriteLine("Guess a letter: ");
                var guess = ReadLine();
                if(guess.Length == 1) RunGame(HandleGuess(puzzle, guess[0]));
                else WriteLine("Your guess must be a single char");
            }
        }

        public static void MainHangman() {
            var puzzle = FreshPuzzle(RandomWord1);
            RunGame(puzzle);
        }

    }
}

Tags