hanki

from_string

stdlib/core/from_string.hk: text to value, the failure type and the trait.

Named for its trait and not for the operation, as option, result and pair are. parse would have been the obvious name and is the wrong one: a core module claims that filename in every Hanki project (a local file may not shadow a stdlib module), and parse.hk is among the likeliest a program picks; this repo's own examples/v0_1/todo already has one.

The seam every parse shares. A type that can be read from text implements FromString, and generic code can therefore ask for one without knowing which type it will get: def read<T: FromString>(s: string) -> Result<T, ParseError> calls T.parse(s) and dispatches through the bound.

The trait is plain: no type parameters, no associated types. A bound may name a trait's type parameters, and a bare bound on a plain trait needs no arity at all, which is what generic code wants from this one. A parameter here would have to be supplied at every bound (H0570 names the arity), and there is nothing for it to carry.

ParseError reports the input alongside the reason because a caller that threaded a parse through a pipeline no longer has the text: the value it started from is the first thing a failure message wants, and reconstructing it at the call site is what every caller was doing instead.

ParseError

struct ParseError
  input: string
  reason: string
end

A string did not parse as the requested shape. input is the text as given; reason describes what was expected or what broke.

impl Display<ParseError>

to_string

def to_string(self) -> string

Renders the failure as cannot parse '<input>': <reason>.

ParseError(input="nope", reason="expected YYYY-MM-DD").to_string() => "cannot parse 'nope': expected YYYY-MM-DD"

impl Eq<ParseError>

eq?

def eq?(self, other: Self) -> bool

Two parse failures are equal when both the input and the reason match.

ParseError(input="x", reason="y").eq?(ParseError(input="x", reason="y")) => true

FromString

trait FromString

Read a value of the implementing type from text. Err reports the input and the reason in place of a bare None, and a failure therefore explains itself without the caller re-deriving what it passed in.

parse

def parse(s: string) -> Result<Self, ParseError>

Parse s into Self, or say why it could not be.

@no-doctest: the trait declares the seam; a doctest needs a concrete impl, and parse_all below is exercised against one in this module's tests

parse_all

def parse_all<T: FromString>(texts: List<string>) -> Result<List<T>, ParseError>

Parse every element of texts, stopping at the first failure. The generic use the trait exists for: the element type is chosen by the caller's annotation and reached through the bound, never named here.

@no-doctest: needs a FromString impl to name, and no stdlib type has one yet; covered by this module's test block against a local impl

Flag

struct Flag
  enabled: bool
end

A local implementor, which exercises the trait's dispatch before any stdlib type has it.

impl FromString<Flag>

parse

def parse(s: string) -> Result<Flag, ParseError>

Reads on or off, and says what it wanted otherwise. It is tiny: what it demonstrates is the dispatch and never the parsing.

Flag.parse("off").map(|f| f.enabled) => Ok(false)
Flag.parse("yes").map(|f| f.enabled) => Err(ParseError(input="yes", reason="expected on or off"))