Heroes / Esempi / pipeline

examples/pipeline/

Un programma che legge righe di testo e produce un report usando due moduli con lo stesso nome.

I due moduli si chiamano source e si trovano in cartelle diverse. Quando li usa, il programma assegna un nome alternativo a uno dei due, così può distinguere le rispettive funzioni.

Eseguilo

$ heroes run examples/pipeline/main.hero
lines: 4
longest: 65
heading: # A short note
lines: 0
longest: 0
heading: none

exit 0 · 6 righe

Il programma

# Lines in, a report out — and the program that shows what `as` is FOR.
#
# Corpus and regression material (CLAUDE.md §9). The gap was measured on
# Measured: `use ... as` appeared **once** in all of `examples/`, in
# `examples/shapes/main.hero`, where it renames a module that did not have to
# be renamed. That use is real and it is not the feature's warrant.
#
# **THIS ONE CANNOT BE WRITTEN WITHOUT IT.** The program reads from
# `read/source` and writes to `write/source`. Both paths end in `source`, so
# both would bind `source` in this file, and `spec:11` refuses a file that
# binds one name twice. There is no single rename that fixes it, because the
# collision is between two modules and not between a module and a local: one
# of them must be called something else HERE, which is exactly the case panel
# 100 recorded as the alias's justification — *"two published modules ending
# in `client`, whose authors own neither the other's files nor a manifest to
# put a rename in"*. The two directories here stand in for those two
# packages.
#
# Panel 101 then refused binding the whole joined path (`read_source` and
# `write_source`, which would need no alias) on the one argument no other seat
# made: the last-part rule is reversible and the joined rule is not. This
# program is what the refusal costs, written out — one `as` clause — and that
# is the trade the record says was taken deliberately.
#
# What it also pins, since a program that exists may as well earn twice: a
# drive loop over a `T?` rather than over an index, `break` on a condition
# inside it, and `chars()` doing the splitting in the reader.

use read/source as input
use write/source as output

# The report: how many lines, how long the longest was, and the first line
# that looked like a heading.
function report(text: str) -> str
    r: input.Reader @= input.new_reader(text)
    s: output.Sink @= output.new_sink()
    longest: i64 @= 0
    lines: i64 @= 0
    heading: str @= ""

    while true
        one = input.next_line(@r)

        if one.is_err()
            # The only way out: the reader said it is past its last line.
            break
        row = one.must()
        lines @ lines + 1

        if row.len() > longest
            longest @ row.len()

        if heading.len() == 0 && looks_like_heading(row)
            heading @ row

    output.emit(@s, "lines: " + lines.to_str())
    output.emit(@s, "longest: " + longest.to_str())

    if heading.len() > 0
        output.emit(@s, "heading: " + heading)
    else
        output.emit(@s, "heading: none")

    return output.rendered(s)

# A heading is a line whose first character is `#`, which is the one
# convention this program invents and the only one it needs.
function looks_like_heading(row: str) -> bool
    if row.len() == 0
        return false
    first: str @= ""

    for ch in row.chars()
        if first.len() == 0
            first @ ch

    return first == "#"

# A `str` is single-line (`spec:18`), so the sample is one literal with its
# newlines escaped rather than a block. That is not a limitation worked
# around: it is why `\n` is one of the six escapes the spec allows.
constant SAMPLE: str
    "# A short note\n\nIt has a blank line in it, and a line longer than all the others.\nThen a short one.\n"

function main()
    print(report(SAMPLE))
    print(report(""))

test "the report counts what the reader hands over"
    said = report("a\nbb\nccc\n")
    assert said == "lines: 3\nlongest: 3\nheading: none"

test "a heading is found once and the first one wins"
    said = report("# one\n# two\n")
    assert said == "lines: 2\nlongest: 5\nheading: # one"

test "empty text reports nothing rather than failing"
    assert report("") == "lines: 0\nlongest: 0\nheading: none"

test "a blank line counts as a line and is not a heading"
    said = report("\n\n")
    assert said == "lines: 2\nlongest: 0\nheading: none"

