Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

F#  Module 3 – Functions and functional style

Currying, partial application and pipelines

See why every F# function takes one argument, specialise functions by partial application, chain them with |> and >>, and use the _.Name shorthand.

  • Beginner
  • 10 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain currying and use partial application to specialise a general function
  • Compose functions with |>, >> and <|, and order parameters so that pipelines read well
  • Choose between curried and tupled parameters, especially at .NET boundaries
  • Use the F# 8 _.Property shorthand in pipelines, with its data first

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain currying and use partial application to specialise a general function
  • Compose functions with |>, >> and <|, and order parameters so that pipelines read well
  • Choose between curried and tupled parameters, especially at .NET boundaries
  • Use the F# 8 _.Property shorthand in pipelines, with its data first

Before you start

On this page

add 1 is not a mistake in F#. Given one argument instead of two, add hands back a new function, which F# Interactive prints as val addOne: (int -> int), and addOne 41 is 42. That one rule, that every function takes a single argument, is why F# code configures behaviour without classes and chains steps with |>.

You will see how curried function: A function that takes its arguments one at a time, written with spaces, such as area 3 4. Its type, int -> int -> int, has one arrow for each argument. More in “From C# to F#: a side-by-side syntax map” take their arguments, specialise a pricing rule by partial application, read and write pipelines with |>, >> and <|, decide between curried and tupled parameters where .NET code is involved, and use the F# 8 shorthand _.Name without tripping over inference. Six sessions recorded on .NET 10 show each step.

Every F# function takes one argument

A function of two parameters looks like one that takes two arguments at once. Give it only the first and see what comes back:

add given one argument, then the same function as nested lambdas Python · currying_session.py
from session import fsi

# add given one argument instead of two, then the same function written as two nested lambdas.
fsi([
    "let add x y = x + y;;",
    "let addOne = add 1;;",
    "addOne 41;;",
    "(add 1) 41;;",
    "let addNested = fun x -> fun y -> x + y;;",
    "addNested 1 41;;",
    "#quit;;",
], args=["--nologo"])

Output

$ dotnet fsi --nologo
> let add x y = x + y;;
val add: x: int -> y: int -> int

> let addOne = add 1;;
val addOne: (int -> int)

> addOne 41;;
val it: int = 42

> (add 1) 41;;
val it: int = 42

> let addNested = fun x -> fun y -> x + y;;
val addNested: x: int -> y: int -> int

> addNested 1 41;;
val it: int = 42

> #quit;;

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 currying_session.py

add 1 returned a function. The parentheses in val addOne: (int -> int) mark a function value whose parameters have no names, and applying it to 41 gave 42, as (add 1) 41 did. The Functions reference names this: supplying fewer arguments than a function takes creates a new function that expects the rest, which is currying. addNested, written as one lambda that returns another, printed the same signature as add: in F#, a function of several parameters is a chain of one-argument functions. Step through add 1 41 one argument at a time:

Interactive · stage by stage add 1 41, one argument at a time Step through the call. Each argument goes in on its own, and the first one gives back a function that waits for the second.
add 1 41, one argument at a time, step 5 of 5. Showing add (same as fun x -> fun y -> …), add 1 (x is 1, y still to come), (add 1) 41, 42 (val it: int = 42). In focus: add.addsame as fun x -> fun y -> …add 1x is 1, y still to come(add 1) 41the second argument42val it: int = 42

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. A function of one argument. add has the type int -> int -> int, which F# reads as int -> (int -> int): give it an int and you get back a function that waits for the next int.
  2. The first argument. add 1 supplies x and stops there. The result is neither an error nor a number but a function, which F# Interactive printed as val addOne: (int -> int).
  3. The second argument. Applying that function to 41 supplies y. (add 1) 41 is exactly what add 1 41 means, because function application groups from the left.
  4. The body runs at the end. The body runs only once every argument has arrived. 1 + 41 is 42, the same answer as addOne 41.
  5. The same function, written out. addNested, written fun x -> fun y -> x + y, printed the same signature as add, x: int -> y: int -> int. A function of two arguments is a function that returns a function.

So the arrows of a signature group from the right, int -> (int -> int), while function application groups from the left: add 1 41 is (add 1) 41.

Key idea

An F# function takes one argument and returns its result, which may be another function waiting for the next argument: add 1 is a complete value of type int -> int.

Checkpoint

Check: Currying, one argument at a time

  1. Question 1 of 2 What did F# Interactive print for let addOne = add 1, where add x y = x + y?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: val addOne: (int -> int)

    A curried function given its first argument returns a function of the rest. The parentheses mark a function value without parameter names.

  2. Question 2 of 2 addNested was written as fun x -> fun y -> x + y. Why did it print the same signature as add?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: let add x y = … is a chain of one-argument functions too, so both take x and return a function that takes y

    Writing a function with several parameters is shorthand for nested one-parameter lambdas.

Partial application fixes the settings and leaves the data

Currying pays off when one argument is a setting and the other is the data. These two pricing rules take their setting first and the price last, so fixing the setting leaves a function of the price:

prices.fsx: two general rules and three specialised ones FSX · prices.fsx
// Two general rules, each with its setting first and the price last.
let addTax (rate: decimal) (price: decimal) = price + price * rate
let discount (percent: decimal) (price: decimal) = price - price * percent / 100m

// Partial application fixes a setting and leaves a function of the price alone.
let withTax = addTax 0.18m
let saleDiscount = discount 10m
let salePrice = saleDiscount >> withTax

let products = [ "Kettle", 1200m; "Lamp", 850m; "Mug", 250m ]

for name, price in products do
    printfn "%-6s list %8.2f  tax %8.2f  discount %8.2f  sale %8.2f" name price (withTax price) (saleDiscount price) (salePrice price)
#load runs prices.fsx and prints what it defined Python · prices_session.py
from session import fsi

# #load runs prices.fsx, then prints the signature of each value it defined.
fsi([
    '#load "prices.fsx";;',
    "#quit;;",
], args=["--nologo"])

Output

$ dotnet fsi --nologo
> #load "prices.fsx";;
[Loading prices.fsx]
Kettle list  1200.00  tax  1416.00  discount  1080.00  sale  1274.40
Lamp   list   850.00  tax  1003.00  discount   765.00  sale   902.70
Mug    list   250.00  tax   295.00  discount   225.00  sale   265.50
module FSI_0002.Prices
val addTax: rate: decimal -> price: decimal -> decimal
val discount: percent: decimal -> price: decimal -> decimal
val withTax: (decimal -> decimal)
val saleDiscount: (decimal -> decimal)
val salePrice: (decimal -> decimal)
val products: (string * decimal) list

> #quit;;

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 prices_session.py

withTax is addTax with its rate fixed at 0.18, and saleDiscount is discount with 10 %. Neither needed a class, a constructor or a field to hold its setting: partial application: Calling a curried function with fewer arguments than it takes, which returns a new function of the remaining ones: addTax 0.18m returns a function that takes the price. keeps the setting inside the new function. salePrice joins two of them, which the next section explains. The specialised functions, as #load printed them:

Name Defined as Signature Kettle, list 1,200.00
addTax the general rule rate: decimal -> price: decimal -> decimal needs both
withTax addTax 0.18m (decimal -> decimal) 1,416.00
saleDiscount discount 10m (decimal -> decimal) 1,080.00
salePrice saleDiscount >> withTax (decimal -> decimal) 1,274.40

Parameter order decides whether this works. A setting that callers fix goes first and the data that changes on every call goes last, which is the order of the List module’s own functions: List.map f xs takes the function first, so List.map withTax is itself a function of a list.

Putting the data first

Wrong: let addTax (price: decimal) (rate: decimal) = …. Then addTax 0.18m fixes the price, not the rate, and every call site needs a lambda to fix the rate. Right: settings first, data last, as addTax rate price has it.

Checkpoint

Check: Partial application and parameter order

  1. Question 1 of 2 withTax is addTax 0.18m. What did prices.fsx print for the Kettle with tax, from a list price of 1,200.00?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 1416.00

    Partial application stored 0.18 inside withTax, so withTax price is addTax 0.18m price.

  2. Question 2 of 2 You define let applyDiscount (price: decimal) (percent: decimal) = …. What goes wrong when a pipeline needs the same 10 % discount for every price?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: applyDiscount 10m fixes the price, not the percent, so every use needs a lambda such as fun p -> applyDiscount p 10m

    Settings first, data last: that order lets callers fix the settings once and reuse the function, as List.map f xs does.

