How can I add different separators between the elements of a list?

I have code which boils down to the following (at a basic level):

#r @"nuget: FSharpPlus"
open FSharpPlus

type Position = Position of int

type Thing =
| Doodah
| Widget
| Gizmo

type Whatsit =
| Doodah of Position
| Widget of Position
| Gizmo of Position

let describe whatsit =
 match whatsit with
 | Doodah (Position position) -> "Doodah " + position.ToString()
 | Widget (Position position) -> "Widget " + position.ToString()
 | Gizmo (Position position) -> "Gizmo " + position.ToString()

let things = [Doodah; Widget; Doodah; Widget; Gizmo]

let whatsits = things |> List.rev |> List.mapi (fun i thing -> thing (Position i)) |> List.rev

whatsits |> List.map describe |> List.intersperse ", " |> List.reduce (+)

// val whatsits: Whatsit list =
// [Doodah (Position 4); Widget (Position 3); Doodah (Position 2);
// Widget (Position 1); Gizmo (Position 0)]
// val it: string = "Doodah 4, Widget 3, Doodah 2, Widget 1, Gizmo 0"

However, I would like the last separator to be sometimes different, dependant upon specific rules which are:
(1) If there is zero or one Whatsit then there is no separator.
(2) If there is more than one Whatsit then the separator between the Whatsit with Position 0 (if there is one) and the next Whatsit (to its left in the list above) should be:
(a) “, “ if the Whatsit with Position 0 is a Gizmo, or;
(b) “and “ if the Whatsit with Position 0 is a Widget or a Doodah.
(3) Separators between all other Whatsits should be “, “.

Some expected example outputs are:
“Gizmo 0”
“Widget 0”
“Doodah 0”
“Gizmo 1, Gizmo 0”
“Gizmo 1 and Widget 0”
“Gizmo 1 and Doodah 0”
“Gizmo 2, Doodah 1” (There may not be anything at Position 0)
“Gizmo 2, Widget 1, Gizmo 0”
“Gizmo 2, Widget 1 and Widget 0”
“Doodah 3, Widget 2, Gizmo 1 and Doodah 0”
“Doodah 4, Gizmo 2 and Doodah 0” (some Whatsits may be removed from the list before adding the separators)

Is there a way to do this?

I’ve looked at List.fold and List.mapFold but I don’t think I can quite get my head around how to use these for this sort of thing where the number of elements is different to the number of separators.

IMO, once you get logic that’s complex like that, you’re better off with manual recursion instead of trying to build a solution by composing a bunch of other functions. I was able to get the behavior you describe with

let rec f = function 
  | [] -> ""
  | [oneThing] -> describe oneThing
  | x::[Gizmo (Position 0) as y] -> sprintf "%s, %s" (describe x) (describe y)
  | x::[Doodah (Position 0) as y] 
  | x::[Widget (Position 0) as y] -> sprintf "%s and %s" (describe x) (describe y)
  | x::y::rest -> sprintf "%s, %s" (describe x) (f (y::rest))

let testCases = 
  [
    [Gizmo (Position 0)]
    [Widget (Position 0)]
    [Doodah (Position 0)]
    [Gizmo (Position 1); Gizmo (Position 0)]
    [Gizmo (Position 1); Widget (Position 0)]
    [Gizmo (Position 1); Doodah (Position 0)]
    [Gizmo (Position 2); Doodah (Position 1)]
    [Gizmo (Position 2); Widget (Position 1); Gizmo (Position 0)]
    [Gizmo (Position 2); Widget (Position 1); Widget (Position 0)]
    [Doodah (Position 3); Widget (Position 2); Gizmo (Position 1); Doodah (Position 0)]
    [Doodah (Position 4); Gizmo (Position 2); Doodah (Position 0)]
  ]

for testCase in testCases do
  printf "%s\n" (f testCase)

You could probably do it with a fold or mapFold, but it’s also probably going to be more complicated than just manual recursion.

(Note that the implementation I gave isn’t tail-recursive, and so you could get a stack overflow if you had tens of thousands of elements. It could be modified to be tail-recursive, but would probably be a bit less intuitive).

1 Like

Thanks for that great reply.

Your solution is much more elegant than the one I eventually got working with a List.pairwise, three List.maps, a List.map2, a List.concat and two helper functions (with other stuff).
I’m going to have to pick your solution apart to see how it works as I’ve not yet used various things in it.

Cheers.

P.S. I should have said that the list, in the cases I’m using it, won’t ever be more than 10 elements long so a stack overflow shouldn’t be a problem.

1 Like

You’re welcome! I’m happy to describe any of the components of the solution I posted in more detail if you get stuck.