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
  • Definition
  • Standard operators
  • Piping operators
  • Pipe right |>
  • Pipe left <|
  • Pipe right 2 ||>
  • Compose >>
  • Reversed Compose <<
  • Pipe |> or Compose >> ?
  • Point-free style
  • Pros/Cons āš–ļø
  • Limit šŸ›‘
  • Custom operators
  • Operator overload
  • Creation of a new operator
  • Symbols allowed in an operator
  • Usage symbols
  • Operator or function?
  • Infix operator vs function
  • Using an operator as a function

Was this helpful?

Edit on GitHub
  1. Functions

Operators

Definition

Function whose name is a set of symbols

  • Unary operator: let (~symbols) = ...

  • Binary operator: let (symbols) = ...

  • Symbols = combination of % & * + - . / < = > ? @ ^ | ! $

2 ways of using operators:

  1. As an operator: 1 + 2

    • without the ()

    • prefix position if unary -1.

    • infix position id binary 1 + 2

  2. As a function: (+) 1 2

    • with the ()

Standard operators

Also defined in FSharp.Core.

  • Arithmetic operators: +, -...

  • Pipeline operators

  • Composition operators

Piping operators

Binary operators, placed between a simple value and a function

  • Apply the value to the function = Pass value as argument

  • Avoid parentheses otherwise required for precedence reason

  • There are several pipes

    • Pipe right |> : the usual pipe

    • Pipe left <| a.k.a. reversed pipe

    • Pipe right 2 ||>

    • Etc.

Pipe right |>

Reverse the order between function and value: val |> fn ≔ fn val

  • Natural "subject-verb" order, as a method call of an object (obj.M(x))

  • Pipeline: chain function calls, without intermediate variable

  • Help the type inference - example:

let items = ["a"; "bb"; "ccc"]

let longestKo = List.maxBy (fun x -> x.Length) items  // āŒ Error FS0072
//                                   ~~~~~~~~

let longest = items |> List.maxBy (fun x -> x.Length) // āœ… Return "ccc"

Pipe left <|

fn <| expression ≔ fn (expression)

  • ā˜ Usage a little less common than |>

  • āœ… Minor advantage: avoids parentheses

  • āŒ Major disadvantage: reads from right to left → Reverses natural English reading direction and execution order

printf "%i" 1+2          // šŸ’„ Error
printf "%i" (1+2)        // With parentheses
printf "%i" <| 1+2       // With reversed pipe

Quid of such expression: x |> fn <| y ā“

Executed from left to right: (x |> fn) <| y ≔ (fn x) <| y ≔ fn x y

  • Goal: use fn as infix

  • Cons: difficult to read because of double reading direction ā—

šŸ‘‰ Tip: TO AVOID

Pipe right 2 ||>

(x, y) ||> fn ≔ fn x y

  • To pass 2 arguments at once, in the form of a tuple

  • Used infrequently, but useful with fold to pass the initial value (seed) and the list before defining the folder function and help the type inference for a lambda folder.

let items = [1..5]

// šŸ˜• While reading, we can miss the 0 at the end (the seed)
let sumOfEvens =
    items |> List.fold (fun acc x -> if x % 2 = 0 then acc + x else acc) 0

let sumOfEvens' =
    (0, items)
    ||> List.fold (fun acc x -> if x % 2 = 0 then acc + x else acc)

// šŸ’” Replace lambda with named function
let addIfEven acc x = if x % 2 = 0 then acc + x else acc
let sumOfEvens'' = items |> List.fold addIfEven 0

ā˜ļø This operator corresponds to the notion of "uncurrying", i.e. being able to call a curried f function by passing it these 2 parameters in the form of a tuple:

let uncurry f (a,b) = f a b

Compose >>

Binary operators, placed between two functions → The result of the 1st function is used as an argument for the 2nd function

f >> g ≔ fun x -> g (f x) ≔ fun x -> x |> f |> g

  • The out/in types must match: f: 'T -> 'U and g: 'U -> 'V → 'U āœ…

  • Signature of the final function: 'T -> 'V.

let add1 x = x + 1
let times2 x = x * 2

let add1Times2 x = times2 (add1 x) // Verbose
let add1Times2' = add1 >> times2   // Terse šŸ‘

Reversed Compose <<

Rarely used, except to restore a natural reading order.

Example with not (which replaces the ! in C♯):

let Even x = x % 2 = 0

// Pipeline
let Odd x = x |> Even |> not

// Compose
let Odd = not << Even

Pipe |> or Compose >> ?

Definition
Focus, Mindset

Compose

let h = f >> g

Functions

Pipe

let result = value |> f

Values

Point-free style

A.k.a Tacit programming

Writing functions without mentioning the parameters (referred here as "points"), just by using function composition (1) or partial application (2).

// Example 1: function composition
let add1 x = x + 1                // (x: int) -> int
let times2 x = x * 2              // (x: int) -> int
let add1Times2 = add1 >> times2   // int -> int

// Example 2: partial application
let isEven x = (x % 2 = 0)
let evens = List.filter isEven // int list -> int list

// Example 2': printfn partially applied
let greetLong name age = printfn $"My name is %s{name} and I am %i{age} years old!" // name:string -> age:int -> unit
let greetShort = printfn "My name is %s and I am %d years old!" // (string -> int -> unit)

Pros/Cons āš–ļø

āœ… Pros

  • Concise style

  • At function/operation level, by abstracting the parameters

āŒ Cons

Loses the name of the parameter now implicit in the signature → Unimportant if the function remains understandable:

  • When the parameters name is not significant (e.g. x)

  • When the combination of parameters type + function name is unambiguous

  • For private usage - Not recommended for a public API

Limit šŸ›‘

Works poorly with generic functions:

let isNotEmptyKo = not << List.isEmpty
// šŸ’„ Error FS0030: Value restriction: The value 'isNotEmptyKo' has an inferred generic function type

// Alternatives
let isNotEmpty<'a> = not << List.isEmpty<'a>    // šŸ‘Œ Explicit type annotation
let isNotEmpty' list = not (List.isEmpty list)  // šŸ‘Œ Explicit parameter

Custom operators

2 possibilities:

  • Operator overload

  • Creation of a new operator

Operator overload

Usually concerns a specific type → Overload defined within the associated type (as in C♯)

type Vector = { X: int; Y: int } with
    // Unary operator (see ~ and 1! param) for vector inversion
    static member (~-) (v: Vector) =
        { X = -v.X
          Y = -v.Y }

    // Binary addition operator for 2 vectors
    static member (+) (a: Vector, b: Vector) =
        { X = a.X + b.X
          Y = a.Y + b.Y }

let v1 = -{ X=1; Y=1 } // { X = -1; Y = -1 }
let v2 = { X=1; Y=1 } + { X=1; Y=3 } // { X = 2; Y = 4 }

Creation of a new operator

  • Definition rather in a module or associated type

  • Usual use-case: alias for existing function, used as infix

// "OR" Composition of 2 functions (fa, fb) which return an optional result
let (<||>) fa fb x =
    match fa x with
    | Some v -> Some v // Return value produced by (fa x) call
    | None   -> fb x   // Return value produced by (fb x) call

// Functions: int -> string option
let tryMatchPositiveEven x = if x > 0 && x % 2 = 0 then Some $"Even {x}" else None
let tryMatchPositiveOdd x = if x > 0 && x % 2 <> 0 then Some $"Odd {x}" else None
let tryMatch = tryMatchPositiveEven <||> tryMatchPositiveOdd

tryMatch 0;; // None
tryMatch 1;; // Some "Odd 1"
tryMatch 2;; // Some "Even 2"

Symbols allowed in an operator

Unary operator "tilde " → ~ followed by +, -, +., -., %, %%, &, &&

Unary operator "snake " → Several ~, e.g. ~~~~

Unary operator "bang " → ! followed by a combination of !, %, &, *, +, ., /, <, =, >, @, ^, |, ~, ? → Except != (!=) which is binary

Binary operator → Any combination of !, %, &, *, +, ., /, <, =, >, @, ^, |, ~, ? → which does not match a unary operator

Usage symbols

All operators are used as is ā— Except the unary operator "tilde": used without the initial ~.

Operator
Declaration
Usage

Unaire tilde

let (~&&) x = …

&&x

Unaire snake

let (~~~) x = …

~~~x

Unaire bang

let (!!!) x = …

!!!x

Binary

let (<ˆ>) x y = …

x <ˆ> y

ā˜ To define an operator beginning or ending with a *, you must put a space between ( and * as well as between * and ) to distinguish from a block of F♯ comments (* *). → let ( *+ ) x y = x * y + y āœ…

Operator or function?

Infix operator vs function

šŸ‘ Pros :

  • Respects the natural reading order (left → right)

  • avoids parentheses → 1 + 2 * 3 vs multiply (add 1 2) 3

    → 1 + 2 * 3 vs multiply (add 1 2) 3

āš ļø Cons :

  • A "folkloric" operator (e.g. @!) will be less comprehensible than a function whose name uses the domain language.

Using an operator as a function

šŸ’” You can use the partial application of a binary operator :

Examples:

  • Instead of a lambda: → (+) 1 ≔ fun x -> x + 1

  • To define a new function : → let isPositive = (<) 0 ≔ let isPositive x = 0 < x ≔ x >= 0 \

PreviousStandard functionsNextAddendum

Last updated 1 month ago

Was this helpful?

šŸ”—

F♯ coding conventions > Partial application and point-free programming