InnerAlien

InnerAlien

Hardware|Software

20 Feb 2026

Flatten a Nested List, official

№7.1 · 99 Picat Problems

In 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 X and the tail Xs
  • Concatenate the results with ++
  • Key insight: This works whether X is a list or an atom! If X is an atom, myFlatten(X) will match the third clause and return [X]. If X is 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 X doesn'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

  1. Pattern matching eliminates guards: Instead of using list(X) to check if something is a list, this solution relies on pattern matching. If X matches [_|_], it's a list (clause 1). If it matches [], it's empty (clause 2). Otherwise, it's an atom (clause 3).

  2. 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.

  3. 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 on X and let the pattern matching sort it out.

  4. Trust the recursion: This solution is a lovely example of "just recurse and it works out." Don't overthink whether X is a list or atom.

  5. 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.