- Creating a ReasonML Native App: What We Are Building
- Creating a ReasonML Native App: Installing, Editing, Configuring
- Creating a ReasonML Native App: Building and Running
- Creating a ReasonML Native App: Configuration Files
- Creating a ReasonML Native App: Curses Terminal Interface
- Creating a ReasonML Native App: Python Plugins
- Creating a ReasonML Native App: Files I/Os
- Creating a ReasonML Native App: Native Promises
- Creating a ReasonML Native App: Web Requests
- Creating a ReasonML Native App: Shell Commands
- Creating a ReasonML Native App: Handling CSV Files
- Creating a ReasonML Native App: Native JSON Handling
- Creating a ReasonML Native App: Memoizing DNS queries
Before going into how specific features were implemented with Reason and OCaml’s help, let’s have a quick look at our application’s logic
(Refresh if the diagram below doesn’t display properly)
You will immediately notice that:
- This is a CLI application, with some help from NCurses when editing a scenario file.
- We try to keep all local side effects isolated in their own class.
- Surely, most of this business must be asynchronous!
Since this is a CLI application, the first thing we are going to do is try to figure out what it is the user wants us to do. That is…
Handling command line arguments
The beauty of having pattern matching available is that we can quickly write some arguments handling without requiring an additional library.
First, we are going to have a quick look at Sys.argv
to determine what high level operation we are trying to perform. We may wish to run a scenario, edit its json file, display our program’s help page, etc.
Keeping in mind that, as in any language, argv[0]
represents our executable itself, here is some simple routing:
let () =
switch (Sys.argv) {
| [|_|] => display_help()
| [|_, "test"|] => run_test()
| [|_, "help"|] => display_help()
| [|_, "sanity"|] => display_sanity()
| [|_, "timestamp", ts, te|] => display_timestamp(ts, te)
| [|_, "reset"|] => Config.write_default_config_file()
| arguments when arguments[1] == "run" =>
...
| [|_, "modified"|] => display_modifications()
| [|_, "edit", fn|] => Editor.edit_source(fn)
| _ => display_help()
};
If the user, for instance, types ./reasonable-fidelity timestamp
and omits both timestamp parameters, we will default to displaying the help page.
[| |]
while lists use [ ]
Our most interesting match is the run
argument, which was purposefuly elided here. What our program does is check for pre-requisites, take the relevant remaining arguments and passes them to a function that will then match these arguments to build an array of valid requests:
let rec process_args = (args, ret) =>
switch (args) {
| [] => ret
| [head, ...tail] =>
switch (head) {
| "--csv" => ArgSet.add(Csv, process_args(tail, ret))
| "--forcelogin" => ArgSet.add(ForceLogin, process_args(tail, ret))
| "--source" =>
switch (tail) {
| [] => raise(MissingArgument("--source"))
| [arg1, ...tail] =>
ArgSet.add(Source_file(arg1), process_args(tail, ret))
}
| "--config" =>
switch (tail) {
| [] => raise(MissingArgument("--config"))
| [arg1, ...tail] =>
ArgSet.add(Config_dir(arg1), process_args(tail, ret))
}
| arg_ => raise(InvalidArgument(arg_))
}
};
These valid requests were declared as a variant type:
type cmdlineargs =
| Source_file(string)
| Config_dir(string)
| ForceLogin
| Csv;
Now that we have a normalized array of requests, we can process it:
let arg_use_csv = ref(false);
let arg_no_login = ref(true);
let arg_source_file = ref("example.har");
let arg_dir_path = ref("config");
ArgSet.iter(
k =>
switch (k) {
| Csv => arg_use_csv := true
| ForceLogin => arg_no_login := false
| Source_file(file_name) => arg_source_file := file_name
| Config_dir(dir_path) => arg_dir_path := dir_path
},
args,
);
Using mutability, we will set some variables to default sane values, then update them with our array’s content.
ref(value)
and that referenced value can be updated using the :=
operator.But, what is ArgSet?
The missing piece: ReasonML lets you create modules based on other modules, to implement your own behaviors. In this example, we are talking about a very light implementation:
module ArgSet =
Set.Make({
type t = cmdlineargs;
let compare = Pervasives.compare;
});
We are creating a new module, meant to store requests in a set. This set is declared to be of a type cmdlineargs
(see above) and its components can be compared using the default Pervasives
module’s comparison method. If you are not yet familiar with this module, please bookmark this page as it provides basic operations over ReasonML’s built-in types.
As you write more ReasonML, you will see lots of references to type t
as an object’s type. This convention is used for clarity. For instance, it could be used when destructuring objects to write concise matching.
Finally, we use a couple exceptions to denote incorrect arguments:
exception MissingArgument(string);
exception InvalidArgument(string);
Yes, ReasonML has exceptions support. No, you should not use them in idiomatic code. Here, I am using exceptions because I wish to bail out of the program itself. If it was in the middle of, say, processing files in a fairly long loop, returning an option
object would allow the program to treat this error code as any expected condition.
This is not a new pattern, but it is worth noticing its adoption in “modern” languages: the authors of the Rust language decided not to support exceptions and instead always provide error codes. This forces the developer to immediately think about how to handle these errors, as opposed to passing the buck further up the execution stack.
Comments powered by Talkyard.