cleaned up appendix and 002

This commit is contained in:
Linus Björnstam 2026-10-26 09:51:10 +01:00
parent 623b68513c
commit d05f4c91dd
2 changed files with 49 additions and 82 deletions

View file

@ -1,54 +1,14 @@
@section{Appendices}
@subsection{Coming from Scheme or ML}
@subsection{Language inspiration}
Bjolang sits at the crossroads of two major functional programming lineages. From the Scheme and Lisp tradition, it draws its uniform S-expression syntax, prefix notation, code-as-data perspective, and hygienic macro system. From the ML family—specifically OCaml and F#—it inherits static Hindley-Milner type inference, algebraic data types, compile-time exhaustiveness checking, traits, and high-performance immutable data structures.
Bjolang is firmly rooted in two large programming language families, and draws inspiration from both. I spent most of my life writing r6rs scheme. I think it is a beautiful language, and it is still often what I reach for to try out new things. The second is ML, mostly ocaml, with it's strong tradition of always managing to select the right abstractions. If you come from any of these languages you will feel at home in one way or the other. Some things I took from both -- like immutability -- will make adherents of both churches feel right at home and some things will feel weird.
Depending on which background you bring to Bjolang, parts of the language will feel like second nature while other parts will require shifting your mental habits.
When deciding on API for bjolang I mostly asked myself "What would ocaml/scheme do?". With ocaml there is a rich set of libraries solving real world problems, so inspiration was easy to find, and from scheme I mostly had a look at racket (which is kind of r6.1rs).
@subsubsection{Coming from Scheme}
A special nod goes to Alex Shinn of Chibi scheme. Both @code{(std fmt)}, @code{(std rx)} and @code{(std scan)} are heavily influenced by programs of his. In fact, the whole idea of the @code{loop} form comes from an earlier project of mine extending his foof-loop.
If you have written Scheme, Racket, or Common Lisp, the visual rhythm of Bjolang will feel immediately comfortable:
@read-list{
- S-expression syntax and prefix notation: Expressions are nested parenthesized lists, and operators are ordinary functions called in prefix position: @code{(+ 1 2 (* 3 4))}.
- Familiar control forms: Core branching and binding forms like @code{let}, @code{let*}, @code{cond}, @code{when}, @code{unless}, and @code{begin} behave as you expect.
- First-class closures: Functions are first-class values created with @code{fun} (or @code{defun} at top level), capturing lexical scope, and passing into or returning from higher-order functions.
- Persistent linked lists: The @code{List} type provides standard singly-linked functional lists with @code{Cons} and @code{Nil}, along with familiar combinators such as @code{list-head}, @code{list-tail}, @code{list-map}, and @code{list-filter}.
- Hygienic macros: Bjolang features a compile-time syntactic macro system based on pattern matching with @code{syntax-match}, syntax quotation with @code{#'(...)}, and explicit identifier injection with @code{inject}.
}
However, several fundamental differences will quickly become apparent:
@read-list{
- Static typing and mandatory top-level signatures: Dynamic typing is absent. While types inside function bodies are inferred automatically using Hindley-Milner type inference, every top-level definition and exported value requires an explicit signature: @code{(: name type)}. Type errors are caught at compile time before any code runs.
- No truthiness tricks or @code{#f}-as-nil: In Scheme, functions like @code{memq} or @code{assoc} return either a value or @code{#f} to signal absence, and @code{#f} is often treated as a general falsey sentinel. In Bjolang, absent values are strictly represented by the @code{(Option %a)} union (@code{Some} or @code{None}), and operations that may fail return @code{(Result %e %a)} (@code{Ok} or @code{Err}). Booleans (@code{#t} and @code{#f}) are strictly booleans; conditionals do not coerce non-boolean values.
- Vectors and maps are persistent by default: In Scheme, vectors are mutable flat arrays. In Bjolang, @code{(Vec %a)} is an immutable Relaxed Radix Balanced (RRB) tree with efficient O(log n) random indexing, updates, and concatenation. Similarly, @code{(Map %k %v)} is an immutable Compressed Hash-Array Mapped Prefix-tree (CHAMP). In-place mutation is never the default: mutable state requires explicit declaration with @code{def/mutable} and modification with @code{set!}.
- Structural pattern matching over @code{car}/@code{cdr}: Destructuring lists, records, and unions is done using the @code{match} construct rather than long chains of @code{car}, @code{cdr}, or @code{cadr}. The compiler checks pattern matches for exhaustiveness and warns or errors if any potential value is unaccounted for.
- Concurrency without @code{call/cc}: Scheme systems often implement coroutines or thread libraries using first-class continuations (@code{call/cc}). Bjolang does not provide @code{call/cc}. Instead, it offers direct-style cooperative fibers (bjoroutines), typed channels, and Concurrent ML (CML) event combinators running on a dedicated fiber scheduler.
}
@subsubsection{Coming from F# or OCaml}
If you come from F#, OCaml, or Haskell, the conceptual model of Bjolang's type system and runtime behavior will feel very natural:
@read-list{
- Hindley-Milner type inference: Types within local expressions and function bodies are completely inferred. Polymorphic functions declare type variables with a leading percent sign, such as @code{%a} or @code{%elem}.
- Algebraic data types: Data structures are modeled as sum types (@code{Union}) and product types (@code{Record} on the heap, or value-type @code{Struct} on the stack).
- Exhaustive pattern matching: The compiler tracks which variants of a union or struct combination have been handled in a @code{match} or @code{def} binding, rejecting patterns that fail to cover every possible case.
- Immutability by default: Data structures, record fields, and bindings are immutable unless explicitly declared mutable.
- Traits and ad-hoc polymorphism: Bjolang's @code{def/trait} and @code{impl} system works much like Haskell typeclasses or Rust traits. Traits can define default method implementations, require super-traits, and specify associated types.
- Direct-style concurrency: Concurrency is written in direct style without callback pyramids, closely resembling F#'s async computations or OCaml 5 effect handlers.
}
The main shifts you will encounter coming from ML are:
@read-list{
- Syntax and parentheses: There is no indentation-based syntax (like F#) or @code{let ... in} keyword framing (like OCaml). Everything is an S-expression with prefix notation. While this takes an afternoon to get used to, it makes code structure uniform and simplifies automated tooling.
- First-class compile-time macros: Rather than relying on external preprocessors like OCaml's PPX or compiler plugins, Bjolang allows writing hygienic macros in the language itself using @code{def/macro}. These transformers run inside the compiler process during the expansion phase.
- Concurrent ML events instead of raw tasks: Rather than manipulating raw asynchronous tasks or promises directly, Bjolang builds upon Concurrent ML. The primitive unit of asynchronous composition is the @code{(Event %a)}. Events can be combined with @code{choose}, wrapped with transformations via @code{wrap}, and selectively synchronized using @code{sync}.
- Automatic colour twins: In many typed asynchronous languages, asynchronous functions have distinct types (@code{Task<'T>} or @code{Async<'T>}) that cannot be passed to standard higher-order functions like @code{List.map} without creating asynchronous duplicates (@code{List.mapAsync}). Bjolang solves this "function colouring problem" at the compiler level: higher-order functions declaring @code{-?->} parameters automatically generate both synchronous and suspending twins, allowing the exact same @code{list-map} to be used whether your callback performs I/O or pure computation.
}
The concurrency story is that of ConcurrentML. A language almost as old as me, that has lived on in various smaller projects. It came to my attention through Andy Wingo, who in his turn got it from the sages of Racket. There are various resources online about it, but I would really recommend watching Andy's speeches about it. There is also a talk circulating online from Mike Sperber talking about it, which can probably be found on the large streaming site.
@subsection{Reading compiler errors}
@ -414,3 +374,17 @@ Key editing features and keybindings include:
- @code{C-c C-o}: Follow the reference at point, opening the target URL or jumping directly to an included file.
- Document navigation: Seamless integration with @code{outline-minor-mode} and @code{imenu} allows folding sections and navigating complex documents through hierarchical headings.
}
@subsection{Why it is a sad world on dotnet}
The reason bjolang runs on dotnet is mostly because of inertia. I had a lot of code already written for c#, and reimplementing RRB trees or CHAMPs in ocaml is nothing I felt like doing. Running on dotnet comes with tradeoffs, most notably function colouring. Had I gone with ocaml5, I would have gotten effect handlers, which means delimited control, which means everything can potentially be a bjoroutine. Now I have async, which means colouring and compiling functions in two ways and sad faces all the way down.
The longer I have come with bjolang, the more I have started looking sideways at ocaml. When it comes to selecting abstractions, ocaml and racket are somehow almost always spot on, which means my main design guideline is "when in doubt, look at ocaml/racket/(guile)". A lot of the things that were not implemented in bjolang are things that simply were too expensive on dotnet, but more or less free on ocaml.
Most of it is delimited control, but having the world's best STM and full effect handlers would make my life a breeze.
@subsubsection{So... when will you switch?}
Probably never. Interop is too nice to have. Just being able to Nuget a nice markdown parser and write some glue is just too damn nice. I did some playing with ocaml, and there are some things I really do not like. Minor gc collections stop all domains. Boxed generics.
But the effect handlers, man...