A pure command-line wizard by Mark Seemann
An example of a small Abstract Syntax Tree written with F# syntactic sugar.
In the previous article, you got an introduction to a functional command-line API in F#. The example in that article, however, was too simple to highlight its composability. In this article, you'll see a fuller example.
Command-line wizard for on-line restaurant reservations #
In previous articles, you can see variations on an HTTP-based back-end for an on-line restaurant reservation system. In this article, on the other hand, you're going to see a first attempt at a command-line client for the API.
Normally, an on-line restaurant reservation system would have GUIs hosted in web pages or mobile apps, but with an open HTTP API, a self-respecting geek would prefer a command-line interface (CLI)... right?!
Please enter number of diners: four Not an integer. Please enter number of diners: 4 Please enter your desired date: My next birthday Not a date. Please enter your desired date: 2017-11-25 Please enter your name: Mark Seemann Please enter your email address: mark@example.com {Date = 25.11.2017 00:00:00 +01:00; Name = "Mark Seemann"; Email = "mark@example.com"; Quantity = 4;}
In this incarnation, the CLI only collects information in order to dump a rendition of an F# record on the command-line. In a future article, you'll see how to combine this with an HTTP client in order to make a reservation with the back-end system.
Notice that the CLI is a wizard. It leads you through a series of questions. You have to give an appropriate answer to each question before you can move on to the next question. For instance, you must type an integer for the number of guests; if you don't, the wizard will repeatedly ask you for an integer until you do.
You can develop such an interface with the commandLine
computation expression from the previous article.
Reading quantities #
There are four steps in the wizard. The first is to read the desired quantity from the command line:
// CommandLineProgram<int> let rec readQuantity = commandLine { do! CommandLine.writeLine "Please enter number of diners:" let! l = CommandLine.readLine match Int32.TryParse l with | true, dinerCount -> return dinerCount | _ -> do! CommandLine.writeLine "Not an integer." return! readQuantity }
This small piece of interaction is defined entirely within a commandLine
expression. This enables you to use do!
expressions and let!
bindings to compose smaller CommandLineProgram
values, such as CommandLine.writeLine
and CommandLine.readLine
(both shown in the previous article).
After prompting the user to enter a number, the program reads the user's input from the command line. While CommandLine.readLine
is a CommandLineProgram<string>
value, the let!
binding turns l
into a string
value. If you can parse l
as an integer, you return the integer; otherwise, you recursively return readQuantity
.
The readQuantity
program will continue to prompt the user for an integer. It gives you no option to cancel the wizard. This is a deliberate simplification I did in order to keep the example as simple as possible, but a real program should offer an option to abort the wizard.
The function returns a CommandLineProgram<int>
value. This is a pure value containing an Abstract Syntax Tree (AST) that describes the interactions to perform. It doesn't do anything until interpreted. Contrary to designing with Dependency Injection and interfaces, however, you can immediately tell, from the type, that explicitly delimited impure interactions may take place within that part of your code base.
Reading dates #
When you've entered a proper number of diners, you proceed to enter a date. The program for that looks similar to readQuantity
:
// CommandLineProgram<DateTimeOffset> let rec readDate = commandLine { do! CommandLine.writeLine "Please enter your desired date:" let! l = CommandLine.readLine match DateTimeOffset.TryParse l with | true, dt -> return dt | _ -> do! CommandLine.writeLine "Not a date." return! readDate }
The readDate
value is so similar to readQuantity
that you might be tempted to refactor both into a single, reusable function. In this case, however, I chose to stick to the rule of three.
Reading strings #
Reading the customer's name and email address from the command line is easy, as no parsing is required:
// CommandLineProgram<string> let readName = commandLine { do! CommandLine.writeLine "Please enter your name:" return! CommandLine.readLine } // CommandLineProgram<string> let readEmail = commandLine { do! CommandLine.writeLine "Please enter your email address:" return! CommandLine.readLine }
Both of these values unconditionally accept whatever you write when prompted. From a security standpoint, all input is evil, so in a production code base, you should still perform some validation. This, on the other hand, is demo code, so with that caveat, it accepts all strings you might type.
These values are similar to each other, but once again I invoke the rule of three and keep them as separate values.
Composing the wizard #
Together with the general-purpose command line API, the above values are all you need to compose the wizard. In this incarnation, the wizard should collect the information you type, and create a single record with those values. This is the type of record it must create:
type Reservation = { Date : DateTimeOffset Name : string Email : string Quantity : int }
You can easily compose the wizard like this:
// CommandLineProgram<Reservation> let readReservationRequest = commandLine { let! count = readQuantity let! date = readDate let! name = readName let! email = readEmail return { Date = date; Name = name; Email = email; Quantity = count } }
There's really nothing to it. As all the previous code examples in this article, you compose the readReservationRequest
value entirely inside a commandLine
expression. You use let!
bindings to collect the four data elements you need, and once you have all four, you can return a Reservation
value.
Running the program #
You may have noticed that no code so far shown define functions; they are all values. They are small program fragments, expressed as ASTs, composed into slightly larger programs that are still ASTs. So far, all the code is pure.
In order to run the program, you need an interpreter. You can reuse the interpreter from the previous article when composing your main
function:
[<EntryPoint>] let main _ = Wizard.readReservationRequest |> CommandLine.bind (CommandLine.writeLine << (sprintf "%A")) |> interpret 0 // return an integer exit code
Notice that most of the behaviour is defined by the above Wizard.readReservationRequest
value. That program, however, returns a Reservation
value that you should also print to the command line, using the CommandLine
module. You can achieve that behaviour by composing Wizard.readReservationRequest
with CommandLine.writeLine
using CommandLine.bind
. Another way to write the same composition would be by using a commandLine
computation expression, but in this case, I find the small pipeline of functions easier to read.
When you bind two CommandLineProgram
values to each other, the result is a third CommandLineProgram
. You can pipe that to interpret
in order to run the program. The result is an interaction like the one shown in the beginning of this article.
Summary #
In this article, you've seen how you can create larger ASTs from smaller ASTs, using the syntactic sugar that F# computation expressions afford. The point, so far, is that you can make side-effects and non-deterministic behaviour explicit, while retaining the 'normal' F# development experience.
In Haskell, impure code can execute within an IO
context, but inside IO
, any sort of side-effect or non-deterministic behaviour could take place. For that reason, even in Haskell, it often makes sense to define an explicitly delimited set of impure operations. In the previous article, you can see a small Haskell code snippet that defines a command-line instruction AST type using Free
. When you, as a code reader, encounter a value of the type CommandLineProgram String
, you know more about the potential impurities than if you encounter a value of the type IO String
. The same argument applies, with qualifications, in F#.
When you encounter a value of the type CommandLineProgram<Reservation>
, you know what sort of impurities to expect: the program will only write to the command line, or read from the command line. What if, however, you'd like to combine those particular interactions with other types of interactions?
Read on.