I find results work best when they work on the same type – something like validating a string in a form.
For something like processing args in a CLI app, if you create some kind of context object to use instead of retaining values in a function scope, writing the functions you need gets much easier and then you end up with the nice “railway”
type errors =
| DuplicatedArgumentError(array<string>)
| UnrecognizedArguments(array<string>)
type mode = Production | Development
type options = {
cache: bool,
mode: mode,
}
type cmd = {
argv: array<string>,
options: options,
}
let defaultOptions = {
cache: false,
mode: Production,
}
let processArgs = args => {
let argv = String.split(args, " ")
Array.reverse(argv)
{argv, options: defaultOptions}
}
let noDuplicateArgs = cmd =>
switch duplicates(cmd.argv) {
| [] => Ok(cmd)
| duplicates => Error(DuplicateArgumentError(duplicates))
}
let allArgsUsed = cmd =>
switch cmd.argv {
| [] => Ok(cmd)
| items => Error(UnrecognizedArguments(items))
}
let flag = (tag, f) => cmd => {
argv: cmd.argv->Array.filter(x => x !== tag),
flags: if cmd.argv->Array.includes(tag) {
f(cmd.flags)
} else {
cmd.flags
},
}
let getCacheOption = flag("--cached", flags => {...flags, cache: true})
let getModeOption = flag("--dev", flags => {...flags, mode: Development})
let parseArgs = args =>
processArgs(args)
->noDuplicateArgs
->Result.map(getCacheOption)
->Result.map(getModeOption)
->Result.flatMap(allArgsUsed)
->Result.map(cmd => cmd.options)