F# Training
F# Training
F# Training
  • Presentation
  • Fundamentals
    • Introduction
    • Syntax
      • Bases
      • Functions
      • Rules
      • Exceptions
    • First concepts
    • šŸ”Quiz
  • Functions
    • Signature
    • Concepts
    • Syntax
    • Standard functions
    • Operators
    • Addendum
    • šŸ”Quiz
    • šŸ“œSummary
  • Types
    • Overview
    • Tuples
    • Records
    • Unions
    • Enums
    • Anonymous records
    • Value types
    • šŸ“œRecap
    • Addendum
  • Monadic types
    • Intro
    • Option type
    • Result type
    • Smart constructor
    • šŸš€Computation expression (CE)
    • šŸš€CE theoretical basements
    • šŸ“œRecap
  • Pattern matching
    • Patterns
    • Match expression
    • Active patterns
    • šŸš€Fold function
    • šŸ“œRecap
    • šŸ•¹ļøExercises
  • Collections
    • Overview
    • Types
    • Common functions
    • Dedicated functions
    • šŸ”Quiz
    • šŸ“œRecap
  • Asynchronous programming
    • Asynchronous workflow
    • Interop with .NET TPL
    • šŸ“œRecap
  • Module and Namespace
    • Overview
    • Namespace
    • Module
    • šŸ”Quiz
    • šŸ“œRecap
  • Object-oriented
    • Introduction
    • Members
    • Type extensions
    • Class, Struct
    • Interface
    • Object expression
    • Recommendations
Powered by GitBook
On this page
  • No object orientation where F♯ is good
  • Simple object hierarchy
  • Structural equality
  • Object-oriented recommended use-cases
  • Class to encapsulate mutable state
  • Interface grouping features
  • User-friendly API
  • F♯ API consumed in C♯
  • Dependency management
  • FP based technique
  • OO technique
  • Advanced FP techniques
  • Higher-order function limits
  • 1. Lambda arguments are not explicit
  • 2. Lambda is a command 'T -> unit
  • 3. Lambda: "really" generic!?

Was this helpful?

Edit on GitHub
  1. Object-oriented

Recommendations

Recommendations for object-oriented programming

PreviousObject expression

Last updated 1 month ago

Was this helpful?

No object orientation where F♯ is good

Inference works better with function (object) than object.Member

Simple object hierarchy

āŒ Avoid inheritance

āœ… Prefer type Union and exhaustive pattern matching

Recursive types

This is particularly true for recursive types. You can define a fold function for them.

šŸ”— , F# for fun and profit

Structural equality

āŒ Avoid class (reference equality by default)

āœ… Prefer a Record or a Union

šŸ‘Œ Alternatively, consider Struct for performance purposes

ā“ Consider custom structural equality for performance purposes šŸ”— , Compositional IT

Object-oriented recommended use-cases

  1. Encapsulate mutable state → in a class

  2. Group features → in an interface

  3. Expressive, user-friendly API → tuplified methods

  4. API F♯ consumed in C♯ → member extensions

  5. Dependency management → injection into the constructor

  6. Tackle higher-order functions limits

Class to encapsulate mutable state

// šŸ˜• Encapsulate mutable state in a closure → impure function → counter-intuitive āš ļø
let counter =
    let mutable count = 0
    fun () ->
        count <- count + 1
        count

let x = counter ()  // 1
let y = counter ()  // 2

// āœ… Encapsulate mutable state in a class
type Counter() =
    let mutable count = 0 // Private field
    member _.Next() =
        count <- count + 1
        count

Interface grouping features

let checkRoundTrip serialize deserialize value =
    value = (value |> serialize |> deserialize)
// val checkRoundTrip :
//   serialize:('a -> 'b) -> deserialize:('b -> 'a) -> value:'a -> bool
//     when 'a : equality

serialize and deserialize form a consistent group → Grouping them in an object makes sense

let checkRoundTrip serializer data =
    data = (data |> serializer.Serialize |> serializer.Deserialize)

šŸ’” Prefer an interface to a Record (not possible with Fable.Remoting)

// āŒ Avoid: not a good use of a Record: unnamed parameters, structural comparison lost...
type Serializer<'T> = {
    Serialize: 'T -> string
    Deserialize: string -> 'T
}

// āœ… Recommended
type Serializer =
    abstract Serialize<'T> : value: 'T -> string
    abstract Deserialize<'T> : data: string -> 'T
  • Parameters are named in the methods

  • Object easily instantiated with an object expression

User-friendly API

// āœ–ļø Avoid                         // āœ… Favor
module Utilities =                  type Utilities =
    let add2 x y = x + y                static member Add(x,y) = x + y
    let add3 x y z = x + y + z          static member Add(x,y,z) = x + y + z
    let log x = ...                     static member Log(x, ?retryPolicy) = ...
    let log' x retryPolicy = ...

