DEV Community

Cover image for Creating a .NET Core 3.0 F# Console App
Matt Eland
Matt Eland

Posted on • Updated on

Creating a .NET Core 3.0 F# Console App

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.

Console Application

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 hits X 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.

New FSharp Console App

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.

Class Library Project

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:

  1. WorldPos
  2. Actors
  3. World

FSharp Class Library Ordering

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}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)  
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Console 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)

Collapse
 
alexgman profile image
Alex Gordon

So why are you using f# to do oop style development?

Collapse
 
integerman profile image
Matt Eland

Because I'm still learning. If you look at the next article in the series, I fix a lot of it.

Collapse
 
ondrj profile image
Ondřej

Still good article :)
Keep on coding!