Pipelines and composition read from left to right

The same three text steps can be written as nested calls, as a pipeline: A chain of functions joined by the pipe operator |>, which passes the value on its left to the function on its right, so x |> f |> g means g (f x). More in “From C# to F#: a side-by-side syntax map” and as a composed function. All three printed the same text:

clean.fsx: one cleaning, three ways FSX · clean.fsx
open System

let trim (s: string) = s.Trim()
let lower (s: string) = s.ToLowerInvariant()
let squeeze (s: string) = String.Join(" ", s.Split(' ', StringSplitOptions.RemoveEmptyEntries))

let raw = "   Order  READY for   Pickup  "

// The same three steps, three ways.
let nested = squeeze (lower (trim raw))
let piped = raw |> trim |> lower |> squeeze
let tidy = trim >> lower >> squeeze

printfn "nested:   [%s]" nested
printfn "pipeline: [%s]" piped
printfn "composed: [%s]" (tidy raw)
printfn "each step: [%s] [%s] [%s]" (trim raw) (raw |> trim |> lower) piped
clean.fsx, then ||> and <| at the prompt, and a <| chain that fails Python · pipes_session.py
from session import fsi, run

# clean.fsx, then ||> and <| at the prompt, and a chain of <| that fails until parentheses group it.
run("""
dotnet fsi clean.fsx
""")
fsi([
    "let add x y = x + y;;",
    "(3, 4) ||> add;;",
    'let shout (s: string) = s.ToUpper() + "!";;',
    'printfn "%s" <| shout "ready";;',
    'let trim (s: string) = s.Trim()\nlet lower (s: string) = s.ToLowerInvariant();;',
    'shout <| lower <| trim "  Pickup ";;',
    'shout <| (lower <| trim "  Pickup ");;',
    "#quit;;",
], args=["--nologo"])

Output

$ dotnet fsi clean.fsx
nested:   [order ready for pickup]
pipeline: [order ready for pickup]
composed: [order ready for pickup]
each step: [Order  READY for   Pickup] [order  ready for   pickup] [order ready for pickup]
$ dotnet fsi --nologo
> let add x y = x + y;;
val add: x: int -> y: int -> int

> (3, 4) ||> add;;
val it: int = 7

> let shout (s: string) = s.ToUpper() + "!";;
val shout: s: string -> string

> printfn "%s" <| shout "ready";;
READY!
val it: unit = ()

> let trim (s: string) = s.Trim()
- let lower (s: string) = s.ToLowerInvariant();;
val trim: s: string -> string
val lower: s: string -> string

> shout <| lower <| trim "  Pickup ";;

  shout <| lower <| trim "  Pickup ";;
  ^^^^^

stdin(7,1): error FS0001: Type mismatch. Expecting a
    'string -> 'a -> 'b'
but given a
    'string -> string'
The type ''a -> 'b' does not match the type 'string'

> shout <| (lower <| trim "  Pickup ");;
val it: string = "PICKUP!"

> #quit;;

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pipes_session.py

squeeze (lower (trim raw)) must be read from the inside out. raw |> trim |> lower |> squeeze reads in the order the steps run, because x |> f means f x: the Functions reference shows that |> is defined in FSharp.Core as let (|>) x f = f x. trim >> lower >> squeeze takes no data at all. It is function composition: Joining two functions into a new one with >> or <<: f >> g is the function fun x -> g (f x), which runs f and passes its result to g., a new function, tidy, that runs the three steps when it gets a string. Step through the pipeline with the values the last line printed:

Interactive · stage by stage One string through raw |> trim |> lower |> squeeze Step through the pipeline. Each |> hands the value on its left to the function on its right, and the brackets show the value so far.
One string through raw |> trim |> lower |> squeeze, step 5 of 5. Showing raw, |> trim ([Order READY for Pickup]), |> lower ([order ready for pickup]), |> squeeze ([order ready for pickup]), trim >> lower >> squeeze. In focus: trim >> lower >> squeeze.rawspaces around and inside|> trim[Order READY for Pickup]|> lower[order ready for pickup]|> squeeze[order ready for pickup]trim >> lower >> squeezethe same steps, as tidy

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The value goes first. The pipeline starts with the data, raw, which is " Order READY for Pickup ": three spaces in front, two at the end, and two or three between some words.
  2. trim. raw |> trim means trim raw. The outer spaces go, and the program printed [Order READY for Pickup], with the inner spaces still there.
  3. lower. The result of trim is the input of lower, which gives [order ready for pickup]. Each step sees only the value the one before it produced.
  4. squeeze. squeeze splits on spaces, drops the empty pieces and joins the words with one space: [order ready for pickup], the same text the nested call printed.
  5. The steps as one function. trim >> lower >> squeeze joins the three steps into a new function, tidy, without any data. tidy raw printed [order ready for pickup] as well.

The symbol and operator reference lists two relatives. ||> passes a pair into a curried function, so (3, 4) ||> add is add 3 4, 7. <| passes the value on its right to the function on its left, which saves parentheses after a function such as printfn "%s" <| shout "ready". Its precedence also catches people out:

Chaining <| as if it read right to left

Wrong: shout <| lower <| trim " Pickup ". <| groups from the left, so F# tried shout <| lower first, giving shout a function instead of a string, and stopped with error FS0001. Right: one <| per expression, or parentheses, as in shout <| (lower <| trim " Pickup "), which gave “PICKUP!”, or simply a pipeline.

Text Diff Paste the raw text and the cleaned text side by side to see every change the pipeline made.

Checkpoint

Check: Pipelines and composition

  1. Question 1 of 2 What does raw |> trim mean?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: trim raw

    A pipeline is ordinary function application written with the argument first, so the steps read in the order they run.

  2. Question 2 of 2 What is tidy in let tidy = trim >> lower >> squeeze?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A function from string to string that runs the three steps when it is called

    f >> g is fun x -> g (f x). Use it to name a chain of steps that you will apply later or pass to another function.

Tupled parameters and .NET methods take one tuple

A parameter list in parentheses with commas is one argument, a tuple. The Parameters and Arguments reference calls that the tuple form, and the spaced one the curried form. Here is the difference in practice:

A tupled function, a .NET method, and a lambda around one Python · tupled_session.py
from session import fsi

# A tupled function takes one tuple, so it cannot be given half its arguments; .NET methods take their arguments
# the same way, and a lambda turns one into a function a pipeline can use.
fsi([
    "let area (w, h) = w * h;;",
    "area (3, 4);;",
    "(3, 4) |> area;;",
    "let areaOf3 = area 3;;",
    "System.Math.Max(3, 7);;",
    "System.Math.Max 3 7;;",
    "[ -2; 5 ] |> List.map (fun x -> System.Math.Max(x, 0));;",
    "#quit;;",
], args=["--nologo"])

Output

$ dotnet fsi --nologo
> let area (w, h) = w * h;;
val area: w: int * h: int -> int

> area (3, 4);;
val it: int = 12

> (3, 4) |> area;;
val it: int = 12

> let areaOf3 = area 3;;

  let areaOf3 = area 3;;
  -------------------^

stdin(4,20): error FS0001: This expression was expected to have type
    'int * int'
but here has type
    'int'

> System.Math.Max(3, 7);;
val it: int = 7

> System.Math.Max 3 7;;

  System.Math.Max 3 7;;
  ^^^^^^^^^^^^^^^^^

stdin(6,1): error FS0503: A member or object constructor 'Max' taking 1 arguments is not accessible from this code location. All accessible versions of method 'Max' take 2 arguments.

> [ -2; 5 ] |> List.map (fun x -> System.Math.Max(x, 0));;
val it: int list = [0; 5]

> #quit;;

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tupled_session.py

area (w, h) takes one int * int, so area (3, 4) works and so does piping a whole pair, (3, 4) |> area, which gave 12. area 3 does not: error FS0001 expected an int * int and got an int. .NET methods take their arguments the same way. System.Math.Max(3, 7) is 7, while System.Math.Max 3 7 stopped with error FS0503, because every version of Max takes its two arguments at once. To use such a method in a pipeline, wrap it in a lambda: List.map (fun x -> System.Math.Max(x, 0)) turned [ -2; 5 ] into [0; 5].

