OOP in F#

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.

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 typeFollowed 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 field2they are initialized via constructor parameters There are also two properties Property1 and Property2where 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 ExampleMethodwhich 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 withFollowed 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 IExampleInterfaceproviding 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 BaseClassadding 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 Shapeproviding 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 withwhere 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 withand the program displays a corresponding message instead of crashing

Bloc try...finally is used to guarantee that certain code is executed after a block tryregardless 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 MyCustomExceptionif 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 Withdrawpreventing 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 IDrawableand 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.

Related posts