5

I am trying to parse something that may be a list of items, or which may be just one item. I want to put the results into a DU (Thing below).

The way I'm approaching this is as below, but it gives me a list of things even when there is only one thing in the list.

let test p str =
    match run p str with
    | Success(result, _, _)   -> printfn "Success: %A" result
    | Failure(errorMsg, _, _) -> printfn "Failure: %s" errorMsg

type Thing =
    | OneThing of int
    | LotsOfThings of Thing list

let str s = pstringCI s .>> spaces 

let one = str "one" |>> fun x -> OneThing 1
let two = str "two" |>> fun x -> OneThing 2
let three = str "three" |>> fun x -> OneThing 3

let oneThing = (one <|> two <|> three)
let lotsOfThings = sepBy1 oneThing (str "or") |>> LotsOfThings

let lotsFirst = (lotsOfThings <|> oneThing)
test lotsFirst "one or two" // Success: LotsOfThings [OneThing 1; OneThing 2]
test lotsFirst "one" // Success: LotsOfThings [OneThing 1]

What is the correct way to return OneThing when there is only one item in the list?

I can do that if I test the list before returning, like the below. But that doesn't really "feel" right.

let lotsOfThings = sepBy1 oneThing (str "or") |>> fun l -> if l.Length = 1 then l.[0] else l |> LotsOfThings

LinqPad of the above is here: http://share.linqpad.net/sd8tpj.linq

Sean Kearon
  • 10,987
  • 13
  • 77
  • 93
  • 2
    As a comment, [some sister projects](https://github.com/kontan/Parsect) would see `sepByN(min, max, p, separator)` a naturally-fit approach here, so your original solution based on a guard rule is not bad at all. – Be Brave Be Like Ukraine May 22 '18 at 05:34
  • Thanks for that feedback, bytebuster - that's good to know too. – Sean Kearon May 22 '18 at 10:04

1 Answers1

6

If you don't like testing the list length after parsing, then you might try switching your <|> expression to test the single-item case first, and use notFollowedBy to ensure that the single-item case won't match a list:

let oneThing = (one <|> two <|> three)
let separator = str "or"
let lotsOfThings = sepBy1 oneThing separator |>> LotsOfThings

let oneThingOnly = oneThing .>> (notFollowedBy separator)
let lotsSecond = (attempt oneThingOnly) <|> lotsOfThings
test lotsSecond "one or two" // Success: LotsOfThings [OneThing 1; OneThing 2]
test lotsSecond "one" // Success: OneThing 1

Note the use of the attempt parser with oneThingOnly. That's because the documentation for the <|> parser states (emphasis in original):

The parser p1 <|> p2 first applies the parser p1. If p1 succeeds, the result of p1 is returned. If p1 fails with a non‐fatal error and without changing the parser state, the parser p2 is applied.

Without the attempt in there, "one or two" would first try to parse with oneThingOnly, which would consume the "one" and then fail on the "or", but the parser state would have been changed. The attempt combinator basically makes a "bookmark" of the parser state before trying a parser, and if that parser fails, it goes back to the "bookmark". So <|> would see an unchanged parser state after attempt oneThingOnly, and would then try lotsOfThings.

rmunn
  • 34,942
  • 10
  • 74
  • 105
  • 3
    One situation where this answer might not be appropriate is if an input like "one of" (with nothing after the "of") would be valid in your real parsing project. That text would fail the `oneThingOnly` parser but would also fail the `lotsOfThings` parser. In most situations that's not a big deal, but if you need to allow that specific case, then you'd have to add a third item to your `<|>` choice: `oneThing .>> separator .>> (notFollowedBy oneThing)`. – rmunn May 22 '18 at 06:24
  • 1
    I had tried some variations with `attempt`, but it was the use of `notFollowedBy` that I was missing. Thank you for your well constructed reply and also your additional useful comment. :) – Sean Kearon May 22 '18 at 06:50
  • 1
    `fun l -> l |> LotsOfThings` is equivalent to `LotsOfThings` – Fyodor Soikin May 22 '18 at 12:05
  • @FyodorSoikin - Good point; I wasn't looking all that closely at the rest of that definition. I've kept my example close to the OP's original code, but included your suggestion as a comment. – rmunn May 23 '18 at 04:33
  • Yes, that was my bad...I'll update the code to improve the question for those who come along in the future. Thanks @FyodorSoikin! – Sean Kearon May 23 '18 at 07:07