Wrap-up
Where you stand
Six modules ago, you had never opened dotnet fsi. Here's, in one sentence each, what you can now do, and its anchor on the C# side:
letbinds a name to a value or a function, with a type inferred by the compiler rather than written by hand as in C# (module 2).letvsmutable: an F# value is fixed by default;<-is only allowed on something explicitly declaredmutable— the opposite of C#'s default variable (module 3).|>and>>turn a chain of nested calls (theEnumerable.Sum(Enumerable.Select(...))style) into a left-to-right reading, and let you build a new function from two others without ever supplying them a value (module 4).matchreplacesif/elifandswitchto branch on the shape of a value, including decomposing a tuple in a single step (module 5).- Records offer structural equality by default — two instances with the same values are equal — where an ordinary C# class compares references (module 6).
None of these five points is an isolated topic: the exercise that follows combines all of them into a single small program.
Integrating exercise
You're managing a small product catalog. Each product has a name and a price. Goal: classify each product into a price tier, then only display the ones that aren't "Budget."
Open dotnet fsi and build the program step by step. This exercise recombines everything you've seen so far: take whatever time you need — this module's estimated duration mostly accounts for reading, not the pace at which you have to work through the five steps.
Step 1 — the record and the data, reusing exactly the syntax from module 6:
type Product = { Name: string; Price: float };;
let catalog =
[ { Name = "Pen"; Price = 1.5 }
{ Name = "Keyboard"; Price = 45.0 }
{ Name = "Monitor"; Price = 220.0 }
{ Name = "Mouse"; Price = 15.0 } ];;
Notice that here the list elements are separated by a plain line break, with no ; — a valid alternative to the semicolon seen in module 4, as long as every element stays aligned in the same column. Both notations produce exactly the same list.
Step 2 — a classification function, reusing match with guards as in module 5:
let priceTier (product: Product) =
match product.Price with
| p when p < 10.0 -> "Budget"
| p when p < 100.0 -> "Standard"
| _ -> "Premium";;
The (product: Product) annotation isn't optional here the way it was in module 2: without it, the compiler has no way of knowing, at the point where it accepts product.Price, what type the product parameter belongs to — exactly the module 2 case where an annotation becomes necessary as soon as you access a member of a specific type.
Step 3 — the pipeline, reusing |>, List.filter, and List.map as in module 4:
let result =
catalog
|> List.filter (fun product -> priceTier product <> "Budget")
|> List.map (fun product -> product.Name, priceTier product);;
Step 4 — verify, by displaying the result:
printfn "%A" result;;
Expected output:
[("Keyboard", "Standard"); ("Monitor", "Premium"); ("Mouse", "Standard")]
"Pen" doesn't show up: at 1.5, it's classified "Budget" by priceTier, so it's excluded by the step 3 filter. If your result still includes "Pen", re-read the List.filter condition — it's the same kind of ordering or inverted-condition mistake as in module 4.
Step 5 — write a function from scratch. With no example to copy this time, write your own isExpensive product function that returns true if priceTier product equals "Premium", false otherwise — using either match or a plain comparison, your choice. Test it on the four products in the catalog and check that only "Monitor" returns true.
What awaits you at the Intermediate tier
This Beginner tier deliberately stops before several topics that directly extend what you've just practiced. They aren't taught here — only named, so you know where to continue:
- Discriminated unions: a way to describe that a value can take several distinct, mutually exclusive shapes — richer than the plain category
stringthatpriceTierreturns here. - The
Optiontype: a way to represent explicitly, in the type itself, that a value might be absent — without resorting tonull. Resultand railway-oriented programming: a way to carry a possible failure through a|>pipeline, rather than throwing an exception.- Testability and mocking in F#: how to structure F# code so it stays easy to unit test, including when it depends on external resources.
You now have the foundations you need — let, immutability, |>/>>, match, records — to tackle these four topics once the Intermediate tier is available.
Check your understanding
In let priceTier (product: Product) = match product.Price with ..., why is the (product: Product) annotation necessary here, when it wasn't for most functions in earlier modules?
In step 3, catalog |> List.filter (fun p -> priceTier p <> "Budget") |> List.map (fun p -> p.Name, priceTier p), if you swap the two steps (map first, then a filter that calls priceTier again on the result), what happens?
To write isExpensive product, which returns true only if priceTier product = "Premium", which formulation is closest in spirit to the match seen in module 5?
Want to hear about the next modules?
The Academy stays free and open-access, no sign-up required. If you'd just like to be notified by email when a new module ships, here you go — no obligation, unsubscribe anytime with one click.