Language-ext: Request for code review on F# Tic-Tac-Toe port

Created on 24 Aug 2018  路  20Comments  路  Source: louthy/language-ext

Hi,

As a learning exercise on FP and LangExt I decided to port simple Tic-Tac-Toe game from F# (which I am also learning but can't use at work yet)to C# / LangeExt.

Original game code is here: code

with YT video here: link

Originally, I wanted to port 2 more additions link to original code (pure IO and AI) but they both use IO monads which I am too unfamiliar with to understated, let alone port it to C#.

My effort is here: link

I tried to stay as close as possible semantically to original version (same function names and paramaters) so it was easier to follow the port but I'd also like to know if tools in LangExt could make it better. Validations maybe?

Could you kindly review my version, comment on it and help me make it better? I had issues with implementing F# discriminated unions (went with "safe enum C# idiom but not fully) especially in the context of equality. Was not sure if Row should indeed be a NewType of a tuple etc.

Thanks for LangExt and for the help!

examples / documentation

Most helpful comment

parseMove can be done more functionally like this:
c# static Option<Position> parseMove(string str) => from s in Optional(str) let t = s.Split(' ') where t.Length == 2 from r in parseOneThroughThree(t[0]) from c in parseOneThroughThree(t[1]) select Position(c, r);

All 20 comments

I had issues with implementing F# discriminated unions (went with "safe enum C# idiom but not fully) especially in the context of equality.

I am curious about this part. Can you provide a link to an example of this in your code?

@bender2k14
Well, my understanding is that F# discriminated unions / sum types are derivatives of common base classes in C#, this allows for pattern matching witch if is / switch in C#7 link

so where in F# there is:
type OneThroughThree = | One | Two | Three

in my code:

public abstract class OneThroughThree
    {
      public static readonly One One = new One();
      public static readonly Two Two = new Two();
      public static readonly Three Three = new Three();

      public static readonly OneThroughThree[] Values = new OneThroughThree[] { One, Two, Three };

      protected OneThroughThree() { }
    }

    public class One : OneThroughThree { }
    public class Two : OneThroughThree { }
    public class Three : OneThroughThree { }

so later you can:

// where row is of type OneThroughThree
switch (row)
{
  case One _:
  case Two _:
 ...
}

I am having difficultly following your example. Can you share a link to a specific location in the code that you are asking us to review that where you

went with "safe enum C# idiom but not fully

?

I am asking for a review for all of it ;) Original code is only 179 lines of F# code, my C#/LangExt version is 3 times this much but most of it is actually syntactic noise like "{}" and explicit class ctors. Both code gists (original and mine) are fully working console-based Tic-Tac-Toe games so it can be built and run.

Ah, I looked at your C# code. Here is the end of a specific example. You have to include this "dead code" because (quoting from here)

The compiler does not emit a warning in those cases where a default case has been written but will never execute. This is consistent with current switch statement behavior where all possible cases have been listed.

Although you don't have a default case, what you do is equivalent in this case.

There is a "safer" way to implement an #F discriminated union in C#, which is to use the visitor pattern. Then you won't need dead code like that linked above. Furthermore, the code would not compile if you added more cases (like converting from 3-by-3 tic-tac-toe to 5-by-5).

I am asking for a review for all of it ;)

And every journey occurs one step at a time ;)

I know of visitor pattern and use it in OO code but I really wanted to go FP port here, sum types, pattern matching and all. Do you have any other insights into my version of original code? Maybe you'd implement TTT game using LangExt in a very different way? Would love to get some feedback.

What about the visitor pattern is contrary to style of functional programming?

Not claiming it is contrary (in fact it is a sum-type link) but visitor pattern is based on virtual methods, double-dispach and generally a OO solution while FP's go-to solution for sum types is via pattern matching. Since I am doing this for learning purposes that is the way I took here.

Don't be confused by Microsoft's Pattern Matching article that you linked to above. It is not equivalent to the FP concept of pattern matching. Instead, that article is about the idiomatic way to do "C# pattern matching". In contrast, Language Ext

uses and abuses the features of C# to provide a functional-programming 'Base class library', that, if you squint, can look like extensions to the language itself. The desire here is to make programming in C# much more reliable...

An opportunity exists in your C# code to make it more consistent with the spirit of this library: more reliable by being less idiomatic and more faithfully mapping sum types.

@bender2k14 while I get that C#'s pattern matching isn't the same as say F#'s, it does come from FP land. The idea is to match the actual type of an object and operate on that data. On that note I just rewrote this part of my code:

        switch (outcome(newBoard))
        {
          case Winner winner:
            Console.WriteLine($"{winner.Letters} wins!!");
            Console.WriteLine(render(newBoard));
            break;

          case Draw _:
            Console.WriteLine("It is a draw!");
            break;

          case NoneYet _:
            playIo(GameState(newBoard, otherPlayer(gameState.WhoseTurn)));
            break;
        }

to:

        outcome(newBoard).Match(
          winner: letter =>
          {
            Console.WriteLine($"{letter} wins!!");
            Console.WriteLine(render(newBoard));
          },
          draw: () =>
          {
            Console.WriteLine("It is a draw!");
          },
          noneYet: () =>
          {
            playIo(GameState(newBoard, otherPlayer(gameState.WhoseTurn)));
          });

as described in the visitor pattern / sum type article I linked earlier. While it is not looking as FPish, it has a very nice property: it enforces exhaustive matching by requiring to pass all 3 Func<> arguments.

Which approach do you think is better?

Also, what about rest of the code?!?! ;)

