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.

OCaml  Module 4 – Tuples, records, variants and pattern matching

Pattern matching in depth

Combine or-patterns, as-patterns and guards in OCaml, read warnings 8, 11 and 4, see why guards weaken the exhaustiveness check, and match on exceptions.

  • Intermediate
  • 5 minutes
  • Examples run with ocaml 5.5.1, opam 2.6.0, dune 3.24.2 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 5 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Combine nested, or-, as- and guarded patterns in one match, written with function or match
  • Recognise when guards weaken the exhaustiveness check, and rewrite the match so it passes
  • Interpret warnings 8 and 11, and repair a fragile catch-all case that warning 4 reports
  • Use match with exception and explain how it differs from try with

Before you start

What you’ll learn · Before you start

What you will learn

  • Combine nested, or-, as- and guarded patterns in one match, written with function or match
  • Recognise when guards weaken the exhaustiveness check, and rewrite the match so it passes
  • Interpret warnings 8 and 11, and repair a fragile catch-all case that warning 4 reports
  • Use match with exception and explain how it differs from try with

Before you start

On this page

A match on an integer with the cases 0, n when n > 0 and n when n < 0 covers every number, yet OCaml warns that it is not exhaustive and offers 1 as a value it may miss. The checker reads patterns, not guards, and knowing what it reads is the difference between a warning you can fix and one you learn to ignore.

You will combine or-patterns, as-patterns and guards, rewrite guarded matches so the checker can prove them complete, read warnings 8, 11 and 4, and handle an exception from the value being matched without hiding others.

Or-patterns, as-patterns and guards combine in one case

A pattern can do more than name a constructor. An or-pattern: A pattern p1 | p2 that matches when either side matches; both sides must bind the same variables., p1 | p2, matches when either side does, so (0, _) | (_, 0) catches a zero in either place. An as-pattern, p as name, matches p and also binds the whole matched value to name. A guard: A condition after a pattern, written when expr, that is tested only once the pattern has matched; if it is false, matching goes on to the next case., when condition, adds a test that runs only after the pattern has matched. Cases are tried from the top, and the first one whose pattern and guard both succeed wins. Step through the session’s classify on two of its inputs:

Interactive · step by step classify tries its cases from the top Step through two calls. The four cases are on the left; the value being matched and the outcome are on the right.
classify tries its cases from the top, step 6 of 6. Showing (0, _) | (_, 0) (no zero), (a, b) as p when a = b (guard false), when a > 0 && b > 0 (guard false), _, value matched ((-1, 3)), result (other). In focus: _, result.(0, _) | (_, 0)no zero(a, b) as p when a = bguard falsewhen a > 0 && b > 0guard false_value matched(-1, 3)resultother

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

Every step as text
  1. classify (4, 4). The value (4, 4) is tried against the first case. Neither side is 0, so the or-pattern fails and the next case is tried.
  2. Pattern, then guard. (a, b) as p matches any pair, binding a = 4, b = 4 and p to the whole pair. Only then is the guard a = b tested, and it is true.
  3. The first full match wins. The second case's result is used and the later cases are never looked at. The session printed "(4, 4) is a double".
  4. classify (-1, 3). A new call starts again from the top. (-1, 3) has no zero, so the first case fails.
  5. Two guards fail. The pattern of case 2 matches but a = b is false; case 3 matches the pattern too, but a > 0 is false. A failed guard sends matching on to the next case.
  6. The catch-all. _ matches anything, so the last case gives "other". Without it, this value would reach no case and raise Match_failure.

classify with or-, as- and guarded cases, and a nested list pattern Python · classify_session.py
from session import toplevel, write

