Post

Getting Started with F# and .NET 6

F# is a functional-first programming language that runs on the .NET platform. It combines the power of functional programming with the interoperability and performance of .NET. F# can help you write concise, expressive, and reliable code for a variety of applications.

.NET 6 is the latest version of the .NET platform that was released in November 2021. It is a unified platform that supports cross-platform development, cloud-native applications, and high-performance computing. .NET 6 also introduces many new features and improvements for F# developers.

To understand the difference between .NET Framework, .NET Core and .NET 6, you can read the my blog post A Comprehensive Comparison: .NET Framework vs .NET Core vs .NET 5/.NET 6.

In this blog post, I will help you get started with F# in the context of .NET 6. You will learn how to install .NET 6 and F#, how to create your first F# project, how to work with F# data types and functions, and how to use pattern matching. By the end of this post, you will have a basic understanding of F# and its capabilities.

Understanding F#

What is F#?

F# is a functional-first programming language that supports multiple programming paradigms, such as object-oriented, imperative, and concurrent programming. F# is a member of the ML family of languages, which includes OCaml and Haskell. F# is also a .NET language, which means it can interoperate with other .NET languages, such as C# and VB.NET, and use the .NET libraries and frameworks.

Advantages of using F#

F# has many advantages that make it a great choice for modern software development. Some of the main advantages are:

  • Concise and expressive syntax: F# has a simple and elegant syntax that lets you write less code and focus on the logic and meaning of your program. F# also supports many syntactic features that make your code more readable and expressive, such as type inference, pattern matching, list comprehensions, and computation expressions.
  • Strong type system: F# has a powerful and flexible type system that helps you catch errors at compile time and enforce correctness and safety in your code. F# also supports advanced type features, such as generics, algebraic data types, units of measure, and type providers, that enable you to model complex domains and work with diverse data sources.
  • Functional programming capabilities: F# is a functional-first language, which means it emphasizes the use of pure functions, immutable data, and higher-order functions. Functional programming can help you write code that is easier to reason about, test, and maintain. F# also supports many functional programming concepts and techniques, such as recursion, currying, partial application, and monads, that enable you to write elegant and expressive code.

Installing .NET 6 and F#

Installing .NET 6 SDK

To get started with F# and .NET 6, you need to install the .NET 6 SDK, which is a set of tools and libraries that you need to develop and run .NET applications. The .NET 6 SDK is available for Windows, macOS, and Linux, and you can download it from the official .NET website.

The installation process may vary depending on your operating system, but in general, you need to download the installer or the binary archive for your platform and follow the instructions to install the .NET 6 SDK.

To verify that you have successfully installed the .NET 6 SDK, you can open a terminal or a command prompt and run the following command:

1
dotnet --version

This should display the version of the .NET SDK that you have installed, which should be 6.0.0 or higher.

Adding F# to the development environment

After installing the .NET 6 SDK, you also need to add F# language support to your development environment. This will enable you to write, compile, and run F# code using your preferred code editor or IDE.

There are many options for developing F# applications, but in this blog post, I will focus on two popular ones: Visual Studio Code and Visual Studio.

Visual Studio Code

Visual Studio Code is a lightweight and cross-platform code editor that supports many languages and features. To use Visual Studio Code for F# development, you need to install the Ionide extension, which provides F# language support, such as syntax highlighting, code completion, debugging, and more.

After installing the Ionide extension, you can start writing F# code in Visual Studio Code.

Creating Your First F# Project

Creating a new F# project

Now that you have installed .NET 6 and F#, you are ready to create your first F# project. There are two ways to create a new F# project: using the command-line interface or using Visual Studio Code.

Command-line interface

The command-line interface is a tool that lets you create, build, and run .NET applications using commands. To create a new F# project using the command-line interface, you need to use the dotnet new command, which creates a new project based on a template.

There are many templates available for different types of .NET applications, such as console, web, library, and test. You can see the list of templates by running the following command:

1
dotnet new list

For this blog post, I will use the console template, which creates a simple console application. To create a new F# console project, you can run the following command:

1
dotnet new console -lang F#

