1

I'm new to functional programming and am working on a project in F#.

I've run a problem: I have a list of type string list list and I need to build separate lists based on the middle element of each string list. For example:

[["1";"b";"2"];["2";"a";"0"];["3";"b";"4"];["3";"a";"5"]]

Would be broken into 2 lists similar to the following:

let a = [["2";"0"];["3";"5"]]
let b = [["1";"2"];["3";"4"]]

I tried to use let a = [for [x;y;z] in myList do yield [x;z]] but am having trouble adding in the condition of y = "b", for instance.

Any help would be greatly appreciated

is0
  • 117
  • 5

2 Answers2

2
let myList = [["1";"b";"2"];["2";"a";"0"];["3";"b";"4"];["3";"a";"5"]]

let a = [for [x;y;z] in myList do if y="a" then yield [x;z]]
let b = [for [x;y;z] in myList do if y="b" then yield [x;z]]
Functional_S
  • 1,159
  • 6
  • 9
1

You're trying to split a list by its middle element. What is the expected behaviour when your list does not have 3 elements?

In the answer provided by Functional_S, you'll see the wiggly lines under the [x;y;z] in

let a = [for [x;y;z] in myList do if y="a" then yield [x;z]]

The compiler says "Incomplete pattern matches". Rather than now adding extra checks to handle empty lists, lists of length 2, etc, consider changing the design of your data types. If you have data that always contains of 3 elements, then use a data structure that has exactly 3 elements. Tuples are an obvious choice here, or use a record.

let myList = [ ("1","b","2"); ("2","a","0"); ("3","b","4"); ("3","a","5") ]
let splitByMiddle = 
    myList 
    |> List.groupBy (fun (_, middle, _) -> middle)
    |> List.map (fun (middle, elems) -> middle, elems |> List.map (fun (l, _, r) -> l, r))

If you execute that in interactive, you'll get:

val splitByMiddle : (string * (string * string) list) list =
    [("b", [("1", "2"); ("3", "4")]); ("a", [("2", "0"); ("3", "5")])]

An alternative would be:

let splitByMiddle = 
    myList 
    |> List.map (fun (l, middle, r) -> middle, (l, r))
    |> List.groupBy fst
    |> List.map (fun (middle, elems) -> middle, elems |> List.map snd)

I find that F# is really at its peak performance when you model your domain as closely as possible with your datatypes. In languages like Matlab, vectors and matrics are your number one work horse, you'd put everything into lists. But in F#, defining data types comes so cheaply (in terms of typing effort) - and once you've done so, the compiler is your best friend to remind you of possible corner cases your code is not covering.

In that light: I see all your middle elements are string, whereas the left/right elements are integers. Maybe your domain is better modelled by this record?

type R = 
    { 
        Left: int
        Right: int
        Middle: string
    }
let create (l, m, r) = { Left = l; Right = r; Middle = m}
let myList = [ create(1,"b",2); create(2,"a",0); create(3,"b",4); create(3,"a",5) ]
let splitByMiddle = 
    myList 
    |> List.groupBy (fun r -> r.Middle)

This will give you:

val splitByMiddle : (string * R list) list =
  [("b", [{Left = 1;
       Middle = "b";
       Right = 2;}; {Left = 3;
                     Middle = "b";
                     Right = 4;}]); ("a", [{Left = 2;
                                            Middle = "a";
                                            Right = 0;}; {Left = 3;
                                                          Middle = "a";
                                                          Right = 5;}])]
Anton Schwaighofer
  • 3,119
  • 11
  • 24