# Or-patterns, as-patterns and guards combine in one match. Cases are tried from the top; a guard (when …) is tested
# only after its pattern has matched. function is fun plus match on the argument.
write("classify.ml", """let classify = function
  | (0, _) | (_, 0) -> "has a zero"
  | (a, b) as p when a = b -> Printf.sprintf "(%d, %d) is a double" (fst p) (snd p)
  | (a, b) when a > 0 && b > 0 -> "both positive"
  | _ -> "other"

let first_two = function
  | x :: (y :: _ as rest) -> Printf.sprintf "%d then %d; tail %s" x y (String.concat "," (List.map string_of_int rest))
  | [x] -> Printf.sprintf "only %d" x
  | [] -> "empty"
""")
toplevel("""
#use "classify.ml";;
List.map classify [(0, 5); (4, 4); (2, 9); (-1, 3)];;
List.map first_two [[7; 8; 9]; [7]; []];;
""")

Output

$ ocaml
OCaml version 5.5.1
Enter #help;; for help.

# #use "classify.ml";;
val classify : int * int -> string = <fun>
val first_two : int list -> string = <fun>
# List.map classify [(0, 5); (4, 4); (2, 9); (-1, 3)];;
- : string list =
["has a zero"; "(4, 4) is a double"; "both positive"; "other"]
# List.map first_two [[7; 8; 9]; [7]; []];;
- : string list = ["7 then 8; tail 8,9"; "only 7"; "empty"]

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

classify returned "has a zero" for (0, 5), "(4, 4) is a double" for (4, 4), "both positive" for (2, 9) and "other" for (-1, 3). Patterns also nest: in first_two, x :: (y :: _ as rest) takes the first element as x and binds the rest of the list, which must start with some y, to rest; for [7; 8; 9] it printed 7 then 8; tail 8,9. Both functions are written with function, which is fun x -> match x with in one word: the cases match the function’s single argument.

Pattern Matches Binds
(0, _) or (_, 0), as one or-pattern a pair with a zero on either side nothing
(a, b) as p any pair a, b and the whole pair p
(a, b) when a = b a pair whose sides are equal a, b
x :: (y :: _ as rest) a list of two or more x, y, and rest from y on

An or-pattern that binds different names

Wrong: | (0, b) | (a, 0) -> …, which OCaml refuses because each side of an or-pattern must bind the same variables. Right: | (0, _) | (_, 0) -> …, or two separate cases when each needs its own variable.

Guards are invisible to the exhaustiveness check

The compiler proves a match complete from its patterns alone. A guard is an arbitrary expression, so the checker treats every guarded case as one that may fail, and a match whose guards happen to cover everything still looks incomplete to it:

A guarded sign, warning 11, and an impossible case Python · warnings_session.py
from session import toplevel

# Guards are not used by the exhaustiveness check, so a match whose guards cover every int still draws warning 8.
# Warning 11 marks a case that can never be reached. A refutation case, -> ., states that a case is impossible.
toplevel("""
let sign n = match n with 0 -> "zero" | n when n > 0 -> "positive" | n when n < 0 -> "negative";;
let sign n = match n with 0 -> "zero" | n when n > 0 -> "positive" | _ -> "negative";;
List.map sign [0; 12; -3];;
let f x = match x with _ -> 0 | 1 -> 1;;
let g = function Some 0 -> "zero" | Some _ -> "some" | None -> "none" | Some 1 -> "one";;
type empty = |;;
let only_none (x : empty option) = match x with None -> 0 | Some _ -> .;;
""")

Output

$ ocaml
OCaml version 5.5.1
Enter #help;; for help.

# let sign n = match n with 0 -> "zero" | n when n > 0 -> "positive" | n when n < 0 -> "negative";;
Line 1, characters 13-95:
1 | let sign n = match n with 0 -> "zero" | n when n > 0 -> "positive" | n when n < 0 -> "negative";;
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Warning 8 [partial-match]: this pattern-matching is not exhaustive.
  Here is an example of a case that is not matched:
    1
    (However, some guarded clause may match this value.)

