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.
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:
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:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- 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.
- 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).
- 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.
- 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.
- 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
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:
// 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) 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
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:
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 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:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- 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.
- trim. raw |> trim means trim raw. The outer spaces go, and the program printed [Order READY for Pickup], with the inner spaces still there.
- 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.
- 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.
- 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.
Checkpoint
Check: Pipelines and composition
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:
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:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| 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
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:
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()) 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
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:
// 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) 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.
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.
-
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?
-
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?
Key takeaways
- Every F# function takes one argument:
add 1is a function of typeint -> int, andadd 1 41is(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 |> fmeansf xand runs steps in the order they are written,f >> gbuilds 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.
_.Nameis short forfun x -> x.Nameand, 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
- Functions (Microsoft Learn)
- Using functions in F# (Microsoft Learn)
- Symbol and operator reference (Microsoft Learn)
- Parameters and Arguments (Microsoft Learn)
- Lambda Expressions: The fun Keyword (Microsoft Learn)
- F# RFC FS-1129: shorthand anonymous unary functions (F# Software Foundation (fslang-design))
- Announcing F# 8 (Microsoft .NET Blog)
- F# coding conventions: partial application and point-free programming (Microsoft Learn)
- F# component design guidelines (Microsoft Learn)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress