Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enumoji implementation in C# 14.0 #2

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
.idea
.idea
.ionide
.fake
2 changes: 1 addition & 1 deletion GameOfLife.sln
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameOfLife", "GameOfLife\GameOfLife.csproj", "{980E6F7A-195D-4F06-9398-B07DD274D20A}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameOfLife", "GameOfLife\GameOfLife.fsproj", "{980E6F7A-195D-4F06-9398-B07DD274D20A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
3 changes: 3 additions & 0 deletions GameOfLife/GameOfLife.csproj → GameOfLife/GameOfLife.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>

</Project>
125 changes: 0 additions & 125 deletions GameOfLife/Program.cs

This file was deleted.

67 changes: 67 additions & 0 deletions GameOfLife/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
module GameOfLife.Program
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although you can do C# style static classes in F# with some boilerplate. A module is the preferred vehicle for static functions. CLI wise it will look like a static class in C#.

open System
open System.Security.Cryptography
open System.Text
open System.Threading

let rows = 15
let columns = 15
let timeout = 500
Comment on lines +7 to +9
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can infer types on our statically scoped variables!

let mutable runSimulation = true
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

F# requires you to qualify any variable you want to mutate. You save so much boiler plate with type inference, adding this extra 6 let word hardly seems like a thing, and makes it easier to reason about variables. Notice we only have to use the keyword 3 times in this program ported directly from C#.


type Status = ``💀`` = 0 | ``😁`` = 1
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Enumoji's we don't have to use conditional expressions for the 3 data types needed for two states Alive|Dead in this port. We can then use the F# operators string, int, and enum to convert to what we need.


let private nextGeneration (currentGrid: Status [,]) =
Copy link
Author

@jbtule jbtule Sep 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although there is a recursive modifier (rec) that can be applied to a module or namespace, typically you declare your functions before you use them in F#. So nextGeneration helper function was moved to the top of the file in this port.

let nextGeneration = Array2D.zeroCreate rows columns
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

F# has functions to create arrays, rather than syntax. While an F# List is very different the C# List, an Array is identical.

// Loop through every cell
for row in 1..(rows-2) do
for column in 1..(columns-2) do
Comment on lines +17 to +18
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

F# doesn't have a post increment operator, so you can use a range operator in a for loop instead. F# ranges are inclusive at start and end, and can be used as a sequence, unlike new C# ranges. While imperative for loops are not the preferred F# method, this is how you would do them in F#. If you want to decrement from 10 to 1 the syntax is 10..-1..1

// find your alive neighbor
let mutable aliveNeighbors = 0
for i in -1..1 do
for j in -1..1 do
aliveNeighbors <- aliveNeighbors + int currentGrid.[row+i, column+i]
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no compound increment assign operators in F#. And notice that mutating a variable uses a different assignment operator <-.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also using int function to explicitly convert from enum to int. Implicit conversions are not a thing in F# for the sake of clarity.

let currentCell = currentGrid.[row, column]
// The cell needs to be subtracted
// from its neighbours as it was
// counted before
aliveNeighbors <- aliveNeighbors - int currentCell
// Implementing the Rules of Life
nextGeneration.[row,column] <-
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we didn't declare the variable nextGeneration mutable, the array cells themselves are mutable, as one would expect of an Array. That's actually why the default List type in F# is different, it's an immutable data structure, and immutability is preferred. But if you want to use a C# list it's really easy, the C# List type is aliased as ResizeArray.

match currentCell with
// Cell is lonely and dies OR Cell dies due to over population
| Status.``😁`` when aliveNeighbors < 2 || aliveNeighbors > 3 -> Status.``💀``
// A new cell is born
| Status.``💀`` when aliveNeighbors = 3 -> Status.``😁``
// stays the same
| unchanged -> unchanged
Comment on lines +31 to +37
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the are 4 rules, 2 of them are Alive to Dead cases, so I thought it was clearer to reduce to a compound case and have 3 cases. This could have been done in C# as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A match expression in F# is very much like a switch expression in C#. There are no match statements in F#, but they can act like that if every case returns unit (aka void) and syntactically expressed with empty parentheses. In F# unit type makes it easier to not special case void.

match currentCell with
     // Cell is lonely and dies OR Cell dies due to over population
     | Status.``😁`` when aliveNeighbors < 2 || aliveNeighbors > 3 ->  
           nextGeneration.[row,column] <-Status.``💀``
     // A new cell is born 
     | Status.``💀`` when aliveNeighbors = 3 -> 
           nextGeneration.[row,column] <- Status.``😁``
     // stays the same
     | unchanged -> ()

nextGeneration
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In F# the last line in an expression is the returned value. No return keyword necessary, but that also means no easy short circuiting.


let private print (future: Status[,]) =
let sb = StringBuilder()
for row in 0..(rows-1) do
for column in 0..(columns-1) do
future.[row, column] |> string |> sb.Append |> ignore
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pipe operator |> makes it easy to describe data flowing from one function to another. Otherwise it would have been written with nested parentheses:

ignore(sb.Append(string(future.[row, column])))

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string operator calls ToString() on an object. And the ignore operator is how you discard return values that you are not going to use (Like Append returns a reference to the string builder it's mutating. You'll get a warning if you have an expression that returns a value that isn't unit and it's not the last line of it's enclosing expression.

sb.AppendLine() |> ignore
Console.BackgroundColor <- ConsoleColor.Black
Console.CursorVisible <- false
Console.SetCursorPosition(0,0)
sb.ToString() |> Console.Write |> ignore
Thread.Sleep(timeout)

[<EntryPoint>]
let main _ =
// randomly initialize our grid
let mutable grid = Array2D.init rows columns (fun _ _ -> RandomNumberGenerator.GetInt32(0, 2) |> enum)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

F# array creation functions let you pass a lambda function to be used in initialization. Lambda's are prefixed with fun in F#, it's fun! Since we are randomly filling out the array, I discard the row and column arguments in the lambda. We can use the enum operator which is actually able to infer the enum type statically in this case!!

Console.CancelKeyPress.Add(
fun _ ->
runSimulation <- false
Console.WriteLine("\n👋 Ending simulation."))
Comment on lines +56 to +59
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I read somewhere that C# designers wished they never special cased += and -= operators to be AddHandlerand RemoveHandler methods. So those methods are what you can use in F# instead of the operators. To make things simpler without explicitly creating delegates, you can use the Add method with a lambda, as long as you don't need to remove the handler later.

// let's give our console
// a good scrubbing
Console.Clear();
// Displaying the grid
while runSimulation do
print grid
grid <- nextGeneration(grid)
0
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main function is expected to return an integer. So last line is a zero.