This is part 1 in a new tutorial series on creating a genetic algorithm in F# and .NET Core 3.0.
Learning Goals
This tutorial is focused on creating a new console application and learning some of the basics of F#. By the end of this tutorial you should be able to:
- Understand the basics of F#
- Create a new F# class library
- Create a new F# console application and link it to the class library
- Code a basic console input loop
- Code simple functions and classes in F#
Prerequisites
Before starting, you will need to have Visual Studio 2019 installed (Community edition is free and fine for this purpose). You will also need to make sure the .NET desktop development workload in checked when installing Visual Studio.
Understanding F Sharp
Important Disclaimer: The author is an F# novice. I've read a few books and written a neural net in F# as well as the code from this article series, but otherwise I am very new to the language. This represents my best attempt to share the knowledge I have learned, but does not constitute an authoritative 'best practice' type of model and may include inaccuracies based on incomplete understanding. Still, I want to share what I have learned
F# is a functional programming language that is part of the .NET language family.
The advantage of functional programming languages typically lies in application quality. F# handles nulls better by default and concepts such as discriminated unions and pattern matching make it harder to make mistakes.
Additionally, F#'s syntax is more concise than C#, meaning that it takes significantly fewer lines of code to express the same intent as it does in C# or other languages. Because you have fewer lines of code, it's harder for bugs to hide.
Because F# is part of .NET, it compiles down to IL and runs as part of .NET Framework and .NET Core.
This means that other .NET languages such as C# and VB .NET can interact with F# libraries. This also means that you can mix functional programming and object-oriented programming based on the particular needs of what you're programming.
F# is also not strictly limited to functional programming as F# can be used to create traditional classes following .NET conventions (though the syntax is sometimes very ugly to do so).
Understanding .NET Core 3.0 Console Apps
Console Applications are text-based utilities that run from the command line. They're typically used as part of automated processes or to integrate with other tools.
.NET Core 3.0 console apps operate cross-platform and are not strongly tied to Windows like .NET Framework console apps were.
In this tutorial series, the end result is not going to be a console application, but for the purposes of focusing on the code at first, we'll start with a console application for simplicity.
The Application we'll Create
This is part one of a multi-part series on building a genetic algorithm in F#. The series will feature a 2D game board featuring a squirrel, a dog, an acorn, a tree, and a rabbit. As the series progresses, we'll talk more about what we'll simulate and how it will work as well as what genetic algorithms are.
For now, we'll create a simple console application that generates a game board with a Squirrel somewhere on it, then displays the game board to the user and allows them to regenerate a new game board at random.
In order to do this we'll need:
- A
WorldPos
type that stores a 2D location in the game world - A
Squirrel
class that inherits from an Actor class - A
World
class that arranges the actors in the simulation - A console application that displays the current
World
and prompts the user for input, repeating the loop until the user hitsX
to exit
Let's get started.
Setting up the Solution
Our solution will contain two projects initially, a .NET Core console application and a .NET Standard class library.
Create the Console App
In Visual Studio, create a new project. In the new project wizard, change the Language Type
drop down to F# and then look for the Console App (.NET Core)
option as pictured below. Make sure that it lists F# as the language.
Click Next
, name the project whatever you'd like. Whatever name you choose, I recommend you include the name 'ConsoleApp' somewhere in the name to help remember that this is the console application as we will have multiple projects.
Create the .NET Standard Library
In Visual Studio, right click on the solution at the top of your solution explorer and choose Add > New Project...
.
From there, select the F# Class Library (.NET Standard) option as pictured below.
Make sure that the option has F# specified as the language and that you select the .NET Standard option, not the .NET Core option.
While .NET Core could work, the advantage of .NET Standard is that it can be referenced from a .NET Framework or .NET Core application. If you ever wanted to add a .NET Framework 4.8 application of some sort, you would be unable to reference a .NET Core class library.
My general rule is that unless I have a compelling reason not to, I will always create .NET Standard class libraries.
Click next and name the class library whatever you'd like, then create it. I recommend ending the name of the library with Logic, Domain, or DomainLogic so that it's clear that this is a class library handling application logic. For the rest of this series, I will refer to this as the domain logic library.
Reference the Class Library from the Console Application
Now that we have our two projects in the same solution, expand the console application in the solution explorer, right click on the Dependencies
node, and click Add Reference...
.
On the projects tab, check the box next to the domain logic library you created above and click Ok. This will allow your .NET Core console app to reference logic in the domain logic library.
Adding the Domain Logic
Before we can implement the console application, we'll need to create the classes it references.
Note that in F#, the order of files inside of a project matters. F# will load files from top to bottom, so files at the top cannot reference values defined below them. Visual Studio lets you use alt and the arrow keys to move items up and down in the solution explorer and to add new items above or below an existing project.
The order we'll need for this application is:
- WorldPos
- Actors
- World
Go ahead and create empty F# script files named these things. You may delete or rename the default F# file that the class library starts with.
WorldPos
In the WorldPos file, we'll add the following code:
namespace MattEland.FSharpGeneticAlgorithm.Logic
module WorldPos =
type WorldPos = {X: int32; Y:int32}
let newPos x y = {X = x; Y = y}
Here we're saying that everything belongs in the MattEland.FSharpGeneticAlgorithm.Logic
namespace instead of in a root namespace. This helps keep things organized.
Next, we declare a module called WorldPos
. This will allow other files to open (import) the logic we define here.
Next we define a simple type called WorldPos
that consists of two integer values: X and Y. This compiles down as a simple class, but notice the syntax is incredibly minimal.
Finally, we define a function named newPos
that takes in two parameters named x
and y
. This function will return a new object with an X
and Y
property.
Here's the interesting part: F# interprets return type as a WorldPos
even though no explicit syntax exists declaring this. This is because there is nothing being imported via an open statement that could match the result of newPos
besides the WorldPos
type declared above. If there were, some additional type declarations would be explicitly necessary.
Actors
Next let's look at the actors file:
namespace MattEland.FSharpGeneticAlgorithm.Logic
open MattEland.FSharpGeneticAlgorithm.Logic.WorldPos
module Actors =
[<AbstractClass>]
type Actor(pos: WorldPos) =
member this.Pos = pos
abstract member Character: char
type Squirrel(pos: WorldPos, hasAcorn: bool) =
inherit Actor(pos)
member this.HasAcorn = hasAcorn
override this.Character = 'S'
let createSquirrel pos = new Squirrel(pos, false)
Like before, we're declaring a namespace and a module, but here we're opening another module, in this case the WorldPos
module we defined earlier.
Next we define an abstract class called Actor
and decorate it with an AbstractClass
attribute telling F# that this type should be implemented abstractly. This style of syntax is frequently needed for object-oriented programming concepts.
We define a constructor on Actor
that takes in a WorldPos
. The class defines a Pos
member that returns the pos
argument from the constructor. Note that Pos
is not implemented as a property and cannot be modified as F# values as defined immutable by default.
Next we define an abstract Character
that will return a .NET char
type.
The Squirrel type works similarly to Actor, but is not abstract. It explicitly inherits Actor
and invokes its constructor. It exposes the hasAcorn
parameter via the HasAcorn
member, and then it overrides the Character
value and represents the squirrel class with the S
character.
For those more familiar with F#, note that I'm choosing to work with abstract classes here instead of the F# concept of a discriminated union because it's easier to have sequences (F# collections) with different types sharing the same base class than it is to have sequences of different members of discriminated unions.
Finally, we expose a createSquirrel
function that creates a new Squirrel
instance at the specified pos
. It is defined without an acorn, which makes the squirrel sad.
World
Okay, so now we're seeing some repetitive patterns in defining members. Let's do something a bit more complex.
namespace MattEland.FSharpGeneticAlgorithm.Logic
open System
open MattEland.FSharpGeneticAlgorithm.Logic.Actors
open MattEland.FSharpGeneticAlgorithm.Logic.WorldPos
module World =
let getRandomPos(maxX:int32, maxY:int32, random: Random): WorldPos =
let x = random.Next(maxX) + 1
let y = random.Next(maxY) + 1
newPos x y
let generate (maxX:int32, maxY:int32, random: Random): Actor seq =
let pos = getRandomPos(maxX, maxY, random)
seq {
yield createSquirrel pos
}
type World (maxX: int32, maxY: int32, random: Random) =
let actors = generate(maxX, maxY, random)
member this.Actors = actors
member this.MaxX = maxX
member this.MaxY = maxY
member this.GetCharacterAtCell(x, y) =
let mutable char = '.'
for actor in this.Actors do
if actor.Pos.X = x && actor.Pos.Y = y then
char <- actor.Character
char
Here we start to see a few bits of new syntax.
getRandomPos
is defined as a method (note the parentheses). This is important in this instance because otherwise F# will not re-evaluate the results of a call due to a process called memoization. Since we want to get a different random position every time, it's important to include these parentheses.
getRandomPos
will declare x
and y
as results of the System.Random
instance, holding on to a location within the game world.
Finally, getRandomPos
will call newPos
to build the position object. Because this is the last line in the method, its return WorldPos
is returned by the method. Note that we do not use explicit return
statements in F#.
generate
exposes some new syntax. Instead of single result types, we're now working with sequences, an F# version of an immutable collection that can be iterated over. The Actor seq
syntax indicates that the method will return a sequence of zero to many Actor
instances.
Inside of the generate
method we define a seq { ... }
block. In this block we yield instances of that sequence. For now, we're only including a single Squirrel, but in future parts of this tutorial we will include a wider variety of objects.
Next we define the World
class. This type manages the game board and arrangement of actors within it.
Note that inside of this type definition we declare an actor
variable immediately, then expose that instance via the Actors
member.
The GetCharacterAtCell
method on World
has some interesting syntax.
First, char
is defined as a mutable variable, meaning that it can be assigned a new value to it after its initial assignment. This goes back to F# declaring things as immutable by default and viewing mutability as an anti-pattern to be minimized. The char <- actor.Character
statement later will reassignchar
to hold the value to the right of the arrow.
Secondly, for actor in this.Actors do
defines an F# for loop. Note that indentation governs the beginning and ending of the for block and no end for
style syntax is necessary.
Thirdly, we see an example of F# conditional logic in the if actor.Pos.X = x && actor.Pos.Y = y then
statement. This operates very similar to C# other than we do not have parentheses, we use a single =
operator, and the if
statement doesn't include an end-if, just like the for
loop.
Finally, we end the method with a single char
statement to load the char
variable into memory and return it as the last statement in the method.
Building the Console Application
Now that we can see a bit more of how F# logic flows, let's get the console application operational and play around with it.
This is a lot smaller than the domain logic library and will include a collection of helper functions related to dealing with console input and output and a function representing the main entry point in the application and user input loop.
Display Functions
namespace MattEland.FSharpGeneticAlgorithm.ConsoleTestApp
open System
open MattEland.FSharpGeneticAlgorithm.Logic.World
module Display =
let printCell char isLastCell =
if isLastCell then
printfn "%c" char
else
printf "%c" char
let displayWorld (world: World) =
printfn ""
for y in 1..world.MaxX do
for x in 1..world.MaxY do
let char = world.GetCharacterAtCell(x, y)
printCell char (x = world.MaxX)
let getUserInput(): ConsoleKeyInfo =
printfn ""
printfn "Press R to regenerate or X to exit"
Console.ReadKey(true)
printCell
is a simple function that will display char
on the console. If isLastCell
is true, then the printfn
method will be used which includes a line break, otherwise printf
will be used which will not move down to the next row. The "%c" char
syntax formats the char
character into the string.
displayWorld
uses two nested for loops to loop row by row column by column through the game world by relying on the MaxX
and MaxY
properties on the World
. From there it calls the logic we implemented earlier and then invokes the printCell
method. Note that we enclose x = world.MaxX
in parentheses in order to pass in the boolean result of that evaluation as the isLastCell
parameter.
The getUserInput
method (again, defined as a method to not memoize the results) prompts the user for input, grabs the first key from the keyboard, and returns the result of that call (since it's the last statement of the method).
Main Input Loop
Okay, now the real meat and potatoes of the console application:
open System
open MattEland.FSharpGeneticAlgorithm.Logic.World
open MattEland.FSharpGeneticAlgorithm.ConsoleTestApp.Display
let generateWorld randomizer =
new World(8, 8, randomizer)
[<EntryPoint>]
let main argv =
printfn "F# Console Application Tutorial by Matt Eland"
let randomizer = new Random()
let mutable simulating: bool = true
let mutable world = generateWorld(randomizer)
while simulating do
displayWorld world
let key = getUserInput()
Console.Clear()
match key.Key with
| ConsoleKey.X -> simulating <- false
| ConsoleKey.R -> world <- generateWorld(randomizer)
| _ -> printfn "Invalid input '%c'" key.KeyChar
0 // return an integer exit code
In this final class of ours, we declare a generateWorld
function to keep logic for creating a new World
object in one place.
The main
function is defined as the primary entry point of the application via the EntryPoint
attribute.
Here we define some new variables needed for the core loop, declaring simulating
and world
as mutable as they can change inside the main application loop.
The while simulating do
loop will repeatedly display the state of the world via the displayWorld
function, then grab the user input via getUserInput
, clear the console so that every iteration you only see the world's state.
Finally, we use match
to effectively switch on the key
that was pressed. The | ... ->
syntax indicates a case to match, with logic to execute to the right of the ->
.
For example, when the X
key is pressed, simulating <- false
runs which sets simulating
to false
, causing the loop to terminate.
The | _ ->
syntax indicates a default match - matching any case that was not otherwise matched explicitly. In this case, we use it to tell the user they entered something not expected / supported.
The final 0
statement tells the application to terminate and return 0 for a non-error exit code.
The Finished Result
If you run the application, you should be prompted for input and be able to hit R
to regenerate the world and see the position of the squirrel change, or X
to exit the application.
If you can't build or want to look at the source code, check out the article1
branch on GitHub.
Next Up
Next article I'll expand out the domain logic library to include the other actor types and turn the application into a mini-game where the player controls the squirrel. This will set us up for later articles where we will create a genetic algorithm and neural network to control the squirrel and display the simulation in something nicer than a text-based console application.
Top comments (3)
So why are you using f# to do oop style development?
Because I'm still learning. If you look at the next article in the series, I fix a lot of it.
Still good article :)
Keep on coding!