This will create a new folder with the name of the project and the files needed to run the application. You can also specify the name and the location of the project by using the -n and -o options, respectively. For example, to create a new F# console project named “HelloWorld” in the current directory, you can run the following command:

1
dotnet new console -lang F# -n HelloWorld -o .

Visual Studio Code

Visual Studio Code is a code editor that lets you create, edit, and run .NET applications using a graphical user interface. To create a new F# project using Visual Studio Code, you need to use the Ionide extension, which provides a project creation wizard.

To use the project creation wizard, you can open Visual Studio Code and go to the Command Palette. Then, type “F#: New Project” and press Enter. This will open a dialog box that asks you to select a template, a name, and a location for the project.

For this blog post, I will use the console template, which creates a simple console application. To create a new F# console project, you can select the console template, enter a name for the project, such as “HelloWorld”, and choose a location for the project, such as the current directory.

This will create a new folder with the name of the project and the files needed to run the application.

Understanding the project structure

After creating a new F# project, you can explore the project structure and the files that are generated by the template. The project structure may vary depending on the template and the tool that you used, but in general, it consists of the following files and folders:

  • Program.fs: This is the main file of the project, where you write your F# code. It contains a simple Hello, World! example that prints a message to the console.
  • HelloWorld.fsproj: This is the project file of the project, where you define the properties and the dependencies of the project. It is an XML file that specifies the target framework, the output type, the language version, and the references to other projects and packages.
  • obj: This is a folder that contains intermediate files that are generated during the build process, such as the assembly information and the debug symbols. You should not modify or delete these files, as they are managed by the .NET tools.
  • bin: This is a folder that contains the output files that are generated after building the project, such as the executable and the dependencies. You can run these files to execute your application.

Writing your first F# code

Now that you have created and understood your first F# project, you can start writing your first F# code. To do that, you can open the Program.fs file and edit the code inside.

The Program.fs file contains a simple Hello, World! example that prints a message to the console. The code looks like this:

1
2
3
4
5
6
7
8
// Learn more about F# at https://docs.microsoft.com/en-us/dotnet/fsharp/

open System

[<EntryPoint>]
let main argv =
    printfn "Hello, World!"
    0 // return an integer exit code

Let’s break down the code and explain what each part does:

  • The first line is a comment, which is a piece of text that is ignored by the compiler. You can use comments to document your code or to temporarily disable some code. In F#, you can write comments using // for single-line comments or (* and *) for multi-line comments.
  • The second line is an open statement, which imports a namespace from another module or assembly. In this case, we are importing the System namespace, which contains many useful types and functions for working with the .NET platform. You can use open statements to access the members of a namespace without having to write the full name every time.
  • The third line is an attribute, which is a piece of metadata that can be attached to a declaration or an expression. In this case, we are using the [<EntryPoint>] attribute, which marks the function that will be executed when the program starts. You can use attributes to provide additional information or behavior to your code, such as compiler directives, runtime annotations, or custom metadata.
  • The fourth line is a function declaration, which defines a named function that can take some parameters and return a value. In this case, we are defining a function named main that takes an array of strings as a parameter and returns an integer as a value. The parameter name is argv and the return type is inferred by the compiler. You can use functions to encapsulate and reuse your logic and behavior in your code.
  • The fifth line is a function body, which contains the expressions that will be evaluated when the function is called. In this case, we are using the printfn function, which prints a formatted string to the standard output, followed by a newline. The string that we are printing is "Hello, World!", which is a literal value that represents a sequence of characters. You can use function bodies to specify the actions and results of your functions.
  • The sixth line is a return expression, which specifies the value that will be returned by the function. In this case, we are returning 0, which is a literal value that represents an integer number. The return expression must match the return type of the function, which is inferred by the compiler. You can use return expressions to indicate the outcome and the exit code of your functions.

This is a very simple example of F# code, but it demonstrates some of the basic features and syntax of the language. You can modify the code to change the message or to add more logic and functionality. You can also create more files and functions to organize your code into modules and namespaces.

Working with Data Types and Functions

Introduction to F# data types

F# has a rich and expressive type system that lets you define and manipulate various kinds of data in your code. Some of the most common and useful data types in F# are records and discriminated unions.

Records

Records are immutable data structures that store named fields of values. Records are useful for representing simple data entities, such as customers, products, or coordinates. You can define a record type using the type keyword, followed by the name of the type and the fields. For example, you can define a record type for a person as follows:

1
type Person = { Name: string; Age: int; Occupation: string }

This defines a record type named Person that has three fields: Name, Age, and Occupation. Each field has a type annotation that specifies the type of the value that can be stored in the field. In this case, the Name and Occupation fields are of type string, which represents a sequence of characters, and the Age field is of type int, which represents an integer number.

You can create a record value using the record type name and the field values in curly braces. For example, you can create a record value for a person named Alice as follows:

1
let alice = { Name = "Alice"; Age = 25; Occupation = "Software Engineer" }

This creates a record value named alice that has the field values specified in the curly braces. You can access the field values of a record using the dot notation. For example, you can print the name of Alice as follows:

1
printfn "Alice's name is %s" alice.Name

This prints “Alice’s name is Alice” to the console. You can also update the field values of a record using the with keyword, which creates a new record value with the updated fields. For example, you can update the age of Alice as follows:

1
let aliceWithAge = { alice with Age = 26 }

This creates a new record value named alice that has the same field values as the previous alice value, except for the Age field, which is updated to 26. Note that records are immutable, which means you cannot modify the existing record value, but you can create a new record value with the same name.

Discriminated unions

Discriminated unions are algebraic data types that can represent one of a fixed number of cases. Discriminated unions are useful for modeling complex data scenarios, such as states, options, or errors. You can define a discriminated union type using the type keyword, followed by the name of the type and the cases. For example, you can define a discriminated union type for a shape as follows:

1
2
3
4
type Shape =
    | Circle of radius: float
    | Rectangle of width: float * height: float
    | Triangle of baseside: float * height: float

This defines a discriminated union type named Shape that has three cases: Circle, Rectangle, and Triangle. Each case has a name and an optional payload that specifies the data associated with the case. In this case, the Circle case has a payload of type float, which represents a floating-point number, and the Rectangle and Triangle cases have payloads of type float * float, which represent a pair of floating-point numbers.

You can create a discriminated union value using the case name and the payload value in parentheses. For example, you can create a discriminated union value for a circle with radius 5 as follows:

1
let circle = Circle(5.0)

This creates a discriminated union value named circle that has the case Circle and the payload 5.0. You can also omit the parentheses if the case has no payload or a single payload. For example, you can create a discriminated union value for a rectangle with width 10 and height 20 as follows:

1
let rectangle = Rectangle(10.0, 20.0)

This creates a discriminated union value named rectangle that has the case Rectangle and the payload (10.0, 20.0). You can match the cases of a discriminated union using the match expression, which evaluates different expressions based on the case of the value. For example, you can calculate the area of a shape as follows:

1
2
3
4
5
let area shape =
    match shape with
    | Circle radius -> Math.PI * radius * radius
    | Rectangle (width, height) -> width * height
    | Triangle (baseside, height: float) -> 0.5 * baseside * height

This defines a function named area that takes a shape value as a parameter and returns a float value as a result. The function uses the match expression to match the shape value with different cases and calculate the area accordingly. For example, if you call the function with the circle value, it will return the area of the circle as follows:

1
let circleArea = area circle

This returns the value 78.53981633974483, which is the area of the circle with radius 5.

Defining and using functions

Functions are the building blocks of F# programs, as they allow you to define and reuse your logic and behavior in your code. Functions are values that can take some parameters and return a result. You can define a function using the let keyword, followed by the name of the function and the parameters. For example, you can define a function named add that takes two numbers as parameters and returns their sum as follows:

1
let add x y = x + y

This defines a function named add that takes two parameters named x and y and returns the result of adding them. The return type of the function is inferred by the compiler based on the types of the parameters and the expression. In this case, the return type is the same as the parameter types, which can be any numeric type, such as int, float, or decimal.

You can use a function by calling it with the arguments that match the parameters. For example, you can use the add function to add two integers as follows:

1
let sum = add 3 4

