In the last episode we made a heady brew of Attributes, Reflection and LINQ to discover all the Problem-solving classes in my Project Euler Console App. The aim was to be able present the user with a list of the problems we'd solved, so they could choose one to run.
I showed the beginning of the process - discovering the classes - and the end - running the solution chosen by the user; I thought there would be nothing interesting about the middle part - displaying the list, and getting the user's choice, but I was wrong. C#3.0 makes even Console programming interesting again!
In the first place we've got the task of taking the meta-data about each problem, and making, not a meal out of it, but a menu. If you remember, we have a list of EulerProblemSolverMetadata objects, each containing the problem number, its title, and the information need by the Reflection APIs to invoke the Solve method in the class. What we need is a nicely formatted string that we can display in the Console.
For that we can use the Enumerable.Aggregate method. This, as you know, works its way through a sequence, combining each item with earlier items in whatever way you specify. We'll use it to add a line for each item to a StringBuilder:
var problemsSummary = solversMetaData.Aggregate( new StringBuilder(), (sb, solverData) => sb.AppendFormat("{0}: {1}\n", solverData.Number, solverData.Title) ); Console.WriteLine(problemsSummary);
Output is one thing: getting input from a user is another. If users aren't being deliberately evil and trying to crash a program, then they'll often do something stupid; even the best of us sometimes have "senior moments". All of which means that a good proportion of many code bases is taken up with sanitising user input.
I started off with something pretty standard, as follows:
Console.WriteLine("Which problem would you like to solve?"); EulerProblemSolverMetadata chosenSolver = null; while (chosenSolver == null) { string response = Console.ReadLine(); int problemNumber = 0; bool validResponse = int.TryParse(response, out problemNumber); if (!validResponse) { Console.WriteLine("Please type in a number"); continue; } // check whether there is a Solver for that problem number chosenSolver = solversMetaData.FirstOrDefault(solver => solver.Number == problemNumber); if (chosenSolver != null) { break; } else { Console.WriteLine("That problem number wasn't recognised"); continue; } } return chosenSolver;
That while loop made me feel very uncomfortable. It needs to be there to give the user more than one chance at giving a sensible answer: but it would ruin my Functional Programming credentials if I let that stand as my final answer. So I pondered the problem a little while longer (no pun intended!), squinting at that loop to see if I could discern anything that could be made to look LINQish.
Eventually I realised that what I had was essentially a pipeline: the code repeatedly gets user input, subjects it to a set of checks and successive conversions, until finally a valid answer emerges at the other end. If at any stage the pipeline rejects the item, it starts over again. That's almost the cannonical form of a LINQ query: data source, filters, conversions. Once that thought had clicked in my mind, I wrote down what I wanted my code to look like:
var chosenSolver = Console.In.ReadLines() .Validate(input => input.IsInteger(), "Please type in a number") .Select(input => int.Parse(input)) .Validate(problemNumber => solversMetaData.Any(metaData => metaData.Number == problemNumber), "That problem number wasn't recognised" ) .Select(problemNumber => solversMetaData.First(metaData => metaData.Number == problemNumber) ) .First(); return chosenSolver;
To make this work, we need just two new extension methods: ReadLines is an extension method on TextReader returning an Iterator to generate a sequence containing succesive pieces of user input:
public static IEnumerable<string> ReadLines(this TextReader reader) { while (true) { yield return reader.ReadLine(); } }
The Validate method is identical to Where in that it filters a sequence using a lambda expression; the only difference is that when Validate rejects an item, it writes out the message that we pass to it:
public static IEnumerable<T> Validate<T>(this IEnumerable<T> sequence, Func<T,bool> predicate, string invalidItemMessage) { foreach (var item in sequence) { if (predicate(item)) { yield return item; } else { Console.WriteLine(invalidItemMessage); } } }
With these extension methods in place the LINQ query works identically to the code in the while loop above. Much more beautiful, though, in my opinion, and no loops!
The code can be found in the Program.cs class in the download from MSDN Code Gallery.
7 comments:
I think this is beautiful. It already reminds me a lot of the ways you would handle input in Haskell. Just a minor note, it should be sufficient to write:
.Select(int.Parse), since the signature fits.
I think this is beautiful. It already reminds me a lot of the ways you would handle input in Haskell. Just a minor note, it should be sufficient to write:
.Select(int.Parse), since the signature fits.
Thanks Frank!
That's a useful reminder about the Select statement. I guess I find the syntax I used slightly more readable since (to my eyes anyway) it conveys the fact that a transformation is happening to the lambda variable
Quite quite interesting. It seems you can turn anything into LINQ coding and that's useful for my learning efforts.
One thing I wanted to tell you. It annoys me a bit when you use the 'no pun intended' thing in your writings. I mean, your style is nice and pleasant to read, but this little things simply bug me.
Detailed explanation (a bit too rude) of the annoyance here: http://www.thebestpageintheuniverse.net/c.cgi?u=puns
Dan,
Thanks for the feedback. I'm glad you found the article interesting.
I'll consider my wrist slapped with regards to the "no pun intended" :-). I don't recall using that phrase in any recent articles, and I'll try to keep it that way.
Sam
I just independently invented a console application to dynamically compile and execute user-input LINQ query code. The user queries work over an IEnumerable<string> that is piped from Console.In to provide data input so that you can chain multiple tools together, much like how you've done, but I've made it generic and scriptable.
Have a look: http://bittwiddlers.org/?p=141
I think this is beautiful. It already reminds me a lot of the ways you would handle input in Haskell. Just a minor note, it should be sufficient to write:
.Select(int.Parse), since the signature fits.
Post a Comment