- rtshkmr's digital garden/
- References/
- Algo Reference/
- 99 OCaml Problems/
- 99 OCaml: Intermediate Exercises/
99 OCaml: Intermediate Exercises
Table of Contents
Intermediate problems taken from here. Instead of just being a store for solutions, this aims to actually go deeper and float all the learning takeaways that I had when completing the problem set. The code-snippets here can be tangled in emacs I will expose this code as an org-file publicly once I’ve completed the 99 problems. It will be hosted on Codeberg, hopefully it helps others. and can be executed directly.

TODO Lists #
1. Flatten a List #
Flatten a nested list structure.
my initial version with slow @ (\(O(n^{2})\) runtime)
This works but we can be better.
| |
Many arm recursion | |
Tree-shaped Recursion #
In this problem we see a nested recursive structure, which feels like a tree / there’s a bifurcation for the Many case. The inner case is what we prioritise first, that’s why it’s in the bracketed recursive call.
We control the direction of the tree traversal by choosing to flatten inner into acc first, then continuing with t.
Lack of Tail Recursion #
In the solution above, there’s an outer and an inner aux call.
The outer aux is in tail position but the inner call aux acc inner is an argument to the outer call — this dependency means that the stack holds a frame for the outer aux while the inner aux resolves — we can get one stack frame per nesting level, so each level of nesting adds a frame.
It’s not linear recursion stack overflow but also, this isn’t tail-recursive.
For linear recursion, accumulator threading achieves full tail-recursion. For tree-shaped recursion, accumulator threading is insufficient — you need an explicit stack or CPS
@-elimination pattern #
Whenever we write (f x) @ acc, we should ask: can I pass acc into f directly?
- If yes, do it — eliminates the allocation and the \(O(n)\) concat.
- Recognition signal:
@on the left operand of a recursive call result.
Explicit Stack for Tail-Recursive Solution \(\implies\) Iterative Tree Traversal Pattern #
| |
The stack is just a list of lists for pending work, there’s no frame-nesting, it’s \(O(1)\) stack — stack is a worklist.
2. Eliminate Duplicates #
Eliminate consecutive duplicates of list elements.
This feels like it’s all about doing cheap lookaheads
| |
| |
The model solution given is not tail-recursive though.
Pattern: 3-armed lookahead skeleton #
A two-element lookahead pattern always requires a 3-arm skeleton:
| [] -> base
| [x] -> last-element base
| x :: ((y :: _) as tl) -> general case3. Pack Consecutive Duplicates #
Pack consecutive duplicates of list elements into sublists.
This is about having sub-pattern name bindings and using the right sub-patterns – it’s opposite of the flattening case.
| |
this works! output:
| a | a | a | a |
|---|---|---|---|
| b | |||
| c | c | ||
| a | a | ||
| d | d | ||
| e | e | e | e |
Gotcha: sub-pattern extraction syntactic sugar: [this] \(\implies\) (this :: []) #
Here’s my flawed attempt
| |
this will error in this ambiguous way:
Line 4, characters 59-60:
4 | | [a :: _ as h] :: acc_t, b :: t -> if Stdlib.(=) a b
^
Error: The value b has type 'a list/2 but an expression was expected of type
'a
The type variable 'a occurs inside 'a list/2
File "_none_", line 1:
Definition of type list/2The problem in my original approach is here.
Our intention is to match the head of acc as a non-empty list and we want to bind the first element of that non-empty list to a.
| pattern | meaning |
|---|---|
| Wrong | So, [pat] \(\implies\) pat :: [] so, [a :: _ as h] desugars to (a :: _ as h) :: [] i.e. a head element that is itself a singleton list whose sole element is a :: _ as h |
| Correct | (a :: _ as h) :: acc_t extracts it the way we want — the () is for the head, which is a list itself and a is the head of that list |
to complete the reasoning for why that type error came up, it’s because the unifier infers teh head of acc as 'a list list (i.e. one element list of lists) which makes acc: 'a list list list, but in the other arm and the call site, the type constraint is different.
aux [[]] ls constrain acc : 'a list list. The unifier attempt to reconcile 'a list = 'a coming from b ::: t on the l side, which is where the occurs-check failure is felt, that’s why the /2 on list
so the diff is just:
| [[]], y :: t -> aux [[y]] t
- | [a :: _ as h] :: acc_t, b :: t -> if Stdlib.(=) a b
+ | (a :: _ as h) :: acc_t, b :: t -> if Stdlib.(=) a b
then aux ((b :: h) :: acc_t) t
else aux ([b] :: acc) t
| _, [] -> List.rev acc in
The general rule: to bind a sub-pattern with a name via as, the grouping delimiter must be ( ), never [ ].
[pat] and (pat) are not interchangeable in match arms. Square brackets always construct or match a list; parentheses are purely grouping. When we write [a :: _ as h] in a pattern we are matching a list-of-lists element, not grouping a :: _ as h.
This is the same asymmetry that bites people in expressions: [1 + 2] is a singleton list containing 3, not the same as (1 + 2).
Pattern: Separating Current-Group from Completed-Group Accumulator #
This pattern is seen in the model solution, but I’m not too sold on the notion that it’s better, though it’s slightly cleaner to read.
consider the model solution:
| |
In my approach, I had embedded the in-progress group as the head of the acc, so the acc did double duty of holding the current and completed groups — this meant that I had to use [[]] as a sentinel, which demanded its own arm | [[]], y :: t just for the initialisation artefact.
In the model solution, the current group and the completed groups accumulator are separated which buys us cleaner arms.
4. Decode a Run-Length Encoded List #
Given a run-length code list generated as specified in the previous problem, construct its uncompressed version.
This is essentially a counted repetition.
| |
My solution does a lot of packing, unpacking as well.
| a | a | a | a | b | c | c | a | a | d | e | e | e | e |
The model solution splits it up into two helper functions. It’s clean and likely better performance also. W.r.t performance it’s because of the
Pattern: Prefer dedicated helper when unwinding a counted repetition #
When unwinding a counted repetition, prefer a dedicated helper with the count as a plain integer parameter over re-encoding the count back into the algebraic type on each step. The type is the right representation for the data structure; it is not the right representation for a loop counter.
5. Run-Length Encoding of a List (Direct Solution) #
Implement the so-called run-length encoding data compression method directly.
I.e. don’t explicitly create the sublists containing the duplicates, as in problem “Pack consecutive duplicates of list elements into sublists”, but only count them.
As in problem “Modified run-length encoding”, simplify the result list by replacing the singleton lists (1 X) by X.
| |
| |
The model’s rle counts from 0 (representing “seen one so far, count additional occurrences”), which lets it initialize aux 0 [] list and pass the full list in. My make counts from 1 (representing “actual count of elements seen”), which forces the let h :: t extraction to seed aux (1, h) [] t.
Both are internally consistent; the model’s convention is slightly more brittle to misread (count = 0 means one element, not zero), mine is more natural to reason about. Neither is wrong.
6. Replicate the Elements of a List a Given Number of Times #
Replicate the elements of a list a given number of times.
| |
Improvements:
- it’s not a good idea to couple the
(c, e)as a tuple because it means every recursive call reconstructs the tuple even when only one field changes. We can separate them out and get our intent explicit:let rec aux acc c e src = match (c, src) with ... - my solution is slightly less readable because:
- the 3rd arm
| i, _ ->matchessrc = []whenc > 0, which is correct but not super obvious - a reader would have to convince themselves that
ceventually hits0and then| 0, []fires - in comparison, the prepend function in the model answer makes everything explicit
- the 3rd arm
| |
List.rev list is what makes our aux tail-recursive.7. Drop Every N’th Element From a List #
Drop every N’th element from a list.
| |
For extra clarity, the counter could have been counted-up-to instead of a downcount — would mimic the question’s context better.
| |
The model answer is not tail recursive. I prefer my solution.
8. Extract a Slice From a List #
Given two indices,
iandk, the slice is the list containing the elements between thei‘th andk‘th element of the original list (both limits included).Start counting the elements with 0 (this is the way the List module numbers elements).
Everything is zero-indexed.
| |
| |
Careful on the edge cases #
I’ve been writing solutions that aren’t defensive enough. For example, my pedestrian approach to this used a down-counter but it stopped at i = 1 — so input where i = 0 would break the solution.
Part of this is just because I’ve been too focused on making the code short and recursive functions be tail-recursive.
Careful / Style: wrap nested pattern matches with parentheses #
This is just good style for now, the parentheses are fine to be omitted from below because it’s the last arm, but if there was another arm then it would all be consumed at the same time.
let rec drop i src = match i with
| 0 -> src
| _ -> (match src with
| [] -> src
| _ :: t -> drop (i - 1) t)naturally, there’s other ways to do this — such as just flattening the nesting:
let rec drop i src = match (i, src) with
| 0, _ | _, [] -> src
| _, _ :: t -> drop (i - 1) t9. Rotate a List N Places to the Left #
Rotate a list
Nplaces to the left.
I naturally feel inclined towards a pipeline pattern of data transformations. Wonder if that is idiomatic / what other patterns and idioms may apply here.
my pedestrian solution, not defensive enough + its improvement
| |
Pedestrian solution doesn’t even do the modulo for the n-value, that’s important.
| |
| |
This isn’t too great yet, I wish it would have been more pipeline-like.
| |
Math: modulo fix #
- let n_mod = n mod (List.length l) in
+ let n_mod = if n = 0
+ then 0
+ else ( ( n mod len ) + len ) mod len
n mod (List.length l) handles n > len but breaks on negative n. The model ans uses (n mod len + len) mod len which handles negative rotations correctly — rotating left by -1 is rotating right by 1. The extra + len absorbs the negative residue.
Also: List.length [] is 0, and n mod 0 raises Division_by_zero. The model solution therefore guards with if len = 0 then 0.
10. Extract a Given Number of Randomly Selected Elements From a List #
The selected items shall be returned in a list. We use the Random module and initialise it with
Random.init 0at the start of the function for reproducibility and validate the solution. To make the function truly random, however, one should remove the call toRandom.init 0.
| |
mistakes made in initial attempt
| |
Problems with this:
[bug because of sentinel] the init call for
aux_takeuses 0 as the sentinel, which is why the type unifier complains about the type mismatch.acc_targetis supposed to be generic ('a) instead of being concrete asint.possible solution: option-wrap this instead of using a bad sentinel
model solution uses
@which is non-tail recursive onaccand we should stick to tail-recursive approaches by usingList.rev_appendlet rec aux_take i acc_rest = function | [] -> raise Not_found | h :: t -> if i = 0 then (h, List.rev_append acc_rest t) else aux_take (i - 1) (h :: acc_rest) tCode Snippet 26: UsingList.rev_appendto keep things tail-recursiveWe don’t have early returns here on finding the
chosen_i.
Line 2, characters 13-16:
2 | rand_select ["a"; "b"; "c"; "d"; "e"; "f"; "g"; "h"] 3;;
^^^
Error: This constant has type string/2 but an expression was expected of type
int/2
File "_none_", line 1:
Definition of type int/2
File "_none_", line 1:
Definition of type string/2 | |
The model solution does things differently here:
the model ans does silent-capping instead of exception throwing
it doesn’t raise an error, it just guards against it at the call-site
the model calls
aux (min n len) []\(\rightarrow\) ifn > lenthen it silently caps the extraction at the list length rather than raisingNot_foundTechnically, whether the silent wrapping is better of exception throwing is better should be spec-dependent — for me the more explicit one is better.
the model threads the length through the function calls so there’s only a single call to
List.lengthvs my approach that calls length at each iteration — effectively \(O(n^{2})\)
Caution: Avoid the Redundant Double Exit Pattern #
From the first version below, we have two arms with exits (line 3 and 4). We can just do it in the style of the second version, where the pattern matching has a single exit site (line 12).
| |
11. Generate the Combinations of K Distinct Objects Chosen From the N Elements of a List #
Generate the combinations of K distinct objects chosen from the N elements of a list.
In how many ways can a committee of 3 be chosen from a group of 12 people? We all know that there are C(12,3) = 220 possibilities (C(N,K) denotes the well-known binomial coefficients). For pure mathematicians, this result may be great. But we want to really generate all the possibilities in a list.
So doing this using a mutable style feels so much simpler than doing this on immutable, recursive style. Must be a change in ideology that I need here. My mind immediately goes into a 1/0 Knapsack Algo approach, but that’s best done in a mutable fashion.
The pedestrian recursive solution is a decision-tree traversal.
my pedestrian solution
| |
Also, technically, my version has redundant all_combis that get threaded through because we end up doing the @ combination anyway.
| |
This works but my solution is also not tail recursive. It’s closer to the imperative 0/1 knapsack mental model, that’s why it felt natural.
The model solution adopts the typical recursive approach to this, however, it doesnt seem to be tail-recursive.
I’m compelled to look for a tail-recursive solution.
Tree Traversal Recursion #
This question traverses a decision-tree. The decision-trees are the same in mine vs model solution.
For the model solution, the with_h case does a List.map which prepends the head to all the inner-recursed answers (here).
Here, we shouldn’t be pursuing tail-recursion. If we analyse the complexity of this approach, we see that the recursion depth is limited by min r (List.length l) — i.e. it’s at most r deep, which for combination generation, is always small. The with_h @ without_h at the end of each frame is not avoidable because we need both subtrees before merging them.
Complexity Analysis
with_h @ without_his \(O ( | with\_h | )\) @ every node.When we’re focused on combination generation, this is an inherent — we’re building a result of size
C(n, k)so we need to somehow assemble it.Unless we use some sort of mutable approach, neither mine nor the model version can be improved on here.
Style: Interface design and recursive outer function vs non-recursive initialiser #
Consider the two approaches above between mine vs the model solution. Their shapes look different:
(* --- shape of the model ans: recursive outer function *)
let rec extract k list = ...
(* --- shape of my ans: non-recursive initialiser *)
let extract r l =
let rec build_combi curr_len curr_combi = function ...
in
build_combi 0 [] lIt’s a matter of style here.
| Style | When it’s a good idea |
|---|---|
| model ans style: recursive outer fn | When the recursive structure maps cleanly onto the public parameters — i.e. when the function’s external contract and its internal recursion use the same state — the model’s style is strictly cleaner. extract k list recurses on exactly k and list, nothing else. No auxiliary state is needed. Making the outer function recursive directly exposes that the problem has no initialisation gap. It also forces us to think about whether our function actually needs hidden state. If it doesn’t, the aux wrapper is ceremony. |
| my style: non-recursive initialiser | When the recursion needs state that callers should not supply — accumulators, counters, working lists — the aux pattern is mandatory. Exposing build_combi 0 [] l as the public interface would be a leaky abstraction: callers could pass arbitrary initial state and break invariants. The outer wrapper seals that off. This style also gives the helper a meaningful name. build_combi communicates intent; aux is generic but at least signals “implementation detail.” Anonymous recursion via let rec f = f gives us nothing to grep for. |
The general rule: prefer the recursive-outer style when there is no initialisation gap.
Use the explicit aux when there is one.
The tell is whether we’d be embarrassed to expose the full recursive signature as the public API. If yes — wrap it.
12. Group the Elements of a Set Into Disjoint Subsets #
Group the elements of a set into disjoint subsets
- In how many ways can a group of 9 people work in 3 disjoint subgroups of 2, 3 and 4 persons? Write a function that generates all the possibilities and returns them in a list.
- Generalize the above function in a way that we can specify a list of group sizes and the function will return a list of groups.
| |
13. Sorting a List of Lists According to Length of Sublists #
Sorting a list of lists according to length of sublists.
We suppose that a list contains elements that are lists themselves. The objective is to sort the elements of this list according to their length. E.g. short lists first, longer lists later, or vice versa.
Again, we suppose that a list contains elements that are lists themselves. But this time the objective is to sort the elements of this list according to their length frequency; i.e., in the default, where sorting is done ascendingly, lists with rare lengths are placed first, others with a more frequent length come later.
| |
TODO Arithmetic #
14. Determine Whether a Given Integer Number Is Prime #
Determine whether a given integer number is prime.
| |
15. Determine the Greatest Common Divisor of Two Positive Integer Numbers #
Determine the greatest common divisor of two positive integer numbers.
Use Euclid’s algorithm.
my v0 GCD solution (works, but inefficient)
| |
| |
| |
abs only one time on entry (negative handling), recursion is purely arithmetic16. Calculate Euler’s Totient Function \(\phi(m)\) #
Euler’s so-called totient function \(\phi(m)\) is defined as the number of positive integers
r(1 ≤ r < m) that are coprime tom. We let \(\phi(1)\) = 1.Find out what the value of \(\phi(m)\) is if
mis a prime number.Euler’s totient function plays an important role in one of the most widely used public key cryptography methods (RSA). In this exercise you should use the most primitive method to calculate this function (there are smarter ways that we shall discuss later).
| |
| |
17. Determine the Prime Factors of a Given Positive Integer #
Construct a flat list containing the prime factors in ascending order.
| |
18. Determine the Prime Factors of a Given Positive Integer (2) #
Construct a list containing the prime factors and their multiplicity.
Hint: The problem is similar to problem Run-length encoding of a list (direct solution).
| |
19. Calculate Euler’s Totient Function \(\phi(m)\) (Improved) #
See problem “Calculate Euler’s totient function \(\phi(m)\)” for the definition of Euler’s totient function.
If the list of the prime factors of a number
mis known in the form of the previous problem then the functionphi(m)can be efficiently calculated as follows: Let[(p1, m1); (p2, m2); (p3, m3); ...]be the list of prime factors (and their multiplicities) of a given numberm. Then \(\phi(m)\) can be calculated with the following formula:\[ \phi(m) \newline = (p1 - 1) \times p1^{m1 - 1} \times (p2 - 1) \times p2^{m2 - 1} \times (p3 - 1) \times p3^{m3 - 1} \times … \]
| |
20. Goldbach’s Conjecture #
Goldbach’s conjecture says that every positive even number greater than 2 is the sum of two prime numbers. Example: 28 = 5 + 23. It is one of the most famous facts in number theory that has not been proved to be correct in the general case. It has been numerically confirmed up to very large numbers. Write a function to find the two prime numbers that sum up to a given even integer.
| |
21. A List of Goldbach Compositions #
Given a range of integers by its lower and upper limit, print a list of all even numbers and their Goldbach composition.
In most cases, if an even number is written as the sum of two prime numbers, one of them is very small. Very rarely, the primes are both bigger than say 50. Try to find out how many such cases there are in the range 2..3000.
| |