This calls the add function with the arguments 3 and 4 and assigns the result to a value named sum. The result is 7, which is the sum of the two integers.

Immutable functions

Functions in F# are immutable, which means they cannot be modified or reassigned after they are defined. This makes functions more predictable and reliable, as they always produce the same result for the same input. Immutable functions also enable functional programming, which is a programming paradigm that emphasizes the use of pure functions, immutable data, and higher-order functions.

A pure function is a function that has no side effects, such as modifying global variables, printing to the console, or reading from the file system. A pure function only depends on its input and output, and it always produces the same output for the same input. Pure functions are easier to test, debug, and reason about, as they do not depend on or affect the external state of the program.

An example of a pure function is the area function that we defined earlier, which calculates the area of a shape based on its type and dimensions. The area function has no side effects, and it always produces the same output for the same input. For example, if you call the area function with the same circle value, it will always return the same area value, regardless of the context or the order of the calls.

1
2
let circleArea1 = area circle // returns 78.53981633974483
let circleArea2 = area circle // returns 78.53981633974483

An example of a function that is not pure is the printArea function, which prints the area of a shape to the console. The printArea function has a side effect, which is printing to the console, and it does not produce a meaningful output. The printArea function also depends on the external state of the program, such as the console output buffer, and it may affect the behavior of other functions that use the console.

1
2
3
4
5
let printArea shape =
    printfn "The area of the shape is %f" (area shape)

printArea circle // prints "The area of the shape is 78.53981633974483" to the console
printArea circle // prints "The area of the shape is 78.53981633974483" to the console

Higher-order functions

Higher-order functions are functions that can take other functions as parameters or return other functions as results. Higher-order functions are powerful and expressive, as they allow you to abstract and compose your logic and behavior in your code. Higher-order functions also enable functional programming, as they allow you to create and use higher-level concepts and patterns, such as map, filter, reduce, and curry.

An example of a higher-order function is the map function, which takes a function and a list as parameters and returns a new list with the function applied to each element of the original list. The map function is useful for transforming a list of values into a list of different values, such as converting a list of numbers into a list of strings, such as converting a list of temperatures from Celsius to Fahrenheit. You can define the map function as follows:

1
2
3
4
5
6
let map f list =
    let rec loop acc list =
        match list with
        | [] -> List.rev acc // return the reversed accumulator
        | x :: xs -> loop (f x :: acc) xs // apply f to x and prepend it to the accumulator, then loop on the rest of the list
    loop [] list // start with an empty accumulator and the original list

This defines a function named map that takes two parameters named f and list. The f parameter is a function that takes a value and returns a value, and the list parameter is a list of values. The map function uses a helper function named loop that takes two parameters named acc and list. The acc parameter is a list that accumulates the results of applying f to each element of the list parameter. The loop function uses the match expression to match the list parameter with different cases and recursively apply f to each element of the list. The map function returns the result of calling the loop function with an empty list as the acc parameter and the original list as the list parameter.

You can use the map function by calling it with a function and a list as arguments. For example, you can use the map function to convert a list of temperatures from Celsius to Fahrenheit as follows:

1
2
3
let celsiusToFahrenheit c = c * 1.8 + 32.0 // define a function that converts Celsius to Fahrenheit
let temperatures = [0.0; 10.0; 20.0; 30.0] // define a list of temperatures in Celsius
let converted = map celsiusToFahrenheit temperatures // use the map function to convert the temperatures to Fahrenheit

This calls the map function with the celsiusToFahrenheit function and the temperatures list as arguments and assigns the result to a value named converted. The result is a list of temperatures in Fahrenheit, which is [32.0; 50.0; 68.0; 86.0].

Conclusion

In this blog post, We have learned to get started with F# in the context of .NET 6. You have also learned how to install .NET 6 and F#, how to create your first F# project, how to work with F# data types and functions, and how to use pattern matching. By now, you should have a basic understanding of F# and its capabilities.

I hope you enjoyed this blog post and found it useful and informative.

Thank you for reading this blog post and happy coding with F# and .NET 6! 😊

This post is licensed under CC BY 4.0 by the author.