OOP in F#
Hello, Habre!
Object-oriented programming is an approach to development based on objects — instances of classes that combine data and behavior. In F#, which grew up on functional concepts, OOP is presented as an addition to existing functionality. F# allows you to use classes, inheritance and interfaces, and in such a way that OOP elements just perfectly fit into the functional context, without creating a feeling of foreignness.
Contents
Basics of OOP in F#
Classes and fields
Classes in F# – These are structures that allow you to combine fields and methods that determine the state and behavior of the object, respectively. Creating an F# class starts with the keyword type
Followed by the class name and its constructor:
type MyClass(param1 : int, param2 : string) =
// поля
let mutable field1 = param1
let field2 = param2
// свойство
member this.Property1 = field1
member this.Property2 with get() = field2 and set(value) = field1 <- value
// метод
member this.Method1() =
printfn "Method1 called with field1: %d and field2: %s" field1 field2
MyClass
with two fields: field1
and field2
they are initialized via constructor parameters There are also two properties Property1
and Property2
where Property1
is read-only, and Property2
— for reading and writing, demonstrating the use of F# properties to control access to class data. Method Method1
prints field values.
let
Bindings in a class are used to declare fields or functions that are only available inside the class.
do
bindings execute the initialization code when the class is instantiated.
type MyClass(x: int, y: int) =
let sum = x + y
do printfn "Сумма x и y равна %d" sum
member this.Sum = sum
The constructor initializes the fields and performs an additional action
You can use self identifiers to refer to the current instance of a class:
type MyClass(x: int, y: int) as self =
member self.Sum = x + y
member self.Multiply = x * y
as self
allows you to refer to the current instance of a class inside its methods.
Interfaces
Interfaces are defined using the keyword type
specifying the keyword interface
and enumerating methods and properties without implementing them. Each method or property of an interface is abstract, defining a form without a concrete implementation:
type IExampleInterface =
abstract member ExampleMethod : string -> string
abstract member ExampleProperty : int with get
IExampleInterface
defines an interface with one method ExampleMethod
which takes a string and returns a string, and a property ExampleProperty
read-only type int
.
The implementation of the interface is specified in the class body using the keyword interface
with the following keyword with
Followed by implementations of interface methods and properties:
type ExampleClass() =
interface IExampleInterface with
member this.ExampleMethod(input) = "Processed: " + input
member this.ExampleProperty with get() = 42
ExampleClass
implements IExampleInterface
providing specific implementations for ExampleMethod
and ExampleProperty
. This allows objects ExampleClass
be used where expected IExampleInterface
.
Inheritance and abstract classes
Inheritance allows you to create a new class based on an existing one, inheriting its properties and methods:
try
let result = 10 / 0
with
| :? System.DivideByZeroException -> printfn "Деление на ноль."
| ex -> printfn "Произошло исключение: %s" (ex.Message)
DerivedClass
inherits BaseClass
adding a new parameter param2
and saving the parameter param1
from the base class
Class Annotation in F# is a class that can be instantiated by itself and is the base of other classes. Abstract classes can contain abstract methods (without implementation) and methods with implementation:
[<AbstractClass>]
type Shape(x0: float, y0: float) =
let mutable x, y = x0, y0
abstract member Area: float
abstract member Perimeter: float
member this.Move(dx: float, dy: float) =
x <- x + dx
y <- y + dy
Shape
defines the general characteristics of shapes, such as area and perimeter, but does not provide their concrete implementations, making the class abstract.
An example of inheritance from an abstract class:
type Circle(x: float, y: float, radius: float) =
inherit Shape(x, y)
override this.Area =
Math.PI * radius * radius
override this.Perimeter =
2.0 * Math.PI * radius
Circle
inherits from the abstract class Shape
providing specific implementations for Area
and Perimeter
.
Exception and error management
Bloc try...with
used to catch and handle exceptions. The code is inside the block try
is executed, and if an exception occurs during its execution, execution is transferred to the block with
where the exception can be handled:
try
let result = 10 / 0
with
| :? System.DivideByZeroException -> printfn "Деление на ноль."
| ex -> printfn "Произошло исключение: %s" (ex.Message)
The exception is caught in the block with
and the program displays a corresponding message instead of crashing
Bloc try...finally
is used to guarantee that certain code is executed after a block try
regardless of whether an exception is thrown or not:
try
let result = 10 / 2
finally
// код в этом блоке выполнится в любом случае
printfn "Этот код выполнится независимо от исключений."
You can create custom exception types to handle specific error situations. This is done by inheriting from the class System.Exception
or any other built-in exception:
type MyCustomException(message: string) =
inherit System.Exception(message)
MyCustomException
is a user exception that takes an error message as a parameter and passes it to the base class constructor System.Exception
:
let riskyOperation x =
if x < 0 then
raise (MyCustomException("Число не должно быть отрицательным"))
else
printfn "%d - это положительное число" x
riskyOperation
generates MyCustomException
if a negative number is passed to it, which allows you to specify exactly the type of error and make it easier to handle.
You can also connect logging in errors using Serilog. Add the Serilog dependency to the project via NuGet:
open Serilog
Log.Logger <- LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.WriteTo.File("logs/myapp.txt", rollingInterval = RollingInterval.Day)
.CreateLogger()
Log.Information("Запуск приложения")
try
let result = 10 / 0
with
| ex ->
Log.Error(ex, "Произошло исключение при выполнении операции")
In the block try...with
the exception is caught and logged using Serilog, providing detailed information about the error.
Optimization
Encapsulation is an OOP principle that consists in limiting direct access to some components of an object and controlling access to this data through methods:
type BankAccount() =
let mutable balance = 0.0
member this.Deposit(amount: float) =
if amount > 0.0 then
balance <- balance + amount
else
raise (ArgumentException("Сумма должна быть больше нуля"))
member this.Withdraw(amount: float) =
if amount <= balance && amount > 0.0 then
balance <- balance - amount
else
raise (InvalidOperationException("Недостаточно средств или сумма меньше нуля"))
member this.GetBalance() = balance
Here we use encapsulation to control bank account balance changes through methods Deposit
and Withdraw
preventing direct access to the field balance
inheritance allows you to create a new class based on an existing one, reusing its code and extending its functionality:
type Vehicle() =
abstract member Move: unit -> string
type Car() =
inherit Vehicle()
override this.Move() = "Едет на четырех колесах"
type Bicycle() =
inherit Vehicle()
override this.Move() = "Едет на двух колесах"
Car
and Bicycle
inherit from Vehicle
and implement an abstract method Move
Polymorphism is used to create interfaces and abstract classes that can be implemented by many different classes:
type IDrawable =
abstract member Draw: unit -> unit
type Circle() =
interface IDrawable with
member this.Draw() = printfn "Рисуется круг"
type Square() =
interface IDrawable with
member this.Draw() = printfn "Рисуется квадрат"
let drawShapes shapes =
shapes |> List.iter (fun shape -> shape.Draw())
Circle
and Square
implement the interface IDrawable
and so you can handle them in the same way in the function drawShapes
.
OOP allows you to create structured and easily maintainable systems. And you can learn more about OOP and NAP in general in the framework expert programming courses from my friends at OTUS.