hanki

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:

# === 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