19. Worked example - Todo CLI
The full v0.1-runnable form lives in examples/v0_1/todo/ - hanki run examples/v0_1/todo/ drives the REPL; hanki test examples/v0_1/todo/ runs the parse-layer assertions. The condensed listings below mirror those files. One v0.1 detail worth flagging:
main!receives its argv asList<string>viahanki run path -- arg1 arg2 …- the--separator distinguishes program arguments from anyhanki runflags. The Todo CLI accepts the parameter for spec conformance but doesn't read it.
# === types.hk ===
@derive(Eq)
struct Todo
id: i32
text: string
done: bool
end
@derive(Eq)
type Command
Add(string)
Done(i32)
List
Quit
end
@derive(Eq)
type CommandError
NotFound(i32)
Parse(string)
end
impl Display<Todo>
def to_string(self) -> string
marker = if self.done then "[x]" else "[ ]" end
"#{marker} ##{self.id} #{self.text}"
end
end
impl Display<CommandError>
def to_string(self) -> string
match self
NotFound(id) -> "todo ##{id} not found"
Parse(msg) -> "parse error: #{msg}"
end
end
end
# === parse.hk ===
open option
open types
def parse_command!(line: string) -> Command [throws CommandError]
if line == "quit"
Quit
elif line == "list"
List
elif line.starts_with("add ")
Add(line.slice_from(4))
elif line.starts_with("done ")
match i32.parse(line.slice_from(5))
Some(id) -> Done(id)
None -> throw Parse(line)
end
else
throw Parse(line)
end
end
# === todos.hk ===
# `open types` claims bare `List` for the `Command::List` variant (a bare
# use through a second open exporting it would be ambiguous, H0306), so
# the container type is reached qualified as `list.List`.
use list
open option
open types
actor Todos
state items: list.List<Todo> = []
state next_id: i32 = 1i32
on add(text: string) -> ()
items = items.append(Todo(id=next_id, text=text, done=false))
next_id = next_id + 1i32
end
on mark_done(id: i32) -> () [throws CommandError]
found = match items.find(|t| t.id == id)
Some(_) -> true
None -> false
end
if not found
throw NotFound(id)
end
items = items.map |t|
if t.id == id
Todo(id=t.id, text=t.text, done=true)
else
t
end
end
end
on list_all() -> list.List<Todo>
items
end
end
# === main.hk ===
use list
use parse
use todos
open io
# `open` collision note: both `list` and `types` export a bare `List`
# (parametric collection vs the Command::List variant). Opening both and
# using the name bare would be ambiguous (H0306); here only `types` is
# opened, so it claims `List` for the variant and the `list.List<string>`
# type for argv is reached qualified.
open types
def main!(args: list.List<string>) -> () [io, throws actor.SendFailed]
todos_actor = spawn todos.Todos
loop print!("> ")
match read_line!()
Eof -> break
Line(line) ->
try
match parse.parse_command!(line)
Quit -> break
Add(t) -> todos_actor.add(t)
Done(id) -> todos_actor.mark_done(id)
List ->
todos_actor.list_all().each! |t|
print!("#{t}\n")
end
end
catch e: CommandError
print!("#{e}\n")
end
end
end
end