The Parameters and Arguments reference gives the reason methods use the tuple form: it matches the way arguments are passed to .NET methods, which reads more clearly from other .NET languages, while let-bound functions usually use the curried form. Which form fits depends on who calls the code and how. Answer two questions about yours:

Interactive · answer and compare Curried or tupled parameters? Answer two questions about who calls the function and how. The table marks the forms that would make those calls awkward.
Who will call it?
Do callers fix one argument and vary the other?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Three ways to take two arguments against the uses that differ between them, with what each did in this lesson's runs
Form Partial application In pipelines For .NET callers Named, optional arguments
Curried function Looks like: let area w h = … Good add 1 gave a function, val addOne: (int -> int) Good data last: xs |> List.map f reads left to right Mixed for other .NET languages the guidelines ask for the call shape M(a, b) instead Poor named arguments are allowed only for methods
Tupled function Looks like: let area (w, h) = … Poor area 3 was error FS0001: it expected an int * int Mixed pipe in a whole tuple: (3, 4) |> area gave 12 Mixed area (3, 4) has the call shape, but it is still a module function Poor named arguments are allowed only for methods
Method on a type Looks like: member _.Area(w, h) = … Poor Math.Max 3 7 was error FS0503: Max takes its 2 arguments at once Mixed needs a lambda: List.map (fun x -> Math.Max(x, 0)) Good the tuple form matches how .NET methods take arguments Good named and optional arguments, and overloads, work on methods
How the answers decide
  • “Who will call it?” C# or other .NET code too: adds a caveat to Curried function and Tupled function. For libraries used from other .NET languages, the F# component design guidelines ask for members of types with .NET calling conventions, Method(arg1, arg2), instead of curried parameters.
  • “Do callers fix one argument and vary the other?” Yes, one argument is a setting: adds a caveat to Tupled function and Method on a type. A tuple must arrive whole: area 3 was error FS0001 and Math.Max 3 7 error FS0503. Every partial use then needs a lambda, such as fun x -> Math.Max(x, 0).
  • “Who will call it?” F# code, often in pipelines and “Do callers fix one argument and vary the other?” No, both change on every call: In F# code both function forms work; the curried one reads better in pipelines when the data comes last.

In short: curried let functions for F# code that pipes and specialises, and members with tupled parameters on a type when C# will call them, as the F# component design guidelines ask of libraries for other .NET languages.

Checkpoint

Check: Tupled parameters and .NET methods

  1. Question 1 of 2 Why was let areaOf3 = area 3 refused, when area is let area (w, h) = w * h?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: area takes one tuple argument of type int * int, so 3 alone has the wrong type

    A tupled parameter list is a single argument. To fix one part of it, write a lambda: fun h -> area (3, h).

  2. Question 2 of 2 You want every negative number in a list replaced by 0 with System.Math.Max. Which line works?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: xs |> List.map (fun x -> System.Math.Max(x, 0))

    A .NET method takes its arguments together, so a pipeline uses it through a lambda that supplies them all.

A shorthand for lambdas that read a member

Pipelines are full of tiny lambdas such as fun c -> c.City. F# 8 added a shorthand for them: _.City. The F# RFC FS-1129 defines _.Foo as fun o -> o.Foo, and the F# 8 announcement says it works for a property, a nested property, a method call and an indexer:

shorthand.fsx: _.Length, _.City and _.Name.ToUpper() FSX · shorthand.fsx
type Customer = { Name: string; City: string; Orders: int }

let customers =
    [ { Name = "Asha"; City = "Pune"; Orders = 4 }
      { Name = "Ravi"; City = "Agra"; Orders = 9 }
      { Name = "Meera"; City = "Kochi"; Orders = 2 } ]

// _.City stands for fun c -> c.City; it can read a property, call a method, or both.
printfn "%A" ([ "kiwi"; "fig"; "banana" ] |> List.map _.Length)
printfn "%A" (customers |> List.sortBy _.City |> List.map _.Name)
printfn "%A" (customers |> List.filter (fun c -> c.Orders > 3) |> List.map _.Name.ToUpper())
shorthand.fsx, then the shorthand before its data, and a point-free definition Python · shorthand_session.py
from session import fsi, run