test "the two modules are two modules, and this file calls them by two names"
    # The aliases are not cosmetic: `input.Reader` and `output.Sink` are
    # different types from different files whose paths end in the same word.
    r: input.Reader @= input.new_reader("x\n")
    s: output.Sink @= output.new_sink()
    assert input.remaining(r) == 1
    assert output.count(s) == 0
    output.emit(@s, input.next_line(@r).must())
    assert output.rendered(s) == "x"
examples/pipeline/main.hero, 112 righe
read/source.hero · 62 righe
# Where a record comes FROM. One of two modules in this program whose path
# ends in `source`, which is the whole reason `main.hero` writes `as`.
#
# A reader hands out lines and remembers how far it has got. Nothing here
# touches a file: the text arrives as a `str` so the program is the same on
# every platform and the corpus can run it three ways.

record Reader
    lines: [str]
    at: i64

function new_reader(text: str) -> Reader
    rows: [str] @= []
    piece: str @= ""

    for ch in text.chars()
        if ch == "\n"
            rows @ rows.push(piece)
            piece @ ""
        else
            piece @ piece + ch

    if piece.len() > 0
        rows @ rows.push(piece)

    return Reader(lines: rows, at: 0)

# The next line, or a failure once there are none — which is what makes the
# drive loop in `main.hero` a `while` over a `T?` rather than over an index.
function next_line(@r: Reader) -> str?
    if r.at >= r.lines.len()
        return fail(code: "at_end", msg: "the reader is past its last line")
    one = r.lines[r.at]
    r.at @ r.at + 1
    return ok(one)

function remaining(r: Reader) -> i64
    return r.lines.len() - r.at

test "a reader walks its lines once and then fails"
    r: Reader @= new_reader("a\nb\n")
    assert r.remaining() == 2
    assert next_line(@r).must() == "a"
    assert next_line(@r).must() == "b"
    assert next_line(@r).is_err()
    assert r.remaining() == 0

test "a last line with no newline is still a line"
    r: Reader @= new_reader("only")
    assert r.remaining() == 1
    assert next_line(@r).must() == "only"

test "empty text has no lines at all"
    r: Reader @= new_reader("")
    assert r.remaining() == 0
    assert next_line(@r).is_err()

test "a blank line in the middle is kept"
    r: Reader @= new_reader("a\n\nb\n")
    assert r.remaining() == 3
    assert !next_line(@r).is_err()
    assert next_line(@r).must() == ""
examples/pipeline/read/source.hero, 62 righe
write/source.hero · 36 righe
# Where a report GOES. The second module whose path ends in `source`, and the
# reason this program is the one that shows what `as` is for.
#
# `spec:11` says a file may not bind one name twice, and two files may name
# the same module differently. Both halves are load-bearing here: `main.hero`
# binds `read/source` and `write/source` in one file, so one of them MUST be
# renamed, and there is no rename that would work for both — which is panel
# 100's recorded warrant for the feature, in a program rather than in prose.

record Sink
    rows: [str]

function new_sink() -> Sink
    empty: [str] = []
    return Sink(rows: empty)

function emit(@s: Sink, row: str)
    s.rows @ s.rows.push(row)

function rendered(s: Sink) -> str
    return join(s.rows, "\n")

function count(s: Sink) -> i64
    return s.rows.len()

test "a sink keeps what it is given, in order"
    s: Sink @= new_sink()
    emit(@s, "first")
    emit(@s, "second")
    assert s.count() == 2
    assert s.rendered() == "first\nsecond"

test "an empty sink renders as nothing"
    s: Sink = new_sink()
    assert s.count() == 0
    assert s.rendered() == ""
examples/pipeline/write/source.hero, 36 righe

impronta sha256 1d944f23127d · 4 file · index.json

Successivo: sqlite. Tutti gli esempi. ← Precedente: tally

Eseguine uno: i quattro comandi. Impara il linguaggio, oppure leggi la specifica completa in un solo file.