Flatten a Nested List, official
№7.1 · 99 Picat ProblemsIn the original breakdown we went rogue from the official solution and ended up with something less elegant, so this solution is the same as described on the picat website and to me is much more clear and concise.
myFlatten([X|Xs]) = myFlatten(X) ++ myFlatten(Xs).
myFlatten([]) = [].
myFlatten(X) = [X].
Explanation
Recursive case for any list (line 1):
myFlatten([X|Xs]) = myFlatten(X) ++ myFlatten(Xs).
- Pattern matches any non-empty list
[X|Xs] - Recursively flatten both the head
Xand the tailXs - Concatenate the results with
++ - Key insight: This works whether
Xis a list or an atom! IfXis an atom,myFlatten(X)will match the third clause and return[X]. IfXis a list, it will recurse further.
Base case for empty list (line 2):
myFlatten([]) = [].
- An empty list flattens to an empty list
- This terminates recursion on the tail
Xs
Base case for atoms (line 3):
myFlatten(X) = [X].
- When
Xdoesn't match[_|_](not a list with head/tail), it must be an atom - Wrap it in a single-element list to prepare for concatenation
- This terminates recursion on individual elements
Complete Example
main =>
go_07b.
go_07b =>
A = [1,[2,[3,4],5]],
B = myFlatten(A),
println(B).
myFlatten([X|Xs]) = myFlatten(X) ++ myFlatten(Xs).
myFlatten([]) = [].
myFlatten(X) = [X].
How it works:
A = [1,[2,[3,4],5]]is a nested list- The first clause handles all non-empty lists by splitting into head and tail
- Recursion naturally handles nested lists without needing explicit
list(X)checks println(B)outputs[1,2,3,4,5]
Example Execution
myFlatten([1,[2,[3,4],5]])
→ myFlatten(1) ++ myFlatten([[2,[3,4],5]]) (split into head 1 and tail)
→ [1] ++ myFlatten([[2,[3,4],5]]) (1 is an atom → [1])
→ [1] ++ (myFlatten([2,[3,4],5]) ++ myFlatten([])) (split nested list)
→ [1] ++ (myFlatten([2,[3,4],5]) ++ []) (empty list → [])
→ [1] ++ (myFlatten(2) ++ myFlatten([[3,4],5])) (split into 2 and tail)
→ [1] ++ ([2] ++ myFlatten([[3,4],5])) (2 is an atom → [2])
→ [1] ++ ([2] ++ (myFlatten([3,4]) ++ myFlatten([5]))) (split nested list)
→ [1] ++ ([2] ++ ((myFlatten(3) ++ myFlatten([4])) ++ myFlatten([5])))
→ [1] ++ ([2] ++ (([3] ++ (myFlatten(4) ++ myFlatten([]))) ++ myFlatten([5])))
→ [1] ++ ([2] ++ (([3] ++ ([4] ++ [])) ++ (myFlatten(5) ++ myFlatten([]))))
→ [1] ++ ([2] ++ (([3] ++ [4]) ++ ([5] ++ []))) (atoms → lists)
→ [1,2,3,4,5] (all concatenations complete)Takeaways
-
Pattern matching eliminates guards: Instead of using
list(X)to check if something is a list, this solution relies on pattern matching. IfXmatches[_|_], it's a list (clause 1). If it matches[], it's empty (clause 2). Otherwise, it's an atom (clause 3). -
Polymorphic recursion: The first clause
myFlatten(X)works for both atoms and lists because the function handles both cases. This is a powerful technique—let the recursion figure out what each element is rather than checking explicitly. -
Simpler than Problem 7: Compare this to the guard-based version:
% With guards (Problem 7) myFlatten([X|Xs]) = Res, list(X) => Res = myFlatten(X) ++ myFlatten(Xs). myFlatten([X|Xs]) = [X] ++ myFlatten(Xs). % Without guards (Problem 7b) myFlatten([X|Xs]) = myFlatten(X) ++ myFlatten(Xs).The guard-based version explicitly checks
list(X)and has separate clauses for list vs non-list heads. The elegant version treats both uniformly—just recurse onXand let the pattern matching sort it out. -
Trust the recursion: This solution is a lovely example of "just recurse and it works out." Don't overthink whether
Xis a list or atom. -
Why three clauses? Each clause serves a distinct purpose:
- Clause 1: Decompose a non-empty list into head and tail
- Clause 2: Handle the empty list (recursion terminator for tails)
- Clause 3: Handle atoms (recursion terminator for individual elements)
You need all three because you encounter both empty lists
[](from tails) and atoms (from heads) during recursion.