Posted in:

As promised, here are some F# solutions to my second lunchtime LINQ challenge. I’ve still got a way to go with getting familiar with all the features of F#, so thanks especially to Sehnsucht whose answers pointed me in the direction of some better ways to solve these.

Problem 1 – Motorsport Scores

For the first one, I pretty much went with the same approach as in C#: convert to int, sort, skip 3, then sum:

let scores = "10,5,0,8,10,1,4,0,10,1"
scores.Split(',')
    |> Seq.map int
    |> Seq.sort
    |> Seq.skip 3
    |> Seq.sum
    |> DumpAs "Q1"

Problem 2 – Bishop Moves

My first attempt at this one used Seq.collect, which is the F# equivalent of LINQ’s SelectMany.

seq { 'A' ..'H' }
    |> Seq.collect (fun a -> seq { 1 .. 8 } |> Seq.map (fun x -> sprintf "%c%d" a x))
    |> Seq.filter (fun p -> abs (int p.[0] - int 'C') = abs (int p.[1] - int '6'))
    |> Seq.filter (fun p -> p <> "C6")
    |> String.concat ","
    |> DumpAs "Q2"

But in the C# solution we saw how the LINQ Query Expression Syntax makes for a cleaner way of expressing the problem that doesn’t repeat the starting position. And it turns out that we can create a very similar solution in F#:

seq {
  for r in 1 .. 8 do
  for c in 'a' .. 'h' do
  let dx = abs (r - 6)
  let dy = abs (int c - int 'c') 
  if dx <> 0 && dx = dy
  then yield sprintf "%c%d" c r
}
|> DumpAs "Q2"

Problem 3 – Sampling

This was one where F# surprisingly lacks the method that made it so easy in C#. F~ has a mapi function which gives an index of each item, but there is no corresponding filteri function (feature request here) . So I ended up using a mapi to create a sequence of tuples, which I could then filter and map back to a string.

let samples = "0,6,12,18,24,30,36,42,48,53,58,63,68,72,77,80,84,87,90,92,95,96,98,99,99,100,99,99,98,96,95,92,90,87,84,80,77,72,68,63,58,53,48,42,36,30,24,18,12,6,0,-6,-12,-18,-24,-30,-36,-42,-48,-53,-58,-63,-68,-72,-77,-80,-84,-87,-90,-92,-95,-96,-98,-99,-99,-100,-99,-99,-98,-96,-95,-92,-90,-87,-84,-80,-77,-72,-68,-63,-58,-53,-48,-42,-36,-30,-24,-18,-12,-6"
samples.Split(',')
    |> Seq.mapi (fun a b -> a,(int b))
    |> Seq.filter (fun x -> (fst x + 1) % 5 = 0)
    |> Seq.map (snd >> (sprintf "%d"))
    |> String.concat ","
    |> DumpAs "Q3"

Sehnsucht provided an interesting alternative approach, making use of Seq.chunkBySize:

"0,6,12,18,24,30,36,42,48,53,58,63,68,72,77,80,84,87,90,92,95,96,98,99,99,100,99,99,98,96,95,92,90,87,84,80,77,72,68,63,58,53,48,42,36,30,24,18,12,6,0,-6,-12,-18,-24,-30,-36,-42,-48,-53,-58,-63,-68,-72,-77,-80,-84,-87,-90,-92,-95,-96,-98,-99,-99,-100,-99,-99,-98,-96,-95,-92,-90,-87,-84,-80,-77,-72,-68,-63,-58,-53,-48,-42,-36,-30,-24,-18,-12,-6"
    .Split(',')
    |> Seq.chunkBySize 5
    |> Seq.map (Array.item 4)
    |> DumpAs "Q3"

Problem 4 – Vote Winning Margin

This one was quite easy, as the sumBy function lets us use the same technique as with C# and turn yes into +1 and no into –1 and sum:

"Yes,Yes,No,Yes,No,Yes,No,No,No,Yes,Yes,Yes,Yes,No,Yes,No,No,Yes,Yes"
    .Split(',')
    |> Seq.sumBy (fun x -> if x = "Yes" then 1 else -1)
    |> DumpAs "Q4"

But again Sehnsucht pointed me in the direction of an alternative solution, and that is to use the function keyword (not the same as the fun keyword) which allows you to use pattern matching in the function definition. What I hadn’t realised was that you are not forced to match all possible inputs, and so you can do something like this:

"Yes,Yes,No,Yes,No,Yes,No,No,No,Yes,Yes,Yes,Yes,No,Yes,No,No,Yes,Yes"
    .Split(',')
    |> Seq.sumBy (function "Yes" -> 1 | "No" -> -1)
    |> DumpAs "Q4"  

Problem 5 – Counting Pets

In C# we used GroupBy, but noted that a better solution would be if there was a method called countBy. And guess what, F# has one! And we can use the function keyword we just learned about to specify that we want to count dogs and cats separately but batch all other pets together. And I also learned another new F# function, the ‘destructuring’ operator ||>, letting me pass a tuple to sprintf without needing to use fst and snd explicitly.

"Dog,Cat,Rabbit,Dog,Dog,Lizard,Cat,Cat,Dog,Rabbit,Guinea Pig,Dog"
    .Split(',')
    |> Seq.countBy (function x when x = "Dog" || x = "Cat" -> x | _ -> "Other")
    |> Seq.map (fun x -> x ||> sprintf "%s:%d")
    |> String.concat ","
    |> DumpAs "Q5b"

I have a feeling the map could be cleaned up even further with the composition operator >> but I couldn’t come up with the right syntax.

Problem 6 – Run Length Decoding

Finally, for this problem I took a very similar approach to C#, taking advantage of Regex.Matches and Cast<Match> to get started.

Regex.Matches("A5B10CD3",@"[A-Z]\d*")
    .Cast<Match>()
    |> Seq.map (fun m -> m.Value)
    |> Seq.map (fun m -> new string(m.[0], if m.Length = 1 then 1 else int (m.Substring(1))))
    |> String.concat ""
    |> DumpAs "Q6"

Of course, one of the things that F# makes really easy is to create little helper functions that you can use to simplify your code, in the same way that you might declare intermediate variables in C# to make your code more readable.

So for example we might create a repeats function to decide how many times a letter should be repeated, a repeat function to construct the string with that many repeats, and a matches function to simplify performing the regex. It doesn’t make the solution shorter, but I think this style of coding does aid readability:

let repeats (s:string) =
    match s.Substring(1) with | "" -> 1 | n -> int n

let repeat c n = new string(c, n)

let matches input regex = 
    Regex.Matches(input,regex).Cast<Match>() 
        |> Seq.map (fun m -> m.Value)
    
matches "A5B10CD3" @"[A-Z]\d*"
    |> Seq.map (fun m -> repeat m.[0] (repeats m))
    |> String.concat ""
    |> DumpAs "Q6"

As always, I’d love to hear from any F# gurus in the comments if there are some cool tricks and techniques I’ve missed.

Want to learn more about LINQ? Be sure to check out my Pluralsight course LINQ Best Practices.

Comments

Comment by Sehnsucht

First ; thanks for quoting me in the article :)
A "trick" we often use in functional programming is what is know under the barbarian name eta-conversion (or point free style) that is when you have something along

fun x -> someFct x
That's exactly the same as just someFct
So for problem 2

|> Seq.map (fun x -> sprintf "%c%d" a x)
//can be replaced by
|> Seq.map (sprintf "%c%d" a)

It also works for let clause (with a side note)

let incr x = 1 + x
// which can be written
let incr x = (+) 1 x
// is the same as
let incr = (+) 1
There are difference though from the type system point of view, in .Net parlance the first one is compiled as a method and the second as a field (returning some Func [FSharpFunc probably] type)
There is also problem with that in case of type generalization, if infered type is generic ; as a value (field) can't be generic it needs additional type information (or rolling back to the non eta-converted one)
For problem 4 ; two things,
Patterns are allowed nearly anywhere inside code (in a let with a fun on a for in list comprehension etc.) that's not restricted to function. function is just a shortcut to (fun arg -> match arg with).
Second thing, there is a (theoretical) risk of non-exhaustive match because I only match "Yes" or "No" so I should have added a wildcard case to handle that (Linqpad doesn't warn about this but c/c the code in interactive (without the dump) raise it)
That was the same in some of my solution for first problems or in my alternate solution for Problem 4
Personally ; I'm not sure what approach is better ; leaving a warning (stating clearly the risk) and getting a MatchFailureException in case of something wrong or handling the other case using wildcard (so no warning) and getting what you put inside it (a basic exception with the message you gave if you used : failwith "message")
For problem 5
You don't really need ||> if you don't know it ; as I said before you can pattern match anywhere :

|> Seq.map (fun (pet, count) -> sprintf "%s:%d" pet count)

Sehnsucht
Comment by Mark Heath

great, thanks for these tips. I need to get into the hang of using pattern matching in function definitions

Mark Heath