val sign : int -> string = <fun>
# let sign n = match n with 0 -> "zero" | n when n > 0 -> "positive" | _ -> "negative";;
val sign : int -> string = <fun>
# List.map sign [0; 12; -3];;
- : string list = ["zero"; "positive"; "negative"]
# let f x = match x with _ -> 0 | 1 -> 1;;
Line 1, characters 32-33:
1 | let f x = match x with _ -> 0 | 1 -> 1;;
                                    ^
Warning 11 [redundant-case]: this match case is unused.

val f : int -> int = <fun>
# let g = function Some 0 -> "zero" | Some _ -> "some" | None -> "none" | Some 1 -> "one";;
Line 1, characters 72-78:
1 | let g = function Some 0 -> "zero" | Some _ -> "some" | None -> "none" | Some 1 -> "one";;
                                                                            ^^^^^^
Warning 11 [redundant-case]: this match case is unused.

val g : int option -> string = <fun>
# type empty = |;;
type empty = |
# let only_none (x : empty option) = match x with None -> 0 | Some _ -> .;;
val only_none : empty option -> int = <fun>

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

The first sign drew “Warning 8 [partial-match]”, with 1 as an example of an unmatched value and the note “(However, some guarded clause may match this value.)”. The note is the compiler admitting that a guard might cover it, which it cannot check. The fix is to make the last case unguarded: in the second sign, _ -> "negative" handles whatever the guards did not, and the warning disappeared; List.map sign [0; 12; -3] gave ["zero"; "positive"; "negative"].

The same session shows two more messages. match x with _ -> 0 | 1 -> 1 drew “Warning 11 [redundant-case]: this match case is unused”: _ already took every value, so the case after it can never run. In g, Some 1 came after Some _, which had already matched it. The last line uses a refutation case, Some _ -> ., for a type with no values, empty: it tells the checker that a case is impossible, and the checker agreed. The manual’s chapter on GADTs is where this becomes common.

Key idea

The checker proves exhaustiveness from patterns alone: end a guarded match with an unguarded case, and read warning 8’s guarded-clause note as a request to restructure, not as a false alarm.

Checkpoint

Check: Or-, as- and guarded patterns · Guards and exhaustiveness

  1. Question 1 of 2 What does the lesson's classify return for (0, 0)?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: "has a zero"

    Order matters when patterns overlap: put the more specific case first, as classify does.

  2. Question 2 of 2 match n with 0 -> "zero" | n when n > 0 -> "positive" | n when n < 0 -> "negative" draws warning 8. Which rewrite makes the warning disappear without changing the results?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Make the last case | _ -> "negative"

    The session's second sign used _ -> "negative" and compiled with no warning, giving ["zero"; "positive"; "negative"] for [0; 12; -3].

Warnings 8, 11 and 4 each point at a different fragility

Warning 8 means a value can reach no case and would raise Match_failure. Warning 11 means a case can never be reached, usually because a broader case above it already matches. A third warning protects against the future. A catch-all _ keeps a match exhaustive whatever happens to its type, so when the type gains a constructor the compiler has nothing to say. The session adds Pending to a status type and compiles twice:

A new constructor, an explicit match and a fragile one Python · fragile_session.py
from session import run, write

# A catch-all case keeps a match exhaustive when the type gains a constructor, so the compiler stays silent. Warning 4
# (fragile-match), off by default, reports such matches.
write("status.ml", """type status = Active | Suspended | Closed | Pending

let label = function
  | Active -> "active"
  | _ -> "inactive"

let can_log_in = function
  | Active -> true
  | Suspended | Closed -> false
""")
run("""
ocamlopt -c status.ml
ocamlopt -w +4 -c status.ml
""")

Output

$ ocamlopt -c status.ml
File "status.ml", lines 7-9, characters 17-31:
7 | .................function
8 |   | Active -> true
9 |   | Suspended | Closed -> false
Warning 8 [partial-match]: this pattern-matching is not exhaustive.
  Here is an example of a case that is not matched: Pending
$ ocamlopt -w +4 -c status.ml
File "status.ml", lines 3-5, characters 12-19:
3 | ............function
4 |   | Active -> "active"
5 |   | _ -> "inactive"
Warning 4 [fragile-match]: this pattern-matching is fragile.
  It will remain exhaustive when constructors are added to type status.

File "status.ml", lines 7-9, characters 17-31:
7 | .................function
8 |   | Active -> true
9 |   | Suspended | Closed -> false
Warning 8 [partial-match]: this pattern-matching is not exhaustive.
  Here is an example of a case that is not matched: Pending

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

can_log_in, which lists Active, Suspended and Closed explicitly, drew warning 8 naming the new Pending: the compiler found the place that needs a decision. label, with _ -> "inactive", compiled silently in the first run, so a pending account would be labelled inactive without anyone choosing that. Only with -w +4 did OCaml report it: “Warning 4 [fragile-match]: this pattern-matching is fragile. It will remain exhaustive when constructors are added to type status.” Warning 4 is off by default; the manual’s list of warnings describes it as a fragile match.

Warning What it reports Usual fix
8 partial-match a value that reaches no case add the case, or an unguarded last case
11 redundant-case a case that can never be reached remove it, or move it above the broader case
4 fragile-match a catch-all on a type that may grow list the constructors explicitly

List constructors for types that grow

For a type of states, roles or errors that will gain cases, write every constructor in each match instead of _. The next new constructor then makes warning 8 list every match that must decide what to do with it.

match with exception handles errors from the value being matched

List.assoc raises Not_found when a key is missing. A case of the form exception Not_found -> … in a match catches that exception, but only when it is raised while computing the value being matched, not inside a branch. The session compares it with try … with, which catches the exception wherever it is raised inside the try:

match with exception compared with try with Python · lookup_session.py
from session import toplevel

# match e with … | exception E -> … handles an exception raised while computing e, the value being matched, and no
# other; try … with also catches exceptions raised inside the branches.
toplevel("""
let ages = [("asha", 31); ("ravi", 27)];;
let age_or_zero name = match List.assoc name ages with a -> a | exception Not_found -> 0;;
(age_or_zero "ravi", age_or_zero "meera");;
let sum_ages a b = match List.assoc a ages with x -> x + List.assoc b ages | exception Not_found -> -1;;
sum_ages "meera" "ravi";;
sum_ages "asha" "meera";;
let sum_ages_try a b = try List.assoc a ages + List.assoc b ages with Not_found -> -1;;
sum_ages_try "asha" "meera";;
""")

Output

$ ocaml
OCaml version 5.5.1
Enter #help;; for help.

# let ages = [("asha", 31); ("ravi", 27)];;
val ages : (string * int) list = [("asha", 31); ("ravi", 27)]
# let age_or_zero name = match List.assoc name ages with a -> a | exception Not_found -> 0;;
val age_or_zero : string -> int = <fun>
# (age_or_zero "ravi", age_or_zero "meera");;
- : int * int = (27, 0)
# let sum_ages a b = match List.assoc a ages with x -> x + List.assoc b ages | exception Not_found -> -1;;
val sum_ages : string -> string -> int = <fun>
# sum_ages "meera" "ravi";;
- : int = -1
# sum_ages "asha" "meera";;
Exception: Not_found.
# let sum_ages_try a b = try List.assoc a ages + List.assoc b ages with Not_found -> -1;;
val sum_ages_try : string -> string -> int = <fun>
# sum_ages_try "asha" "meera";;
- : int = -1

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

age_or_zero gave (27, 0): Ravi’s age, then 0 for a missing name. sum_ages "meera" "ravi" gave -1, because the first lookup, the value being matched, raised Not_found. But sum_ages "asha" "meera" stopped with Exception: Not_found: the second lookup ran inside the branch, where the exception case does not apply. The try version caught both and returned -1.

That narrowness is the point. A match … with exception handles the failure of one computation and lets errors in the code that uses its result surface, while a broad try around a whole expression can catch an exception the programmer never meant to handle and turn a bug into a silent default.

Key idea

An exception case in a match covers the value being matched and nothing else; reach for try with only when every part of the expression may raise the same exception for the same reason.

Checkpoint

Check: Warnings 8, 11 and 4 · match with exception

  1. Question 1 of 2 Pending is added to type status. label has | Active -> "active" | _ -> "inactive", and can_log_in lists the other three constructors. What does a normal compile report?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Warning 8 for can_log_in, naming Pending, and nothing for label

    Only with -w +4 did the compiler also report label: "this pattern-matching is fragile. It will remain exhaustive when constructors are added to type status."

  2. Question 2 of 2 sum_ages a b = match List.assoc a ages with x -> x + List.assoc b ages | exception Not_found -> -1. Why does sum_ages "asha" "meera" raise Not_found?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The failing lookup is inside the branch, and an exception case covers only the value being matched

    The try version, try List.assoc a ages + List.assoc b ages with Not_found -> -1, returned -1 for the same names, because try covers everything inside it.

Practise: patterns, guards and warnings

The questions use the session’s classify, sign, status and ages with new values and new cases. Decide which case matches, which warning appears, or whether an exception escapes, before you check:

Practice that adapts to you

Practise this lesson

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

  1. Question 1 of 11 What is let classify = function … the same as?

    Choose one answer.

    Show the answer: Question 1 of 11

    Answer: let classify = fun x -> match x with …

    classify and first_two in the session are both written with function, one argument matched against their cases.

  2. Question 2 of 11 Why is function (0, b) | (a, 0) -> 1 | _ -> 0 refused?

    Choose one answer.

    Show the answer: Question 2 of 11

    Answer: Each side of an or-pattern must bind the same variables, and here one binds b and the other a

    Write (0, _) | (_, 0) when the values are not needed, or two separate cases when each side needs its variable.

  3. Question 3 of 11 first_two matches x :: (y :: _ as rest). For the list [1; 2], what are x, y and rest?

    Choose one answer.

    Show the answer: Question 3 of 11

    Answer: x = 1, y = 2 and rest = [2]

    For [7; 8; 9] the session printed "7 then 8; tail 8,9": rest starts at y.

  4. Question 4 of 11 Why does the exhaustiveness check not count guarded cases?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: A guard is an arbitrary expression, so the compiler cannot know when it is true

    Hence the note "(However, some guarded clause may match this value.)" after warning 8 in the session.

  5. Question 5 of 11 match n with n when n > 10 -> "big" | 5 -> "five" | _ -> "other". What do n = 12 and n = 7 give?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: "big" and "other"

    A failed guard sends matching on to the next case, exactly as for (-1, 3) in classify.

  6. Question 6 of 11 A match on a grade has | g when g >= 90 -> "A" | g when g >= 75 -> "B" | g when g < 75 -> "C" and draws warning 8. What is the best fix?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: Replace the last case with | _ -> "C"

    The last case of a chain of guards is usually the "everything else" case; writing it without a guard lets the compiler see that.

  7. Question 7 of 11 What does Warning 11 [redundant-case] mean?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: A case can never be reached, because earlier cases already match every value it could

    The session drew warning 11 for that match and for Some 1 placed after Some _.

  8. Question 8 of 11 g = function Some 0 -> "zero" | Some _ -> "some" | None -> "none" | Some 1 -> "one" draws warning 11. How should it be fixed if Some 1 must give "one"?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: Move | Some 1 -> "one" above | Some _ -> "some"

    Reordering keeps the match exhaustive and makes every case reachable.

  9. Question 9 of 11 Why can a catch-all case be worse than a missing case when a type gains a constructor?

    Choose one answer.

    Show the answer: Question 9 of 11

    Answer: The catch-all handles the new constructor silently with a behaviour nobody chose, while a missing case is reported by warning 8

    For types that will grow, list the constructors explicitly, or enable warning 4 to find the fragile matches.

  10. Question 10 of 11 ages = [("asha", 31); ("ravi", 27)]. What does age_or_zero "meera" return?

    Type a number.

    Show the answer: Question 10 of 11

    Answer: 0

    List.assoc raises Not_found for Meera while computing the value being matched, so the exception case returns 0.

  11. Question 11 of 11 When is match … with exception the better choice over try … with?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: When only the computation being matched may fail that way, and errors in the code that uses its result should not be caught

    A narrow handler fails loudly on the unexpected case; a broad try can turn a real bug into a default value.

