99 OCaml: Beginner Exercises
Table of Contents
Problems for beginners 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.

Lists #
The exercises are grouped by classic data-structures. Naturally, List is the one we start with to build our foundation.
1. Tail of a List #
Write a function
last : 'a list -> 'a optionthat returns the last element of a list
| |
This is a classic 3-pattern, remember the use of cons for the head-tail separation.
Tail-Recursive, Accumulator Pattern, Continuation Passing Style #
Keeping things tail-recursive
The rule of thumb:
A recursive call is tail-recursive if and only if nothing happens after the recursive call returns. The result must be returned directly — no wrapping, no arithmetic, no cons-ing.
Negative example — reversing a list naively:
(reverse tail) @ [head]The@happens after the recursive return → not tail-recursive → stack-overflows on long lists.
Accumulator Pattern
For functions that are naturally not tail-recursive, introduce an accumulator parameter and pass the growing result forward rather than building it on the return path.
1 2 3 4 5 6 7 8 9(* non-tail-recursive: addition happens after the recursive return *) let rec sum = function | [] -> 0 | x :: rest -> x + sum rest (* tail-recursive: accumulator carries result forward *) let rec sum_tr acc = function | [] -> acc | x :: rest -> sum_tr (acc + x) restCode Snippet 1: tail-recursive positive and negative examplesThe accumulator pattern is defunctionalised CPS — it’s what we get when the continuation is simple enough to collapse into a single state variable.
Continuation Passing Style (CPS)
The core shift: instead of returning a value, we receive the rest of the computation as a function (
k) and decide how to invoke it.Style Shape Direct let y = f x in g yCPS f x (fun y -> g y k)1 2 3 4 5(* CPS sum: continuation k receives the final result *) let rec sum_cps lst k = match lst with | [] -> k 0 | x :: rest -> sum_cps rest (fun r -> k (x + r))Code Snippet 2: cps-style exampleWhat CPS actually buys us (in order of practical importance):
Tail recursion universally. Every call becomes a tail call — no hidden stack growth. This is why compilers use CPS internally.
First-class control flow. “The rest of the program” is a function we hold. We can pass it, store it, call it multiple times, or not at all. This unlocks:
- early exit without language support
- backtracking / search (pass a
k_successandk_failure) - exception handling without runtime magic
- coroutines, async, generators
Explicit, programmable execution order. In direct style the runtime controls when things return and how the stack unwinds. In CPS, we control all of that.
The cost:
- Readability drops. Nested lambdas, inverted flow — objectively harder to read.
- Closure allocation. Each continuation is a heap-allocated closure → GC pressure.
- Debugging is harder. Stack traces flatten; “who called who” disappears – all-too-familiar callback-hell
- Usually not idiomatic OCaml. A simple accumulator handles 90% of cases cleanly.
When CPS is actually worth it:
- Complex early exits across multiple recursion layers
- Backtracking / search (parsers, SAT solvers, Prolog-style)
- Writing interpreters or compilers (explicit evaluation order)
- Async / cooperative multitasking (deferred continuations)
When to use the accumulator instead:
- Any time the continuation is “add this to the running total” or similarly simple — collapse it into state, don’t heap-allocate a closure.
The deeper reframe: A function doesn’t return a value. It receives the rest of the computation and decides how to proceed. Once we see it that way: exceptions = alternate continuations, loops = recursive continuations, async = deferred continuations.
Relationship between the three:
- Direct recursion: implicit stack, not tail-recursive
- Accumulator: explicit state, tail-recursive, works when continuation is simple
- CPS: explicit continuation, tail-recursive, works for arbitrary control flow
- Accumulator = CPS with the continuation defunctionalised into a variable
2. Last Two Elements of a List #
Find the last two (last and penultimate) elements of a list.
| |
The list literal pattern [x; y] is sugar for x :: y :: []. When we need to look ahead by k elements, we write k binders in the terminal arm.
This generalises cleanly to “last three”, “last four”, etc. — same skeleton, wider terminal arm.
3. N’th Element of a List #
Find the N’th element of a list.
| |
| |
my implementation (working, less clean)
| |
Singleton matches cons pattern #
My attempt had some redundancies:
| (0, [x]) -> Some xgets subsumed into| (0, x :: _) -> Some x- this is because a singleton (i.e.
[x]) will match the pattern: =x: _=
- this is because a singleton (i.e.
Balancing Guards #
The idiomatic OCaml preference: Encode constraints in the structure of the pattern (the shape of the data), not in guards.
Reach for guards only when the constraint is genuinely arithmetic or relational — something the type system cannot express structurally.
Reminders #
- the negative input case needs to be handled
4. Length of a List #
Find the number of elements of a list.
OCaml standard library has List.length but we ask that you re-implement it. Bonus for a tail recursive solution.
Solutions:
| |
This is the most idiomatic version. It keeps the name space clean by making the aux helper function scoped to the same function.
It is tail-recursive based on our elaboration above on what tail-recursive means.
| |
I prefer this solution.
my initial solution
I have a recursive helper and a wrapper that calls it and abides by the API that the input example suggests.
| |
this can be improved: the scope of the recursive helper may JUST be the wrapper itself – so we should nest it.
5. Reverse a List #
Reverse a list.
OCaml standard library has List.rev but we ask that you re implement it.
| |
Accumulator is the reversed prefix #
The mental model to lock in:
When reversing with an accumulator, the accumulator is the reversed prefix so far.
Each step takes the head of the remaining list and prepends it to that reversed prefix.
Toplevel evaluation is all-or-nothing #
If a let binding fails to typecheck, it is not defined at all. Subsequent errors (like “unbound value”) are often downstream symptoms, not the root cause.
This feels like a REPL-quirk where in the broken snippet below:
| |
On the cons operator direction: a :: b always means “element on the left, list on the right.”
The type is 'a -> 'a list -> 'a list.
If we ever find ourselves writing list :: element, that’s the tell that we’ve flipped it.
6. Palindrome #
Find out whether a list is a palindrome.
Hint: A palindrome is its own reverse.
| |
My initial attempt
So this is a little naive, I attempted to do two checks.
It seems like it’s possible for us to just use pattern-matching (see ideal solution above).
| |
Structural = vs Physical Inequality == #
| equality | |
|---|---|
structural (=) | Compares values recursively. Works on lists, tuples, variants, records. This is what we want almost always. |
physical (==) | Compares memory addresses — are these the exact same object in the heap? Almost never what we want for value comparison. |
Implicit behaviour:
==may forceintwhen ambiguousIn the example below, we manually implement the
cmpfunction but there’s an area that implicitly type-restricts the arguments toint:ls =[ ]= \(\implies\) tries to make the type concrete and make it anint list
Negative example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17let pos_input = ["x"; "a"; "m"; "a"; "x"];; let neg_input = ["a"; "b"] let is_palindrome ls = let rev ls = let rec aux ls acc = match ls with | [] -> acc | hd :: tl -> aux tl (hd :: acc) in aux ls [] in let rec cmp a b = match (a, b) with | [], [] -> true | h_a :: t_a, h_b :: t_b -> if h_a = h_b then cmp t_a t_b else false in if ls == [] then false else cmp ls (rev ls);; is_palindrome pos_inputLine 2, characters 14-23: 2 | is_palindrome pos_input;; ^^^^^^^^^ Error: The value pos_input has type string/2 list/2 but an expression was expected of type int/2 list/2 Type string/2 is not compatible with type int/2 File "_none_", line 1: Definition of type int/2 File "_none_", line 1: Definition of type list/2 File "_none_", line 1: Definition of type string/2
Language Provides the Structural Equality Logic
The
=recursively walks the data structure and compares element by element. For[1;2;3] = [1;2;3], the runtime traverses both lists in lockstep, comparing heads, recursing on tails, returning true only if every element matches and both lists terminate simultaneously. We don’t write that logic because the language provides it generically for any type.
toplevel quirks:
BasevsStdlibequality gotchaSo in my
utop, I’ve originally defaulted to JaneStreet’sBase, where the=differs fromStdlib.(=). This is an annoying gotcha.Jane Street’s
Baselibrary replaces polymorphic(=)with a version that requires explicit type-specific comparators. If utop is loaded withBase, writingls = List.rev lsfails becauseBase(=)won’t accept'a listwithout a compactor.Fix: use
Stdlib.()= explicitly to get OCaml’s built-in structural equality.let is_palindrome ls = Stdlib.() ls (List.rev ls)=This only surfaces in a
Base-defaulted utop session. In a normal.mlfile withoutopen Base, plain()= works fine.
7. Run-Length Encoding #
Run-length encoding (RLE) is a form of lossless data compression in which runs of data (consecutive occurrences of the same data value) are stored as a single occurrence of that data value and a count of its consecutive occurrences, rather than as the original run. For example, a sequence of “green green green green green” in an image built up from colored dots could be shortened to “green x 5”.
# encode ["a"; "a"; "a"; "a"; "b"; "c"; "c"; "a"; "a"; "d"; "e"; "e"; "e"; "e"];;
- : (int * string) list =
[(4, "a"); (1, "b"); (2, "c"); (2, "a"); (1, "d"); (4, "e")] | |
The official answer:
an alternative, official answer that uses pack
| |
Idiom: Accumulator head as “current working state” #
This is a general idiom, where the in-process group being built (the (freq, symbol) :: a_tl, hd :: tl pattern arm) has the accumulator’s head within it.
The accumulator is doing 2 things here:
- the completed-groups history
- the live group at the front
We’ll see this idiom show up again when doing grouping problems, run-detection, segmentation over a list
Recognition signal for this idiom: when we need to “look at the last thing we committed” while processing the next element.
Idiom: as binding for peek/lookahead pattern-matching #
The as binding we see in the model ans (ref) – b :: _ as t allows us to:
- lookahead at the structure of the tail
t, and still have access to its head,b - we’re doing a next-element peek, we don’t need to reconstruct
tagain when we pass the tail to the recursive call .
The pattern | a :: (b :: _ as t) -> if a = b then aux ... t else aux ... t is the idiom here \(\implies\) OCaml lookahead
Best for whenever we need to compare the current element to the next one without consuming the next, this is the shape.
We see a similar idiom later, where using such as binding helps us avoid unnecessary \(O(k)\) list constructions.
Cost Trade-offs and why pre-pending + List.rev @ the end is correct #
- We have a trade-off where at each step, our prepend happens @ \(O(1)\) and then we do a
List.revthat runs at \(O(n)\) - We shouldn’t be tempted and:
- append at front
accum @ [(1, hd)]to avoid the final reverse because that append happens at \(O(n)\) per step \(\implies\) the whole function would end up being \(O(n^{2})\)
- append at front
Using sentinel values #
We could attempt to use a sentinel that would generalise the init arm into the general case arm – it’s like instead of saying “if no current group, start one”, we say “pretend there’s always a current group, even before the first element” – and that “pretend” value is the sentinel.
this is awkward because the “initialisation” has just been shifted to outside the loop – which is the practical equivalent of a sentinel
| |
Though I don’t really like the use of runtime failwith here.
failwith "unreachable" is a code smell #
It converts a static guarantee into a runtime assertion.
The compiler cannot verify it. Refactoring can silently invalidate it. If we ever refactor the match structure and our assertion becomes wrong, we get a runtime crash instead of a compile error.
When we reach for it, treat it as a signal to restructure the match so the impossible arm simply doesn’t exist.
Design lession: the pack decomposition #
The pack decomposition is a design lesson.
The pack-then-map approach reveals an important learning:
encode is actually two separable concerns
grouping (pack) and
summarising each group (map over length + head).
Our single-pass solution fuses these, which is more efficient but less composable.
The pack version is:
- easier to test
- easier to modify (what if you wanted the groups themselves, not the counts?),
- easier to reason about
The tradeoff is one extra \(O(n)\) pass.
The general design principle to learn:
When a problem has two natural phases, then implementing them separately and composing them is often cleaner than fusing. Fuse only when you have a measured performance reason.
8. Modified Run-Length Encoding #
Modify the result of the previous problem in such a way that if an element has no duplicates it is simply copied into the result list. Only elements with duplicates are transferred as (N E) lists.
Since OCaml lists are homogeneous, one needs to define a type to hold both single elements and sub-lists.
We wish to make a modification. The beauty of the previous model answer is that we don’t make the struct until we know (based on the lookahead) that the upcoming value will be different. This deferred construction is useful for us because it means that we can just focus on construction and not any kind of immutable update. To construct a custom variant type, we just call a dedicated constructor.
| |
Using variant type as an output discriminator #
Our output type is heterogeneous by construction: some elements are singletons, some are pairs, and you need the type system to enforce that distinction rather than just convention.
Here, make_rle centralises the count = 1 decision. If we change what “single” means, it’s at that boundary.
The idiom becomes:
When output elements differ structurally based on runtime data, define a variant type.
Don’t use tuples with sentinel values (like
(1, x)for singles vs(n, x)for multiples) — that’s stringly-typed thinking in disguise.The variant makes invalid states unrepresentable at the type level.
Design Principle on Deferred Construction #
Carry raw data through computation, construct structured output at commit points.
We accumulate count as a raw integer and only resolve it into One or Many at the moment we commit a group (the else branch and the [x] base case). This means the variant decision is made exactly once per group, at the group boundary.
The alternative: deciding One vs Many eagerly and then updating it — would require mutation or reconstruction.
Deferring to the boundary avoids both.
9. Duplicate the Elements of a List #
Duplicate the elements of a list.
This is still tail-recursive.
| |
My initial approach using an accumulator
This is unnecessary for this exercise, the accumulator pattern will necessitate a use of List.rev, which is useful only in the general case.
| |
general, replicate – shape: inner loop builds the repeated segment into the accumulator, outer recursion walks the list:
| |
10. Split a List Into Two Parts; The Length of the First Part Is Given #
Split a list into two parts; the length of the first part is given.
If the length of the first part is longer than the entire list, then the first part is the list and the second part is empty.
| |
the model solution looks like:
| |
What’s interesting:
the
i = 0is the commit point. we don’t want to consumeh, we want to return the whole remaining listthe
as lbindst the entireh :: tpattern tolwithout any reconstruction – no allocation noh :: trebuild.– this is exactly what we had seen before for the
asbinding for lookaheadinitial flawed approach
1 2 3 4 5 6 7 8 9 10 11let split ls k = let rec aux left right k = match k, right with | 0, _ | _, [] -> Some (left, right) | x, hd :: tl -> if x > 0 then aux (hd::left) tl (x - 1) else None in match aux [] ls k with | None -> ls, [] | Some l, r -> (Stdlib.List.rev l, r) ;; split ["a"; "b"; "c"; "d"; "e"; "f"; "g"; "h"; "i"; "j"] 3;; (* split ["a"; "b"; "c"; "d"] 5;; *)This gives the correct answer but it has 2 problems:
problem syntax err: Some l rThe pattern matching arm Some l rdoesn’t match the option-wrapped tuple correctly, it should beSome (l, r). The flawed version implies thatSometakes in 2 arguments, which is not correct.logic issue: the NonebranchWe return Nonewhenx <= 0which is redundant because the0, _arm catches it already so that’s a dead-pathAlso, there’s no need to option-wrap here.
version remark my flawed option-wrapped, negative-k guard, List.rev at call site model ans no wrapper, commit at i=0, h :: t as l for lookahead-free return
as binding to avoid unnecessary list construction – space-saving #
We saw earlier before for the as binding for lookahead what we could do with the h :: t as l.
To generalise a pattern, when our function needs to return a suffix of the original list unchanged, don’t deconstruct and reconstruct it.
Instead, bind it with as and return the binding directly. Reconstruction is \(O(k)\) allocation for nothing.
rule of thumb: failure cases are better candidates for option-wrapping vs boundary conditions #
In my own initial approach, I reached for the option-wrapping naturally to signal “invalid k”.
That’s alright when the caller genuinely needs to distinguish success from failure. But here the spec says “if k is too large, return the whole list and empty” — there’s no failure case, just a boundary condition.
When every input has a valid output, option adds wrapping/unwrapping overhead for no semantic gain.
The model handles the over-length case implicitly: when i > 0 and the list empties, | [] -> List.rev acc, [] fires naturally.
option when “no answer” is meaningfully different from “an answer”. When every input produces a valid output and boundary cases just produce specific values, handle them structurally.11. Remove the K’th Element From a List #
Remove the K’th element from a list.
The first element of the list is numbered 0, the second 1,…
This is a build-up version, we use an index counter and build it up to k.
| |
a count-down approach would have worked too
| |
We could do better and avoid the @ usage by using List.rev_append which reverses the acc, then manually links it to t:
my v0
| |
This feels complicated:
- lookahead is not necessary, it forces us to add the
[x]special case.- Lookahead is only warranted when the value of the next element affects the current decisoin (like in the RLE problem)
- here, our decision is only index-based so it’s sufficient for us to just use a counter for the index – a lookahead would add pointless complexity.
- the logic is tangled because:
- we reconstruct the list just to keep processing it
the model solution:
| |
| solution | pros | cons |
|---|---|---|
| model soln | clarity: direct, mirrors problem statement | not tail-recursive |
| model soln | no final List.rev | Uses stack proportional to k |
| model soln | no extra passes, stops when kth removed | possible stack overflow @ large lists |
| build-up soln | tail-recursive, constant stack space | always traverses the entire list |
| build-up soln | robust for large lists | always does a final List.rev @ \(O(n)\) |
| build-up soln | systematic / mechanical pattern | rebuilds the entire list, even after removal point |
Notes:
- some interesting design considerations:
- does it early exit?
- does it require an explicit reverse of the accumulator at the end of it?
Careful about using @ #
@ is \(O(n)\) where n is the length of the left operand — it has to walk the entire left list to find the end before appending. In our countdown version, List.rev acc @ t is \(O(k)\) for the rev plus \(O(k)\) for the @, so \(O(k)\) total. That’s fine here since it happens once at the commit point. Nevertheless, there’s a way for us to use List.rev_append to avoid using @.
It’s especially dangerous if we use it inside a recursive loop e.g.
(* O(n²) — don't do this *)
| h :: t -> aux (k-1) (acc @ [h]) tEach step would pay \(O(current acc length)\) – summing to \(O(n^{2})\) total.
@ once at a commit point is acceptable; @ inside a recursive call is a complexity trap.12. Insert an Element at a Given Position Into a List #
Start counting list elements with 0.
If the position is larger or equal to the length of the list, insert the element at the end. (The behavior is unspecified if the position is negative.)
model answer
| |
Notes:
- I realise that I prefer the
auxhelper pattern, together with the accumulator / index counter. It keeps me happy that it’s tail recursive vs not being tail recursive. At the same time, all the solutions so far have been recursive in nature an I’m wondering if there are non-recursive implementations that are possible for these problems. - this is good because @ the commit point, we exit (early-exit) so we don’t unnecessarily walk the list.
Iterative approaches (via List.fold_left) and when to use them #
For most of the problems so far, we could have solved it with a list fold List.fold_left and that would have been a non-recursive implementation.
“Non-recursive” typically is just hidden recursion within the library function though.
this solution, using a List.fold_left, it’s more awkward though
| |
Problems:
- doesn’t give an early-exit – will walk the entire list even after doing the insertion.
Library combinators vs using custom, recursive aux functions #
rec aux vs library combinators| approach | best used when |
|---|---|
fold_left | full traversal, accumulating a result. No early exit. Good for: length, sum, reverse, encode (RLE) |
| explicit aux fn | when you need early exit, index tracking with termination, or lookahead. Good for: split, insert_at, remove_at, find |
List.map / List.filter | when output is same shape as input, element-wise |
For these exercises, we’re training the structural recursion muscle. Once that’s automatic, we’ll reach for fold/map/filter when they fit and aux when they don’t, without having to think about it.
The point where explicit aux becomes unnecessary:
- When the problem reduces cleanly to a standard traversal with no early exit and no index state. That’s the signal to reach for a library combinator instead.
13. Create a List Containing All Integers Within a Given Range #
If first argument is greater than second, produce a list in decreasing order.
| |
my answer is the same as the model answer’s tail-recursive version
| |
the model answer also has a non-tail-recursive version:
| |
value of building in descending order #
we count down to the accumulator:
| |
The use of lo, high variable names and the ascending alias makes this version clear to read.
stdlib approaches #
List.init n f — generate n elements by index formula
List.filteri — filter with index
Seq.unfold — generate potentially infinite sequence from statewe could use
Seq.init1 2 3 4let range l r = let lo, hi = min l r, max l r in let ascending = List.init (hi - lo + 1) (fun i -> lo + i) in if l <= r then ascending else List.rev ascendingwe could use
List.init n fas a standard list generation of n elements by a formula- careful on the
Basevs stdlib usage though.
- careful on the
NOTE:
Seq.unfoldis the functional equivalent of a stateful iterator / generator.- useful for things like “generate Fib seq” or “generate primes lazily”
14. Lotto: Draw N Different Random Numbers From the Set 1..M #
Draw N different random numbers from the set 1..M.
The selected numbers shall be returned in a list.
Oddly, this depends on the rand_select problem that is actually in the intermediate set of questions (TODO: permalink to the intermediate problem)
| |
how rand_select works: destructive substitution
Instead of rejection sampling (i.e. pick randomly, retry on collision), rand_select picks a random index, removes that element from the pool, then recurses on the smaller pool.
Uniqueness is guaranteed structurally — we can’t pick the same element twice because it’s no longer in the list.
This is the correct general pattern for “sample without replacement”. Uniqueness is an invariant of the data structure, not a check we perform.
extract does two things:
- It walks to index
n, returns the element there - returns the list with that element removed
acc @ t- this is the pool to pick from for the next iteration
- the
@here is tolerable because it runs at \(O(k)\), where \(k\) is the extraction index – it’s per-draw and not within the recursion ofextractitself
Destructive extraction is always \(O(n·m)\) in the worst case. This is better than rejection sampling because rejection sampling degrades as the pool fills up — if we’ve picked 48 out of 49 numbers, we’ll reject almost every candidate.
my initial flawed attempt
| |
There’s a bunch of problems here:
- the
initfunction makes it such that repeat function calls will yield the same values – do the init outside, or useself_initfor variety of seeds - This function is not to spec because it doesn’t guarantee that the numbers would all be distinct. We could just do an accumulation check and the whole thing will end up being running in \(O(n^{2})\) time, like so:
1 2 3 4 5 6 7 8 9let lotto_select n m = let rec aux acc count = if count = 0 then acc else let candidate = Stdlib.Random.int_in_range ~min:1 ~max:m in if List.mem candidate acc then aux acc count else aux (candidate :: acc) (count - 1) in aux [] n
Careful @ the init for Random #
If we use a fixed salt for the init for the Random (i.e. Random.init(0)) then we need to be careful. If we place it within the function, then all the randoms end up being deterministic – so every output for that function ends up being the same (and being deterministic).
15. Generate a Random Permutation of the Elements of a List #
Generate a random permutation of the elements of a list.
my solution composed a bunch of previous helper functions
| |
This does 3 passes and highlights that we shouldn’t be doing composition here because the helper functions aren’t implemented in the best way yet:
lotto_select a_length a_lengthgenerates indices1..n, but our array is 0-indexed \(\implies\) we need to manually adjust the array access- the offset arithmetic is a sign that the abstraction boundary is wrong.
we do 3 passes when 1 would have been sufficient:
Array.of_list — pass 1 lotto_select — generates index list (pass 2 internally: range + rand_select) aux over orders — pass 3: maps indices back to elements
lotto_select produces numbers from a numeric range, but we want elements from an arbitrary list. Bridging that gap required the array + offset machinery. The model sidesteps the mismatch by operating directly on the list as pool. The correct composition would have been rand_select ls (List.length ls) — which is exactly what the model’s aux does, just inlined.The model answer modifies how the extraction logic works, it doesn’t compose things, just uses the destructive extraction like we did before.
| |
Arithmetic #
16. Determine Whether Two Positive Integer Numbers Are Coprime #
Determine whether two positive integer numbers are coprime.
Two numbers are coprime if their greatest common divisor equals 1.
It’s odd that this is labelled a beginner question AND it depends on the gcd problem, which is tagged as an intermediate question ( see solution here).
| |
17. Compare the Two Methods of Calculating Euler’s Totient Function #
Use the solutions of problems “Calculate Euler’s totient function \(\phi(m)\)” and “Calculate Euler’s totient function \(\phi(m)\) (improved)” to compare the algorithms.
Take the number of logical inferences as a measure for efficiency. Try to calculate \(\phi(10090)\) as an example.
Yet again, this question (though it’s a beginner question), depends on the two intermediate questions: this and this.
| |
An improvement for this would be that single-sample timing is unreliable beacuse the function Unix.gettimeofday() has a microsecond resolution and so the GC, OCaml runtime, OS scheduling would be sources of noise for that scale.
| |
18. A List of Prime Numbers #
Given a range of integers by its lower and upper limit, construct a list of all prime numbers in that range.
My initial solution (less readable) compared with the model solution
| |
The model solution is easier to read because it’s more of a sequential decision-making that we’re doing here — so using nested if s will work better here.
However, the model solution is NOT tail-recursive.
| |
| |
TODO Binary Trees #
TODO Multi-way Trees #
TODO Graphs #
brb #
nice.
