feat(beam): dual-API bindings — BeamChardata, io_lib, and raw-list variants#128
Merged
Conversation
Fable.Beam is increasingly used to author OTP code, not just call into it (cf. fable-compiler/Fable#4771, pinning a module's Erlang name for OTP behaviours). In that world the chardata the OTP string/uri_string modules return is a feature, not an impurity: it is valid anywhere a binary or iodata is accepted (io:format, gen_tcp:send, Cowboy bodies), and keeping intermediate results unflattened is the idiomatic way to build output and flatten once at the I/O boundary. Add BeamChardata (erased unicode:chardata(), in the BeamList/BeamMap family) with ofString/toString, and a `*Raw` variant of every string function that returns chardata: reverseRaw, padRaw, padDirRaw, padWithRaw, replaceFirstRaw, replaceAllRaw, composeQueryRaw. The default string-returning functions are unchanged; the Raw variants skip the unicode:characters_to_binary flatten and hand back BeamChardata. This supersedes the commented-out ImportAll stubs left in String.fs: reverse and pad returned chardata and so never could be ImportAll members, but they are fully bound now (string form + raw form), so the stubs become one-line pointers to the working functions. Not BeamList<char>, as first sketched: only reverse yields a charlist; pad and replace yield iodata (nested binaries and integers), which BeamList<char> would misrepresent. One honest chardata type covers all of them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nvention Write down the convention BeamChardata introduces: when an OTP function returns a BEAM-native form (chardata, or a raw Erlang list) that is efficient for authoring Erlang but awkward in F#, bind it twice — a flattened/ref-wrapped default under the plain name, and a `*Raw` variant returning the native type (BeamChardata / BeamList<'T>). Includes a "where it applies" table: the chardata axis is done (string:reverse/ pad/replace, uri_string:compose_query); the raw-list axis (maps:keys/values/ to_list, string/binary/re:split, proplists:get_keys) and io_lib:format are identified candidates, not yet implemented. Also cross-referenced from the Quick Reference, the F#->Erlang type table, and the type-choice decision tree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the BINDINGS-GUIDE "Dual API" candidates surfaced in the survey. io_lib (new module IoLib.fs): io_lib:format is the canonical build-a-string- from-a-template function and returns chardata. Bound as `format` (-> string, flattened) + `formatRaw` (-> BeamChardata), mirroring the string module. Raw-list variants: every binding that ref-wraps a native Erlang list into an F# array now also offers a `*Raw` returning the native BeamList<'T>, for BEAM-side use without the new_ref round-trip: maps: keysRaw, valuesRaw, toListRaw string: splitFirstRaw, splitAllRaw binary: splitFirstRaw, splitAllRaw re: splitRaw, splitWithRaw, splitMPRaw, splitPartsRaw proplists: getKeysRaw The default array-returning members are unchanged. BeamList lives in Lists.fs, which the fsproj compiled *after* Maps.fs, so Maps could not reference it. Lists and Maps are mutually independent (no circular use), so Lists.fs now compiles first; Maps.fs opens Fable.Beam.Lists. String.fs and Re.fs likewise gain the open. +13 tests (380 total): raw lists assert the native form matches the array member (length / structural equality), and io_lib asserts the format/formatRaw round-trip. Guide's "where it applies" table updated to reflect what's done. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every bug BROKEN-BINDINGS.md documented is fixed and merged, so a file named "broken bindings" that lists nothing broken is stale and misleading. The forensic account stays in git history and the #123 writeup. Its one reusable lesson not already in the guide — assert the invariant a function guarantees, not an implementation-defined value that varies by OTP release (the binary:referenced_byte_size 5-on-25 / 40-on-27 case) — is now an anti-pattern in BINDINGS-GUIDE.md. The chardata bug class it described is already covered by the Dual-API section; that section no longer links the deleted file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The old comments claimed these "can't be ImportAll members" because they return chardata. That is only true for a `string` return (ImportAll codegen can't insert the unicode:characters_to_binary flatten). A BeamChardata-typed ImportAll member needs no conversion and would compile fine. State the real reason: the string form needs a flatten, so it is a module function; the raw form is exposed as `*Raw` to keep raw variants uniform across the library, not because an interface member is impossible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Captures the concern raised in #128 review: each binding module exposes up to three surfaces — the ImportAll interface (xxx.*, tupled), module helpers (curried), and the *Raw variants. The *Raw layer is justified; the wart is the arbitrary interface-vs-module split, driven by whether a return needs an Emit wrapper rather than by anything a caller sees. Repo-wide, predates the dual-API work. Records options (unify onto curried module functions / onto the interface / document only), recommends the curried-module direction prototyped on String first, and notes the breaking, repo-wide impact (Cowboy + synapse). Hand-off for a follow-up session; no code changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #123. Establishes a dual-API convention for bindings whose OTP function returns a
BEAM-native form (chardata, or a raw Erlang list) that is efficient for authoring Erlang but awkward
in idiomatic F#, and applies it across the library.
The convention (written up in
BINDINGS-GUIDE.md): bind such a function twice — an F#-friendlydefault under the plain name (a flattened
string, or a ref-wrapped'T array), and a<name>Rawvariant returning the BEAM-native type (
BeamChardataorBeamList<'T>) for zero-copy output andinterop with hand-written Erlang.
BeamChardataAn erased
unicode:chardata(), in theBeamList/BeamMapfamily (Types.fs):Not
BeamList<char>, as first sketched: onlyreverseyields a charlist;pad/replaceyieldiodata (nested binaries and integers), which
BeamList<char>would misrepresent. One honest chardatatype covers all of them.
What's bound dual-API
*Raw(BEAM)stringBeamChardatastring:reverse/pad/padDir/padWith/replaceFirst/replaceAll,uri_string:compose_query,io_lib:format(new)'T arrayBeamList<'T>maps:keys/values/to_list,string:split×2,binary:split×2,re:split×4,proplists:get_keysio_libis a new module (IoLib.fs) —io_lib:formatis the canonical build-a-string-from-a-templatefunction and returns chardata, so it gets
format(→string) +formatRaw(→BeamChardata).The existing default bindings are unchanged; every
*Rawis additive.Also fixes (from #123's aftermath)
Supersedes the commented-out
reverse/padImportAll stubs: they returned chardata and never couldbe ImportAll members, but are fully bound now (string + raw), so the stubs became one-line pointers.
Small structural change
BeamList(inLists.fs) was compiled afterMaps.fs, so Maps couldn't reference it. Lists andMaps are mutually independent, so
Lists.fsnow compiles first andMaps.fsopens it (as doString.fs/Re.fs).Verification
Every
*Rawlist test asserts the native form matches its array member (length / structuralequality); every chardata
*Rawtest asserts anisListproof it's unflattened plus atoStringround-trip back to the default.🤖 Generated with Claude Code