# shorthand.fsx, then the shorthand before its data, and a point-free definition that is not allowed to be generic.
run("""
dotnet fsi shorthand.fsx
""")
fsi([
    'List.map _.Length [ "kiwi"; "fig"; "banana" ];;',
    'List.map (fun (s: string) -> s.Length) [ "kiwi"; "fig"; "banana" ];;',
    "let revTwice = List.rev >> List.rev;;",
    "let revTwice xs = (List.rev >> List.rev) xs;;",
    "#quit;;",
], args=["--nologo"])

Output

$ dotnet fsi shorthand.fsx
[4; 3; 6]
["Ravi"; "Meera"; "Asha"]
["ASHA"; "RAVI"]
$ dotnet fsi --nologo
> List.map _.Length [ "kiwi"; "fig"; "banana" ];;

  List.map _.Length [ "kiwi"; "fig"; "banana" ];;
  ---------^^^^^^^^

stdin(1,10): error FS0072: The type of this expression could not be inferred before accessing its members. Add a type annotation, e.g. (expr: SomeType), to constrain the type before this point.

> List.map (fun (s: string) -> s.Length) [ "kiwi"; "fig"; "banana" ];;
val it: int list = [4; 3; 6]

> let revTwice = List.rev >> List.rev;;

  let revTwice = List.rev >> List.rev;;
  ----^^^^^^^^

stdin(3,5): error FS0030: Value restriction: The value 'revTwice' has an inferred generic function type
    val revTwice: ('_a list -> '_a list)
However, values cannot have generic type variables like '_a in "let f: '_a". You should define 'revTwice' as a function instead by doing one of the following:
- Add an explicit parameter that is applied instead of using a partial application "let f param"
- Add a unit parameter like "let f()"
- Write explicit type parameters like "let f<'a>"
or if you do not intend for it to be generic, either:
- Add an explicit type annotation like "let f : obj -> obj"
- Apply arguments of non-generic types to the function value in later code for type inference like "do f()".
This error is because a let binding without parameters defines a value, not a function. Values cannot be generic because reading a value is assumed to result in the same everywhere but generic type parameters may invalidate this assumption by enabling type-dependent results.

> let revTwice xs = (List.rev >> List.rev) xs;;
val revTwice: xs: 'a list -> 'a list

> #quit;;

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shorthand_session.py

List.sortBy _.City sorted the customers by city, Agra, Kochi, Pune, and _.Name.ToUpper() read a property and called a method on it. At the prompt, List.map _.Length [ "kiwi"; "fig"; "banana" ] failed with FS0072: the shorthand is a lambda, and like fun s -> s.Length it needs to know its type before it reads a member. In shorthand.fsx the list comes first, through |>, so the same _.Length gave [4; 3; 6].

Lambda Shorthand Needs the type first
fun c -> c.City _.City yes: put the data first with |>
fun c -> c.Name.ToUpper() _.Name.ToUpper() yes
fun (s: string) -> s.Length none: an annotation needs fun no: the annotation gives it

Leaving out the data is called point-free style: Defining a function without naming its data, by composing or partially applying other functions, as let tidy = trim >> lower >> squeeze does., and the session’s last entries show its cost. let revTwice = List.rev >> List.rev has no parameter, so it is a value, and a generic one, which the value restriction forbids: error FS0030, whose message suggests the fix that followed, an explicit parameter, let revTwice xs = (List.rev >> List.rev) xs.

Name the data in public functions

The F# coding conventions advise against partial application and point-free definitions in public APIs: curried values do not label their arguments, so tooltips show (decimal -> decimal) with no names, and there is nothing to inspect in a debugger. Use them inside a module, and give public functions named parameters.

Checkpoint

Check: The _.Name shorthand and point-free style

  1. Question 1 of 2 List.map _.Length [ "kiwi"; "fig"; "banana" ] failed with FS0072, while [ "kiwi"; "fig"; "banana" ] |> List.map _.Length gave [4; 3; 6]. Why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: _.Length is a lambda that reads a member, so it needs its type first, and the pipeline supplies the list first

    _.Name is fun x -> x.Name. A member access needs the type of x, so put the data first with |>.

  2. Question 2 of 2 let revTwice = List.rev >> List.rev was refused with FS0030. Which definition compiled and stayed generic?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: let revTwice xs = (List.rev >> List.rev) xs

    A point-free definition is a value. When its type would be generic, the value restriction applies; adding the parameter back is the fix the error message suggests first.