Advantages of OO implementation:

  • Add method overloaded vs add2, add3 functions (2 and 3 = args count)

  • Single Log method with retryPolicy optional parameter

F♯ API consumed in C♯

Do not expose this type as is:

type RadialPoint = { Angle: float; Radius: float }

module RadialPoint =
    let origin = { Angle = 0.0; Radius = 0.0 }
    let stretch factor point = { point with Radius = point.Radius * factor }
    let angle (i: int) (n: int) = (float i) * 2.0 * System.Math.PI / (float n)
    let circle radius count =
        [ for i in 0..count-1 -> { Angle = angle i count; Radius = radius } ]

šŸ’” To make it easier to discover the type and use its features in C♯

  • Put everything in a namespace

  • Augment type with the functionalities implemented in the companion module

namespace Fabrikam

type RadialPoint = {...}
module RadialPoint = ...

type RadialPoint with
    static member Origin = RadialPoint.origin
    static member Circle(radius, count) = RadialPoint.circle radius count |> List.toSeq
    member this.Stretch(factor) = RadialPoint.stretch factor this

šŸ‘‰ The API consumed in C♯ is ~equivalent to:

namespace Fabrikam
{
    public static class RadialPointModule { ... }

    public sealed record RadialPoint(double Angle, double Radius)
    {
        public static RadialPoint Origin => RadialPointModule.origin;

        public static IEnumerable<RadialPoint> Circle(double radius, int count) =>
            RadialPointModule.circle(radius, count);

        public RadialPoint Stretch(double factor) =>
            new RadialPoint(Angle@, Radius@ * factor);
    }
}

Dependency management

FP based technique

Parametrization of dependencies + partial application

  • Small-dose approach: few dependencies, few functions involved

  • Otherwise, quickly tedious to implement and to use

module MyApi =
    let function1 dep1 dep2 dep3 arg1 = doStuffWith dep1 dep2 dep3 arg1
    let function2 dep1 dep2 dep3 arg2 = doStuffWith' dep1 dep2 dep3 arg2

OO technique

Dependency injection

  • Inject dependencies into the class constructor

  • Use these dependencies in methods

šŸ‘‰ Offers a user-friendly API šŸ‘

type MyParametricApi(dep1, dep2, dep3) =
    member _.Function1 arg1 = doStuffWith dep1 dep2 dep3 arg1
    member _.Function2 arg2 = doStuffWith' dep1 dep2 dep3 arg2

āœ… Particularly recommended for encapsulating side-effects : → Connecting to a DB, reading settings...

Trap

Dependencies injected in the constructor make sense only if they are used throughout the class.

A dependency used in a single method indicates a design smell.

Advanced FP techniques

Dependency rejection = sandwich pattern

  • Reject dependencies in Application layer, out of Domain layer

  • Powerful and simple šŸ‘

  • ... when suitable ā—

Free monad + interpreter patter

  • Pure domain

  • More complex than the sandwich pattern but working in any case

  • User-friendly through a dedicated computation expression

Reader monad

  • Only if hidden inside a computation expression

...

Higher-order function limits

ā˜ļø It's better to pass an object than a lambda as a parameter to a higher-order function when:

1. Lambda arguments are not explicit

āŒ let test (f: float -> float -> string) =...

āœ… Solution 1: type wrapping the 2 args float → f: Point -> string with type Point = { X: float; Y: float }

āœ… Solution 2: interface + method for named parameters → type IPointFormatter = abstract Execute : x:float -> y:float -> string

2. Lambda is a command 'T -> unit

āœ… Prefer to trigger an side-effect via an object → type ICommand = abstract Execute : 'T -> unit

3. Lambda: "really" generic!?

let test42 (f: 'T -> 'U) =
    f 42 = f "42"
// āŒ ^^     ~~~~
// ^^ Warning FS0064: This construct causes code to be less generic than indicated by the type annotations.
//                    The type variable 'T has been constrained to be type 'int'.
// ~~ Error FS0001: This expression was expected to have type 'int' but here has type 'string'
// šŸ‘‰ `f: int -> 'U'` expected

āœ… Solution: wrap the function in an object

type Func2<'U> =
    abstract Invoke<'T> : 'T -> 'U

let test42 (f: Func2<'U>) =
    f.Invoke 42 = f.Invoke "42"

šŸ”—

šŸ”— , F# for Fun and Profit, Dec 2020

The "Recursive types and folds" series
Custom Equality and Comparison in F#
F♯ component design guidelines - Libraries used in C♯
Six approaches to dependency injection