OCaml Module 2
Expressions, values and functions interview questions
Interview questions on expressions, values and functions: warm-ups, then what each harder question tests and how a good answer starts.
Warm-up questions
Short questions with short answers, each answered in full.
-
Fresher to mid
What does shadowing mean in OCaml?
Answered in the lesson Expressions, let bindings, if and scope
-
Fresher to mid
Why does 3 + 2.0 not compile in OCaml?
-
Fresher to mid
Why can "abc" == "abc" be false while "abc" = "abc" is true?
Answered in the lesson Booleans, characters, strings and bytes
-
Fresher to mid
What is currying in OCaml, and what does it let you do?
Answered in the lesson Functions, currying and the pipeline operator
-
Fresher to mid
What does a type such as 'a -> 'a mean, and who writes it?
Answered in the lesson Type inference, polymorphism and type errors
-
Fresher to mid
What do ~ and ? mean in front of an OCaml argument?
Questions with model answers
What each question tests, the first step of a good answer and the follow-ups to expect.
-
Mid to senior
A helper is called as log (read_user ()) (read_amount ()), and both functions print a prompt. In the native build the amount prompt appears first. Why, and how would you make the order dependable on every compiler?
- What it tests
- Knowing that OCaml leaves the evaluation order of function arguments unspecified, and fixing an order with let … in instead of relying on what one compiler happens to do.
- First step
- Recall that the manual promises no order for the arguments of an application; OCaml 5.5.1 evaluated them right to left, both native and bytecode, so read_amount ran first. Then bind each call with its own let … in, in the order you need.
- Follow-ups
-
- Would building to bytecode instead change anything?
- Do tuples and list literals have the same problem?
- How would you spot this pattern in a code review?
From the lesson Expressions, let bindings, if and scope
Model answer, Pro. Model answers open with any pass; prices are in the notice on this page.Opening the model answers with your pass…
Model answers open with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so the model answers can’t be opened at the moment. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to mid
A line such as save record; flush () ran in the toplevel with only a warning, but dune build stops on it with Error (warning 10 [non-unit-statement]). save returns the number of bytes written. What does the error mean, and how would you fix it?
- What it tests
- Understanding that the left side of ; should be unit, that dune's development profile turns warnings into errors, and choosing between using the value and ignore.
- First step
- Read the warning: the expression on the left of ; returned a value that nobody uses, here the int from save. The toplevel only warns, but dune's default development profile makes warnings errors, so the build stops.
- Follow-ups
-
- When is ignore the right fix, and when is it wrong?
- Why does the toplevel accept what dune refuses?
- Could the release profile hide the problem?
From the lesson Expressions, let bindings, if and scope
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A tool adds up file sizes in bytes with plain ints. The native build is right, but the same code built with wasm_of_ocaml for a browser page shows a negative total once the files pass about one gigabyte. Explain what happens and what you would change.
- What it tests
- Knowing that int width depends on the target (63 bits native, 32 under js_of_ocaml, 31 under wasm_of_ocaml), that int arithmetic wraps silently, and choosing Int64 when the range matters.
- First step
- Print Sys.int_size and max_int in each build: native code gives 63 bits, wasm_of_ocaml 31 bits with max_int 1,073,741,823, about one gibibyte. A total above that wraps around to a negative number without any error.
- Follow-ups
-
- Which type would you use instead, and what does it cost?
- How would a test catch this before release?
- Would js_of_ocaml have the same limit?
From the lesson Integers, floats and arithmetic
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Fresher to mid
You receive prices as strings, such as "120", "99" and "abc", and must return their average. Describe an OCaml function for it: how it parses, what it does with bad or empty input, and how it divides.
- What it tests
- Parsing with the _opt functions instead of exceptions, returning an option for empty or invalid input, and converting explicitly from int to float before dividing.
- First step
- Choose the result type first: float option, None when there is no valid price. Parse each string with int_of_string_opt, which returns None for "abc" instead of raising Failure, and decide whether one bad price rejects the whole list or is skipped.
- Follow-ups
-
- Why not use int_of_string and catch the exception?
- What would integer division give for the average of 120 and 99?
- How do you handle a very long list of large prices?
From the lesson Integers, floats and arithmetic
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Mid to senior
A job builds a large CSV export by appending each row to a string with ^ inside a loop, and it slows down sharply as the export grows. Explain why, and how you would rewrite it.
- What it tests
- Knowing that strings are immutable, so each ^ copies both sides and the loop does quadratic work, and that Buffer appends in amortised constant time.
- First step
- Each ^ allocates a new string and copies everything built so far, because strings cannot change. Appending n rows therefore copies about n²/2 rows in total. A Buffer grows in place and copies only when it doubles its capacity.
- Follow-ups
-
- How much copying does the ^ loop do for 10,000 rows of 50 bytes?
- Would String.concat on a list of rows work as well?
- When would you write rows straight to a file instead?
From the lesson Booleans, characters, strings and bytes
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Mid to senior
A module defines let rate = 0.02 and let fee amount = amount *. rate. A colleague adds let rate = 0.03 further down the same file to raise the fee, but fee still charges 2 %. Explain why, and how you would make the rate changeable on purpose.
- What it tests
- Knowing that a closure keeps the bindings that existed when it was made, that a later let shadows instead of assigning, and choosing between a parameter, partial application and a ref.
- First step
- The second let made a new binding called rate; it did not change the first one. fee was defined while rate meant 0.02, and its closure keeps that binding, exactly as the lesson's tax kept the rate 18. after rate was bound to 5.
- Follow-ups
-
- Why not just use a ref for the rate?
- How would partial application help here?
- How would you test that the fee uses the rate you expect?
From the lesson Functions, currying and the pipeline operator
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A teammate defines let ( @- ) a b = a - b to subtract money amounts, and a report computes 100 @- 30 @- 20 as 90 where everyone expected 50. What happened, and how would you choose and test the name of a custom operator?
- What it tests
- Knowing that an operator's first character puts it in a group of the manual's precedence table, which fixes its associativity, and testing a chain of three operands.
- First step
- Look up the group of operators that start with @ in the manual's table: it groups to the right, together with ^. So the expression means 100 @- (30 @- 20), which is 100 - 10 = 90.
- Follow-ups
-
- Which first character would you choose instead, and why?
- Should money code have custom operators at all?
- How would you protect existing call sites while renaming the operator?
From the lesson Functions, currying and the pipeline operator
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A file defines let cache = ref [] at the top level. The same line worked in the toplevel, but ocamlc now refuses the file with an error about non-generalizable type variables. Explain the error and two ways to fix it.
- What it tests
- Understanding the value restriction and weak type variables, why a compiled module needs complete types, and fixing it with an annotation or by changing the definition.
- First step
- ref [] is the result of a function call, so its type variable is not generalised: it is '_weak1 list ref, one element type not known yet. The toplevel can wait for the first use to decide it, but a compiled module's interface must be complete, so ocamlc refuses it.
- Follow-ups
-
- Why is ref [] not given the type 'a list ref?
- What changes if the module has an .mli file?
- When would you make the cache a function instead?
From the lesson Type inference, polymorphism and type errors
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to mid
A colleague's build fails with "This expression has type int but an expression was expected of type float", underlining a recursive call that looks right. How would you find the real mistake, and how could the code keep such errors closer to their cause?
- What it tests
- Knowing that a type error is reported where two types first met, tracing the actual type back to the expression that fixed it, and using annotations to move the message to the cause.
- First step
- Ask where the int came from. The underlined call returns whatever the function returns, so look at the other branches: a base case written 0 instead of 0. fixes the result type to int before the recursive call is checked.
- Follow-ups
-
- Why did the checker not report the 0 itself?
- Where exactly would you add annotations, and why there?
- Do annotations change the compiled program?
From the lesson Type inference, polymorphism and type errors
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A helper is defined as let log ~msg ?(level = "info") = print_endline (level ^ ": " ^ msg). Calls such as log ~msg:"saved"; compile with warnings in the toplevel, but nothing is ever printed. What is wrong, and how do you fix the helper and its callers?
- What it tests
- Knowing that an optional argument is erased only when a later positional argument is applied, reading warnings 16 and 5, and adding a final unit parameter.
- First step
- Read the warning at the definition: warning 16, this optional argument cannot be erased. Nothing positional follows ?level, so log ~msg:"saved" is a partial application that returns a function waiting for ?level, and print_endline never runs.
- Follow-ups
-
- Which warning appears at each call site, and what does it say?
- Why does a final () fix it?
- How do you update the callers safely?
From the lesson Labelled and optional arguments
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
Your library exports resize : int -> int -> image -> image, taking width and height, and users keep swapping the two numbers. You want labels, resize ~width ~height img, without breaking every caller on one day. How would you plan the change?
- What it tests
- Knowing what labels change in a type, which calls keep compiling (complete calls, with warning 6) and which break (partial applications, functions passed to higher-order functions), and planning a gradual migration.
- First step
- List how callers use resize today. A complete call, resize 640 480 img, still compiles after labels are added, with warning 6 for omitted labels, while partial applications such as resize 640 and uses passed to List.map break, because the type has changed.
- Follow-ups
-
- Why keep the image argument unlabelled?
- How do you find every call site that must change?
- Would you keep the old function, and for how long?
From the lesson Labelled and optional arguments
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes