Flatten a Nested List Structure
№7 · 99 Picat ProblemsmyFlatten([]) = [].
myFlatten([X|Xs]) = Res, list(X) => Res = myFlatten(X) ++ myFlatten(Xs).
myFlatten([X|Xs]) = [X] ++ myFlatten(Xs).
myFlatten(X) = [X].
Explanation
This function transforms a nested list structure like [1,[2,[3,4],5]] into a flat list [1,2,3,4,5]. The challenge is handling elements that might themselves be lists.
Base case for empty list (line 1):
myFlatten([]) = [].
- An empty list flattens to an empty list
Recursive case for list head (line 2):
myFlatten([X|Xs]) = Res, list(X) => Res = myFlatten(X) ++ myFlatten(Xs).
This line is dense, so let's break it down piece by piece:
myFlatten([X|Xs]) = Res- The function returns a value that will be bound to variableRes, list(X)- Guard condition: the comma means "and also check this condition". The function clause only matches ifXis a list=>- Separates the guard from the function body (this is different from=which is for assignment/unification)Res = myFlatten(X) ++ myFlatten(Xs)- The body: flattenX(a nested list), flattenXs(the tail), concatenate with++, and bind the result toRes
Why use Res here? Because we need a place to put the guard condition. We can't write:
myFlatten([X|Xs]) = myFlatten(X) ++ myFlatten(Xs), list(X). % Syntax error!
The guard must come before the =>, so we introduce an intermediate variable Res:
myFlatten([X|Xs]) = Res, list(X) => Res = myFlatten(X) ++ myFlatten(Xs).
Think of it as: "This function returns Res, but only if X is a list, and here's how to compute Res..."
Recursive case for non-list head (line 3):
myFlatten([X|Xs]) = [X] ++ myFlatten(Xs).
- Pattern matches a list with head
Xand tailXs - This clause only matches when the previous one doesn't (when
Xis not a list) - Wrap
Xin a single-element list[X]and concatenate with the flattened tail
Base case for non-list atom (line 4):
myFlatten(X) = [X].
- Handles the case where the input is a single element (not a list at all)
- Returns it wrapped in a list
Complete Example
Here's the full runnable code:
main =>
go_07.
go_07 =>
A = [1,[2,[3,4],5]],
B = myFlatten(A),
println(B).
myFlatten([]) = [].
myFlatten([X|Xs]) = Res, list(X) => Res = myFlatten(X) ++ myFlatten(Xs).
myFlatten([X|Xs]) = [X] ++ myFlatten(Xs).
myFlatten(X) = [X].
How it works:
A = [1,[2,[3,4],5]]is a nested list with multiple levelsmyFlatten(A)recursively descends into nested lists- The
list(X)guard determines whether to recurse intoXor treat it as an atom println(B)outputs[1,2,3,4,5]
Example Execution
With A = [1,[2,[3,4],5]]:
myFlatten([1,[2,[3,4],5]])
→ [1] ++ myFlatten([[2,[3,4],5]]) (1 is not a list)
→ [1] ++ (myFlatten([2,[3,4],5]) ++ myFlatten([])) ([2,[3,4],5] is a list, flatten it)
→ [1] ++ (myFlatten([2,[3,4],5]) ++ []) (empty list base case)
→ [1] ++ ([2] ++ myFlatten([[3,4],5])) (2 is not a list)
→ [1] ++ ([2] ++ (myFlatten([3,4],5) ++ myFlatten([5]))) ([3,4] is a list)
→ [1] ++ ([2] ++ (([3] ++ myFlatten([4])) ++ myFlatten([5]))) (flatten [3,4])
→ [1] ++ ([2] ++ (([3] ++ [4]) ++ ([5] ++ []))) (base cases resolve)
→ [1,2,3,4,5] (all concatenations complete)
The recursion explores the nested structure depth-first, collecting atoms into a single flat list.
Takeaways
-
Guard syntax in functions: When you need a guard condition in a function, the syntax is:
functionName(Pattern) = Result, guard_condition => Result = expression.Breaking it down:
= Resultintroduces a variable to hold the return value, guard_conditionadds the condition (comma means "and")=>separates the guard from the bodyResult = expressioncomputes the actual return value
Compare this to simpler functions without guards:
myFlatten([X|Xs]) = [X] ++ myFlatten(Xs). % No guard, direct = expression -
Type checking with guards: The
list(X)built-in predicate checks whetherXis a list. This allows different clauses to handle lists vs atoms differently. Picat evaluates clauses in order, so the guarded clause (line 2) is tried before the unguarded clause (line 3). -
List concatenation: The
++operator concatenates two lists. Each recursive call builds up the result by concatenating smaller flattened pieces. While++is O(n) for the left list, this approach is clean and readable for moderate-sized lists. -
Multiple base cases: Unlike earlier problems with a single base case,
myFlattenhas two: one for empty lists[]and one for non-list atoms. This handles both the recursion termination (when we've consumed all elements) and leaf nodes (individual atoms that need to be wrapped in a list). -
Clause ordering matters: Picat tries clauses in the order they're written. The guarded clause
list(X)must come before the unguarded clause, or else the unguarded one would always match first.