Interview questions

Warm-up (2–5 years): why does a match whose guards cover every integer still draw warning 8?

The exhaustiveness check works from the patterns alone. A guard such as when n > 0 is an ordinary expression that could call any function, so the compiler cannot know when it is true, and it counts every guarded case as one that may fail. In match n with 0 -> … | n when n > 0 -> … | n when n < 0 -> … only the pattern 0 is unguarded, so OCaml reports 1 as a possibly unmatched value and adds that some guarded clause may match it. The fix is to make the last case unguarded, such as | _ -> "negative", which the checker can see covers everything left.

Two harder questions follow, about a status match that silently mislabels a new state and about a lookup that hides real errors. Decide what you would check first, then open each first step:

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

  1. Mid to senior

    A Pending state was added to an account status type. The build was clean, yet support found pending accounts shown as "inactive" on the dashboard. The dashboard's label function ends with | _ -> "inactive". Why did no warning appear, and how would you make the next new state impossible to miss?

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

  2. Mid to senior

    A function wraps a whole computation in try … with Not_found -> 0. A bug elsewhere in it, a lookup in the wrong table, now also returns 0 instead of failing, and totals are quietly wrong. How would you restructure the error handling, and why is it safer?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What do or-patterns, as-patterns and guards each add to a match?

    An or-pattern matches either side, an as-pattern binds the whole matched value, and a guard adds a when test after the pattern. Cases are tried from the top until one matches.

  2. Why is | (0, b) | (a, 0) -> … refused?

    Each side of an or-pattern has to bind the same variables. Write | (0, _) | (_, 0) -> …, or two separate cases.

  3. Why do the cases 0, n when n > 0 and n when n < 0 still draw warning 8?

    The exhaustiveness check reads patterns, not guards, so only 0 counts as covered. End the match with an unguarded case such as | _ -> "negative".

  4. What do warnings 8, 11 and 4 each point at?

    8: a value no case matches; 11: a case that can never run; 4 (off by default): a catch-all that will swallow constructors added later.

  5. Why write every constructor instead of _ for a type that will grow?

    Then the next constructor you add makes warning 8 point at each match that has to handle it.

  6. How does a match … with exception case differ from try … with?

    It catches an exception raised while computing the matched value only; try … with catches one from anywhere inside, the branches included.

  7. What does the function keyword do?

    It defines a function that matches its single argument against cases directly, without naming the argument first.

Key takeaways

  • Or-patterns match either side, as-patterns bind the whole matched value, guards add a test after the pattern, and cases are tried from the top until one matches; function matches a function’s single argument.
  • The exhaustiveness check ignores guards, so a guarded match ends with an unguarded case to be provably complete.
  • Warning 8 reports a value no case matches, warning 11 a case that can never run, and warning 4, off by default, a catch-all that will swallow constructors added later.
  • A match … with exception case catches an exception from computing the matched value only, while try … with catches it from anywhere inside, including the branches.

Terms in this lesson

guard
A condition after a pattern, written when expr, that is tested only once the pattern has matched; if it is false, matching goes on to the next case.
or-pattern
A pattern p1 | p2 that matches when either side matches; both sides must bind the same variables.

References

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.