Practise: a log formatter from one function

Here is one general function, formatLine level at message, specialised into errorLine and infoLine by partial application, and a pipeline that filters, formats and joins a short log. Read it, predict the report, then compare:

logs.fsx: partial application and a pipeline FSX · logs.fsx
// A log formatter built from one general function, specialised by partial application.
type Entry = { Level: string; At: string; Message: string }

let formatLine (level: string) (at: string) (message: string) = $"{at} [{level}] {message}"

// The level comes first, so fixing it leaves a function of the time and the message.
let errorLine = formatLine "ERROR"
let infoLine = formatLine "INFO"

let entries =
    [ { Level = "INFO"; At = "09:00"; Message = "service started" }
      { Level = "ERROR"; At = "09:02"; Message = "payment timed out" }
      { Level = "DEBUG"; At = "09:03"; Message = "cache warm" }
      { Level = "ERROR"; At = "09:05"; Message = "retry failed" } ]

let format entry =
    match entry.Level with
    | "ERROR" -> errorLine entry.At entry.Message
    | _ -> infoLine entry.At entry.Message

// The pipeline: keep what matters, format each entry, join the lines into one report.
let report =
    entries
    |> List.filter (fun e -> e.Level <> "DEBUG")
    |> List.map format
    |> String.concat "\n"

printfn "%s" report
printfn "errors only: %A" (entries |> List.filter (fun e -> e.Level = "ERROR") |> List.map _.Message)
The report, then the messages of the errors only Python · logs_session.py
from session import run

run("""
dotnet fsi logs.fsx
""")

Output

$ dotnet fsi logs.fsx
09:00 [INFO] service started
09:02 [ERROR] payment timed out
09:05 [ERROR] retry failed
errors only: ["payment timed out"; "retry failed"]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 logs_session.py

The DEBUG line is filtered out before formatting, so three lines remain, and _.Message works on the last line because the list of entries comes first in its pipeline. Because the level is the first parameter of formatLine, fixing it gave two functions of the time and the message; with the message first, neither would have been possible without a lambda.

Add a warning level

Add let warnLine = formatLine "WARN", a log entry with the level “WARN”, and a rule | "WARN" -> warnLine … in format, then run dotnet fsi logs.fsx in the folder where you saved it. The report gains a line with [WARN] in it, in the position of its entry.

Last, run the self-check. It covers currying, partial application, pipelines, tupled parameters and the shorthand, picks each next question from what you have already got right, and explains every option the moment you select it:

Practice that adapts to you

Practise this lesson

15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 What is currying?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: Treating a function of several parameters as a chain of one-parameter functions, so that fewer arguments give a new function

    Because F# functions are curried, add 1 41 and (add 1) 41 are the same call.

  2. Question 2 of 15 How does F# read the type int -> int -> int?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: int -> (int -> int): an int goes in, and a function from int to int comes out

    Read each arrow as "returns": an int returns a function, which returns an int.

  3. Question 3 of 15 How does F# group the call on the second line?

    Read the code, then choose one answer.

    let add x y = x + y
    let total = add 1 41
    Show the answer: Question 3 of 15

    Answer: (add 1) 41, which is 42

    Application binds tightest and groups from the left; the operator precedence table lists f x as left associative.

  4. Question 4 of 15 What does partial application of a curried function return?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: A function that takes the remaining arguments

    Partial application is how F# configures behaviour: the fixed arguments live inside the function it returns.

  5. Question 5 of 15 salePrice is saleDiscount >> withTax. What did prices.fsx print as the Lamp's sale price, from a list price of 850.00?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: 902.70

    f >> g runs f first: salePrice took 10 % off 850.00, giving 765.00, and then added 18 % tax to that.

  6. Question 6 of 15 What is List.map withTax?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: A function that takes a list of prices and returns them with tax, decimal list -> decimal list

    The List module puts the function first and the list last, so partial application of its functions builds new list functions.

  7. Question 7 of 15 In f >> g, which function runs first?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: f, and then g on the result of f

    Forward composition and the pipeline both read from left to right, in the order the steps run.

  8. Question 8 of 15 What does (3, 4) ||> add compute, with add x y = x + y?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: add 3 4, which is 7

    |> pipes one value, ||> a pair and |||> a triple, as the symbol and operator reference lists.

  9. Question 9 of 15 shout <| lower <| trim " Pickup " failed with FS0001. Which rewrite works and reads in the order the steps run?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: " Pickup " |> trim |> lower |> shout

    <| groups from the left like the other < operators. Use one <| at a time, or turn the chain into a pipeline.

  10. Question 10 of 15 Which definition takes a single tuple argument?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: let area (w, h) = w * h

    The Parameters and Arguments reference calls (w, h) the tuple form and w h the curried form.

  11. Question 11 of 15 Which call of the tupled area worked in the session?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: (3, 4) |> area, which gave 12

    A tupled function can still sit in a pipeline when the value flowing through it is the whole tuple.

  12. Question 12 of 15 A library of pricing rules will be called from C#. What do the F# component design guidelines ask of its public API?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: Members of types with .NET calling conventions, Method(arg1, arg2), instead of curried parameters

    Inside F#, curried functions suit pipelines; at a boundary with C#, the shape C# expects matters more.

  13. Question 13 of 15 What does _.City stand for?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: fun x -> x.City

    The shorthand saves the fun x -> x. part of a lambda that reads a member.

  14. Question 14 of 15 Which uses of the _. shorthand did the F# 8 announcement say work?

    Choose every answer that is right.

    Show the answer: Question 14 of 15

    Answer:

    • A method call, as in _.Name.ToUpper()
    • A property, as in _.Name
    • An indexer

    _. covers single and nested property access, method calls and indexers, always on the one value the lambda receives.

  15. Question 15 of 15 A public function is written point-free, let normalise = trim >> lower >> squeeze. What do the F# coding conventions suggest?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: Write it with a named parameter, let normalise text = …, so tools show the name and a debugger has a value to inspect

    Partial application and point-free style suit internals; public functions should name their data.

Interview questions

Warm-up (fresher to 2–5 years): how do |> and >> differ in F#, and when do you use each?

|> is the pipe: x |> f means f x, so it hands a value to a function and gives a value back, and raw |> trim |> lower |> squeeze runs three steps on one string in the order they are written. >> is composition: f >> g is a new function, fun x -> g (f x), which does nothing until it receives an argument, so let tidy = trim >> lower >> squeeze names the chain for reuse. Both read from left to right. Use |> when you have the data in hand and >> when you are building a function to pass around. A composed definition without a parameter is a value, so a generic one hits the value restriction: let revTwice = List.rev >> List.rev is error FS0030, and let revTwice xs = (List.rev >> List.rev) xs compiles.

Two harder questions follow, about a point-free public API and about porting a configured C# class:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A library's public functions are written point-free, such as let normalise = trim >> lower >> squeeze and let withTax = addTax 0.18m. Users complain that their editor shows no parameter names, and a new generic helper written the same way does not compile. What do you change, and why?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A C# PriceCalculator class takes a tax rate and a discount in its constructor and has one method, Price(amount). You are porting it to F#. How would you write it, and when would you still keep a class?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Every F# function takes one argument: add 1 is a function of type int -> int, and add 1 41 is (add 1) 41.
  • Partial application fixes the first arguments and returns a function of the rest, so put settings first and data last, as the List module does.
  • x |> f means f x and runs steps in the order they are written, f >> g builds a new function without data, and a chain of <| groups from the left and fails.
  • A tupled parameter list is one tuple argument, as .NET methods take theirs, so it cannot be partially applied; wrap a method in a lambda to use it in a pipeline, and expose tupled members when C# will call the code.
  • _.Name is short for fun x -> x.Name and, like that lambda, needs its data first; point-free definitions without parameters can hit FS0030 and hide argument names, so keep them out of public APIs.

Terms in this lesson

function composition
Joining two functions into a new one with >> or <<: f >> g is the function fun x -> g (f x), which runs f and passes its result to g.
partial application
Calling a curried function with fewer arguments than it takes, which returns a new function of the remaining ones: addTax 0.18m returns a function that takes the price.
point-free style
Defining a function without naming its data, by composing or partially applying other functions, as let tidy = trim >> lower >> squeeze does.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.