Trying to replicate sum types as you have is fine. In my opinion it's the best way without any direct language support. Writing code to do this (like your example with Match and lambdas) is a bad idea. It's far too limited compared to the existing language support and also has a runtime overhead that's entirely unnecessary here.

One thing you could look to change is how you do the matching. Instead of using switch which has no return value, use the ternary operator:

So, this:
```c#
public static string renderValue(Value letter)
{
switch (letter)
{
case Letter l when l.Value == Letters.O: return "O";
case Letter l when l.Value == Letters.X: return "X";
case Unspecified _: return " ";
}

    throw new ArgumentOutOfRangeException();
  }
Would become:
```c#
      public static string renderValue(Value letter) =>
          letter is Letter l && l.Value == Letters.O ? "O"
        : letter is Letter l && l.Value == Letters.X ? "X"
        : letter is Unspecified _                    ? " " 
        : throw new ArgumentOutOfRangeException();

It has the benefits of being more terse and so closer to F# and it's an expression, which is what match is in F#.

Which approach do you think is better?

It depends. Towards the beginning of an application's life, I think it is better to use the more flexible but less save approach that you first used (possibly with @louthy's syntax). As an application matures (which also means that it is changing less), I think it becomes increasingly better to use the less flexible but more safe visitor pattern approach. If you only ever want to pick one and never change, then I would typically go with the former.

Ah, ternary version looks good indeed, thanks. My version with virtual Match<> method is based on Mark Seemann's article, additionally to forcing match exhaustion it also has a property of being an expression if needed as Match() may return value as well. Why do you think this approach is limited?

Is there anything else you think could be improved in this implementation? What other tools are there in LangExt to make it better. For example, I feel like this part of the code needs improving:

      public static Option<Position> parseMove(string str)
      {
        var tokens = str.Split(' ');
        if (tokens.Length == 2)
        {
          return from row in parseOneThroughThree(tokens[0])
                 from col in parseOneThroughThree(tokens[1])
                 select Position(col, row);
        }
        return None;
      }

Why do you think this approach is limited?

Because:

  • it's just case matching, it doesn't add additional logic. So you can't refine your match (like the renderValue which matches on Letter then matches on the letter value).
  • So, without above you can't match twice on the same type
  • You must provide all cases

    • If you look at how you match in F# and the like, it's not always the case that you provide logic for all cases, you sometimes want to fall-through to otherwise.

  • You can't provide a default.
  • You spend most of your life writing boilerplate
  • You will add bugs to your boilerplate as you lose to will to live writing it
  • There are 2n memory allocations per use, where n is the number of cases.

Obviously, I think this approach has some merit, because it's how matching works in Option, Either, etc. in lang-ext. But they're very simple types. I think when you start expanding the scope of the type then you lose a lot by taking this approach.

parseMove can be done more functionally like this:
c# static Option<Position> parseMove(string str) => from s in Optional(str) let t = s.Split(' ') where t.Length == 2 from r in parseOneThroughThree(t[0]) from c in parseOneThroughThree(t[1]) select Position(c, r);

You can probably use Language Ext to remove every if from your code, and it will be better for it.

@louthy thanks for awesome answers, your arguments make sense to me, more functional version of parseMove() looks great and if I understand correctly, now also handles null case for input str.

Mark Seemann isn't that bad ;)) Indeed he was against going full FP in C# but only for the reason of the code being too different from "classic" C# especially when working with programmers that do not know FP techniques. He is not against DI in OO code, in fact he is about to release next edition of his book on the topic, indeed he blogged tho that DI is not needed (DI rejection) in pure FP code. I do understand your general sentiment about blindly following "gurus".

Thing is he does blog about FP in C# and there isn't much to find on the topic. To be honest for last 5 days I am googling for ANYTHING on LangExt to learn it and it is no picnic ;)

More specific questions then:

link and link deal with input parsing and validation. I feel like Validation<> would be of use here, could you help me here?

This code does not use IO monad for Console <-> user interaction. Is there something in LangExt I could use to fix that?

@bender2k14 examples from my code pretty please :)

For example,

public static Option<Board> makeMove(Board board, Move move)
{
  if (select(board, move.At) is Unspecified)
    return Some(placePieceIfCan(move.Place.Value, board, move.At));
  return None;
}

could be written with a ternary as

public static Option<Board> makeMove(Board board, Move move) =>
  select(board, move.At) is Unspecified
    ? Some(placePieceIfCan(move.Place.Value, board, move.At));
    : None;

though this change doesn't depend on Language Ext. @louthy already suggested how to remove this if using Language Ext. The remaining ifs in select and set can also be replaced with ternaries to help rid the code of noise as @louthy showed here.

@slimshader You may want to look at the issues that have been labelled as examples / documentation

Also, there's some useful stuff in the wiki on Thinking Functionally

In terms of an IO monad, you'll probably find it's a bit of a waste of time for C#, but for certain situations where you want to define an algebra of possible IO operations and be able to abstract away from the underlying implementation you can use a Free Monad. The IO monad in Haskell is essentially a more specialised Free Monad. This was discussed here and there are two example projects in the Samples folder called AccountingDSL and BankingAppSample that introduce the concept.

Note: This isn't a feature of lang-ext yet, as a properly generalised Free Monad or even an IO monad is very difficult to achieve without higher-kinds. I am working on it though.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

HistoricallyCorrect picture HistoricallyCorrect  路  6Comments

michael-wolfenden picture michael-wolfenden  路  5Comments

Malgefor picture Malgefor  路  4Comments

TonyHernandezAtMS picture TonyHernandezAtMS  路  5Comments

khtan picture khtan  路  4Comments