This is a port of the OOP to FP-first style of the official Dart learn series. (see OOP-to-FP architectural map)
OOP style - Object Oriented Programming style
FP style - Functional Programming style
NOTE: From this point in time and onwards, the Glossary applies to the remaining part of this document as follows:
From this project's root, run the following command:
dart run cli:main search "Hello, World" >> search_history.txt
This command will redirect and append standard output to the pre-defined
search_history.txtfound at the root of this project; NOTE: redirection is optional but it provides the developer with a means of traversing.
Read this article - Application package on Windows platform
From this project's root, run the following command:
dart run test/test_cli.dart
Remember: Testing is fundamentally an aggressive attempt to break your own code while writing the test suite: if you actively tried to break your application by testing invalid inputs, missing files, and/or network drops, and your core logic handled every single one safely, then seeing "All tests passed!" is well-earned peace of mind.
| Concept | Official Tutorial (OOP-first) | Our architecture (FP-first with minimum footprint of OOP) | Benefits |
|---|---|---|---|
| Data Models | class with mutable properties. | typedef Records (e.g., Summary). |
Guarantees immutability and requires zero boilerplate. |
| JSON Parsing | factory constructors (e.g., fromJson) with manual type casting. | Pure functions (e.g., extractSummary) using Pattern Matching. |
Validates structure and types safely in a single expression. |
| Command Routing | if/else chains checking raw strings. | switch expressions over a sealed class (AppCommand). | The compiler enforces exhaustiveness (you can't forget to handle a command). |
| Side Effects (I/O) | Direct calls to stdout.writeln and stdin.readLineSync inside core logic. | Injecting onOutput and onInput functions at the application boundary. | Isolates messy real-world interactions from pure internal logic. |
| Network Calls | Hardcoded http.get inside processing functions. | Injecting onFetchSummary into executeCommand. | Makes the core router fully deterministic. |
| Error Handling | Scattered try/catch blocks throwing Exceptions. | Returning a (Summary?, String?) at the application boundary | Errors become predictable data, eliminating hidden crashes. |
Eventually, by moving all the unpredictable elements (like stdout) to the very top of main.dart, we transformed executeCommand from an untestable, side-effect-heavy function into a pure, predictable core.
Examples of “application boundary”:
- Making an HTTP network request (servers crash, Wi-Fi drops)
- Reading a file from the hard drive (the file might be locked or deleted).
- Querying a database (the connection might time out).
We have successfully built a purely functional CLI application. The logic is separated from the side effects, errors are treated as data, and JSON is parsed safely using pattern matching without throwing exceptions!
Credits to free-tier AI tools such as Google Gemini, ChatGPT, et al. for putting me on the right track with Dart syntax, good practices of Functional programming style, you name it!