F# Module 4
Modelling data with types interview questions
Interview questions on modelling data with types: short warm-ups, then what each harder question tests, the first step of a good answer and its follow-ups.
Warm-up questions
Short questions with short answers, each answered in full.
-
Fresher to mid
When do you use a tuple and when a record in F#?
Answered in the lesson Tuples, records and anonymous records
-
Fresher to mid
How does a discriminated union differ from an enum in F#, and when do you use each?
-
Fresher to mid
What do the as, OR and AND patterns do in F#? Give an example of each.
-
Fresher to mid
How does a complete active pattern differ from a partial one in F#?
Answered in the lesson Active patterns for readable matching
-
Fresher to mid
How do option and value option differ in F#, and when does each fit?
-
Fresher to mid
What does structural equality mean in F#, and which types have it?
Answered in the lesson Structural equality, comparison and hashing
-
Fresher to mid
What are units of measure in F#, and what do they cost at run time?
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 teammate added a Store record with the same Id and City fields as your Shipment record. Now a file that builds shipments with { Id = …; City = … } fails to compile, although nobody touched it. Why, and how do you make record construction robust in a large codebase?
- What it tests
- How F# infers a record expression's type from its labels and an expected type, why the error appears far from its cause, and which fixes scale beyond one call site.
- First step
- Read the error at the failing line: the expression was inferred as Store, not Shipment. When nothing tells the compiler which type is expected, a record expression takes the most recently declared type that has its labels, and Store now comes later. Fix the line by naming the type once, then ask why two types share every label.
- Follow-ups
-
- Why did the calls that pass the record straight to a function keep compiling?
- Would putting each type in its own module help?
- When is it right for two records to share all their field names?
From the lesson Tuples, records and anonymous records
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
-
Mid to staff
A shared F# library returns anonymous records from its public functions. A minor release added one field to a result, and several services that use the library stopped building. Why did an additive change break them, and how would you design the API instead?
- What it tests
- Knowing that an anonymous record's type is its exact field set, with no structural subtyping, and how to version a public API with named records instead.
- First step
- Find what the failing services do with the result: annotate its type, pass it to their own functions, or compare it with values they build. Each of those names the old field set, and the new value has one field more, so the compiler reports an extra field. Then move the public results to named record types declared in the library.
- Follow-ups
-
- Which consumers kept building, and why?
- How do you add a field to a named record without breaking callers?
- Where do anonymous records still belong in that library?
From the lesson Tuples, records and anonymous records
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
A pricing service keeps 5 million ticks in memory as an array of F# records with three float fields, and its garbage-collection pauses keep growing. Would you make the record a struct? Estimate what changes, and say what you would measure before and after.
- What it tests
- Reasoning about reference and value layouts with numbers, knowing what a struct record costs as well as saves, and measuring allocation and pauses instead of guessing.
- First step
- Estimate both layouts. Each reference record is an object of its own plus an 8-byte reference in the array; a struct record of three floats is 24 bytes stored inline, with no object for the collector to trace. Then measure allocated bytes and pause times on the real service before deciding, and check how the ticks are passed around.
- Follow-ups
-
- How many objects does the collector have to trace in each layout?
- What gets slower when the record becomes a struct?
- What would you try if the record had twelve fields?
From the lesson Tuples, records and anonymous records
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A service reads order states from a database column as an F# enum. Another team added state 7 to the table, and days later a billing job failed with MatchFailureException, far from the code that reads the column. Explain the failure and redesign the boundary so it cannot happen silently.
- What it tests
- Knowing that enums hold any integer and that a complete-looking match fails on unnamed values, and moving the check to the boundary with a union and an explicit error.
- First step
- An enum holds any integer of its underlying type, so 7 became a valid OrderState with no name and travelled until a match without a fallback met it. Convert the column into a union at the edge, with a function that returns Ok for known numbers and Error for the rest, and decide there what an unknown state means.
- Follow-ups
-
- Why did the compiler not catch this?
- Where should the unknown value be reported, and to whom?
- How do you add state 7 properly once its meaning is agreed?
From the lesson Discriminated unions and enums
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to senior
A refund went to the wrong customer because a function took a customer number and an order number, both int, and a caller passed them in the wrong order. How would you use F# types to make this mistake impossible, and what does your fix cost?
- What it tests
- Using single-case unions instead of type abbreviations, constructing them at the edges, and weighing allocation, serialization and interop costs with numbers.
- First step
- A type abbreviation such as type OrderNo = int is only another name for int, so the swapped call compiled. Wrap each id in its own single-case union, type OrderId = OrderId of int, so the compiler refuses a CustomerId where an OrderId is expected, and build the ids once where they enter the program.
- Follow-ups
-
- What does a single-case union cost at run time, and how can you avoid it?
- How do these ids cross into JSON or a database?
- When is a type abbreviation still the right tool?
From the lesson Discriminated unions and enums
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A discount rule reads | Gold spend | Silver spend when spend > 10000m -> spend * 0.1m. A Gold customer who spent 5,000 rupees complains that they got no discount; the author meant the limit for Silver only. Explain the bug and rewrite the rules so that the intent is clear and the compiler still checks them.
- What it tests
- Knowing that a when guard covers every side of an OR pattern, rewriting rules so each guard is local, and keeping the match exhaustive without a wildcard.
- First step
- The guard belongs to the whole rule, so it applies to Gold spend as well as Silver spend, and Gold 5,000 fell through to the catch-all, which gave 0. Split the OR pattern into one rule per case, put the guard only on the Silver rule, and end with rules for the remaining Silver values instead of a wildcard.
- Follow-ups
-
- Why did the compiler not warn about the Gold case?
- How would you test that every tier gets the discount it should?
- When is a guard on an OR pattern exactly what you want?
From the lesson F# pattern matching in depth
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Mid to senior
A settings reader receives values as obj from a .NET library and matches them with :? int, :? string and :? bool, with a fallback rule that uses the default. After a library upgrade, numbers arrive as int64 and every numeric setting quietly takes its default. Diagnose it and design a safer boundary.
- What it tests
- Understanding that type-test patterns test the exact runtime type, why a silent fallback hides the change, and how to convert obj values into F# types once, with errors that say what arrived.
- First step
- A boxed int64 is not an int, so :? int fails and the fallback rule takes the default without a word. Make the fallback report the type it saw, add the numeric types the library can return, and convert every setting into a typed record in one function at the boundary that returns an error for values it does not understand.
- Follow-ups
-
- How would you have noticed the change before users did?
- Should the reader accept int64 and convert it, or reject it?
- Why are type tests a smell inside F# domain code?
From the lesson F# pattern matching in depth
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
An active pattern (|Known|_|) looks a customer up in a database. A match uses it in four rules with different guards, and the database now sees up to four queries per message. Explain why, and restructure the code.
- What it tests
- Knowing that a match may run an active pattern once per rule that tests it, keeping active patterns free of side effects, and moving effects to one call before the match.
- First step
- Each rule that tests Known is a separate test of the pattern, and when a guard fails the next rule runs the pattern again, so every failed rule means another query. Look the customer up once, before the match, and match on the result; keep active patterns pure and cheap, like the functions they are.
- Follow-ups
-
- How would you show the repeated calls before changing anything?
- What if the lookup must stay inside the pattern?
- Which other side effects cause the same trouble?
From the lesson Active patterns for readable matching
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Senior to staff
Your library exposes a union Payment that callers match on directly. You need to split Card into DebitCard and CreditCard without breaking them. How do you use active patterns to change the representation, and how do you move callers over?
- What it tests
- Using active patterns as a stable public view over a private representation, as the F# component design guidelines suggest, and planning a migration that keeps old matches compiling.
- First step
- Make the union's representation private and publish a complete active pattern with the old cases, (|Upi|Card|Cash|), implemented over the new cases, so existing matches keep compiling. Add a second pattern or new functions that expose the split for callers who need it, then deprecate the old view over a release cycle.
- Follow-ups
-
- What do callers lose when they match on an active pattern instead of the union?
- How do callers construct values once the cases are private?
- What is the limit of this approach as the cases grow?
From the lesson Active patterns for readable matching
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Mid to senior
An F# library exposes tryFindCustomer: string -> Customer option. A C# service calls it, reads .Value and crashes with NullReferenceException whenever no customer is found, while every F# test passes. Explain why, and design the API so that both F# and C# callers handle a missing customer safely.
- What it tests
- Knowing that None is stored as null at run time, what a C# caller sees of an F# option, and how to give C# a shape it understands while F# code keeps its options.
- First step
- Check what the C# caller actually receives. F# stores None as null, so for a missing customer the C# variable is null and any member access throws, whatever the F# type says. Then separate the two audiences: keep the option for F# callers, and add a method in a shape C# already handles, such as a TryFindCustomer that returns bool with the customer in an out parameter.
- Follow-ups
-
- Why did the F# tests not catch it?
- How would one library serve both languages without two copies of the logic?
- What changes if the function returned a value option instead?
From the lesson Options, value options and nullness
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
Your team wants F# 9 nullness checking in a service of 180 F# files that calls many .NET libraries. A trial build with <Nullable>enable</Nullable> reports hundreds of FS3261 warnings. How do you roll the checks out without stopping feature work, and which warnings do you fix first?
- What it tests
- Planning an incremental rollout: finding where nulls enter, converting them to options at those boundaries, keeping string | null out of the domain, and stopping new warnings with warnings as errors.
- First step
- Group the warnings by where the value comes from. Most trace back to a few boundaries: .NET calls that may return null, deserialised data and configuration. Convert those values to options where they enter, which clears the warnings downstream, and only then turn FS3261 into an error, project by project, so new code cannot add more.
- Follow-ups
-
- Why does a match on null clear a warning that an if isNull check does not?
- When are nonNull and nullArgCheck the right fix?
- How do you keep the count from growing while the rollout runs?
From the lesson Options, value options and nullness
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A monitoring report counts distinct sensor readings twice, once with List.distinct and once with Set.ofList. Most days the two counts match, but on days when sensors fail, the List.distinct count is higher. Explain the cause and fix the pipeline.
- What it tests
- Knowing that = treats nan as unequal to itself while compare treats two nans as equal, recognising NaN as the value a failed reading produced, and handling it once where the data comes in.
- First step
- Look at what a failed sensor sends. If its reading is NaN, List.distinct, which uses =, keeps every NaN, while Set.ofList, which uses compare, keeps one. Confirm it by counting Double.IsNaN over that day's readings, then decide at the import what a NaN means: dropped, counted as a failure, or kept as one marker.
- Follow-ups
-
- Why do the counts differ only on some days?
- Which other functions in the report depend on NaN in the same way?
- How would you test the fix?
From the lesson Structural equality, comparison and hashing
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A service keeps a disk cache keyed by hash productName so that it can skip work after a restart. In tests every lookup hits the cache, but in production almost every lookup misses after each deployment. Explain why, and design a key that survives restarts.
- What it tests
- Knowing that .NET randomises string hashes per process, that hash codes are not unique, and choosing a stable key such as the text itself or a cryptographic digest.
- First step
- Compare the hash of one product name in two runs: .NET randomises string hashes per process, so after a restart the same name hashes differently and every stored key is stale. The tests wrote and read the cache in one process, which hid it. Key the cache by the name itself, or by a stable digest such as SHA-256 of its UTF-8 bytes.
- Follow-ups
-
- Why did the tests pass?
- Would hash be safe as a key if the strings never changed?
- What does a stable digest cost, and how do you change the key format later?
From the lesson Structural equality, comparison and hashing
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A routing service models distances as float<km>. A partner starts sending miles in the same JSON field. Nothing fails, but fuel estimates are now about 38 % too low. Why did units of measure not catch it, and how would you design the boundary so that it cannot happen silently?
- What it tests
- Knowing that units are erased at run time and that serializers see plain numbers, and designing a contract that carries its units, with checks where the values enter.
- First step
- Units exist only while compiling: the record's float<km> is a Double at run time, and the deserializer put the partner's number into it unchecked, as erased.fsx read 56 into Distance. Put the unit into the contract, as a field name such as distanceKm or a unit field, check plausible ranges on arrival, and convert to float<km> in one boundary function.
- Follow-ups
-
- Where exactly should the check and the conversion live?
- How could the change have been noticed from the data alone?
- What if the partner cannot rename the field?
From the lesson Units of measure
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
You are designing the money types of a payments module that charges in rupees and shows prices in three foreign currencies. Where do units of measure help, where do they not, and which types do you choose?
- What it tests
- Using int64<paise> for exact amounts, units that keep money apart from counts and currencies apart from each other, and naming what units do not check: rounding, rates and formats.
- First step
- Start from what must be exact: rupee amounts in whole paise, int64<paise>, since one rupee is 100 paise. Give every currency its own unit so that rupees never add to dollars, keep exchange rates as dated data with a unit such as INR/USD, and fix one rounding rule that is applied once, at the conversion.
- Follow-ups
-
- What does int64<paise> catch that a plain decimal does not?
- Where can money still go wrong with units in place?
- How do the amounts cross JSON and the database?
From the lesson Units of measure
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes