From b5449eceedbd5d93ea4bd22e604fd972c46eda43 Mon Sep 17 00:00:00 2001 From: Collin Williams <96917990+bluedragon1221@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:44:48 -0500 Subject: Reversible Programming, html formatting changes, and css tweaks --- article.js | 81 ++++++++++--------- fib.html | 41 +++------- forgejno.html | 36 +++++---- no_ai.html | 80 +++++++++++++------ prose.css | 96 ++++++++++++++++++---- reversible.html | 242 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ reversible.rs | 158 ++++++++++++++++++++++++++++++++++++ soplt.html | 17 ++-- stars.js | 1 + style.css | 5 +- writing.html | 22 +++--- 11 files changed, 629 insertions(+), 150 deletions(-) create mode 100644 reversible.html create mode 100644 reversible.rs diff --git a/article.js b/article.js index 4818a3d..723d97f 100644 --- a/article.js +++ b/article.js @@ -1,53 +1,52 @@ +function addHeader() { + const header = document.querySelector("header"); + + const h1 = document.createElement('h1'); + h1.textContent = document.title; + header.appendChild(h1); + + const time = document.createElement('time'); + const metaDate = document.querySelector('meta[property="article:published_time"]'); + const dateString = metaDate.getAttribute('content'); + const displayDate = (new Date(dateString)).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + timeZone: 'UTC' + }); + time.setAttribute('datetime', dateString); + time.textContent = displayDate; + header.appendChild(time); + + const hr = document.createElement('hr'); + header.appendChild(hr); +} + function addTOC() { - var tocDiv = document.createElement("div"); - tocDiv.classList += "toc"; - var toc = document.createElement("ul"); + const tocDiv = document.getElementById("toc"); + if (!tocDiv) return; - document.body.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => { - var link = document.createElement('a'); - link.setAttribute('href', '#'+heading.id.toLowerCase()); + const tocHeading = document.createElement("h2"); + tocHeading.textContent = "Table of Contents"; + tocDiv.appendChild(tocHeading); + + const toc = document.createElement("ul"); + + document.querySelectorAll('article :is(h1, h2, h3, h4, h5, h6):not(header *, #toc *)').forEach(heading => { + const link = document.createElement("a"); + link.href = "#" + heading.id.toLowerCase(); link.textContent = heading.textContent; - - var li = document.createElement('li'); + + const li = document.createElement("li"); li.appendChild(link); - + toc.appendChild(li); }); - tocDiv.append(toc); - document.body.prepend(tocDiv); - - var styleBlock = document.createElement("style"); - styleBlock.textContent = ` - .toc { - position: fixed; - width: 17rem; - top: 32px; - left: calc(50vw - 37.5ch - 20rem); - padding-right: 0.75rem; - z-index: 1; - - ul { - margin: 0; - padding-left: 1rem; - line-height: 1.4; - } - - li { - margin-bottom: 0.35rem; - font-size: 1.1rem; - } - } - - @media (max-width: 1000px) { - .toc { - display: none; - } - } - `; - document.head.append(styleBlock); + tocDiv.appendChild(toc); } document.addEventListener("DOMContentLoaded", () => { + addHeader() addTOC() }) \ No newline at end of file diff --git a/fib.html b/fib.html index 1d38013..998308e 100644 --- a/fib.html +++ b/fib.html @@ -2,35 +2,13 @@ I accidentally derived a formula for the Fibonacci Sequence + - + -
-

I accidentally derived a formula for the Fibonacci Sequence

-

Published June 11, 2026

-
+
+

Last night my brother pitched me this math problem over dinner (which he later revealed came from this video). It goes something like this:

@@ -71,7 +49,14 @@

Next, since I had no idea how I would go about counting the sequences, I looked at how I could group them, in hoping that would make them easier to count.

Arbitrarily, I decided to group the sequences by how many 2s were in them.

For the 5-stair staircase example, all of the possible sequences of 1s and 2s that sum to 5 look like:

- [1 1 2 1]
[2 1 2]
[1 2 1 1]
[1 1 1 1 1]
[2 1 1 1]
[1 2 2]
[1 1 1 2]
[2 2 1]
+
[1 1 2 1]
+[2 1 2]
+[1 2 1 1]
+[1 1 1 1 1]
+[2 1 1 1]
+[1 2 2]
+[1 1 1 2]
+[2 2 1]

Grouping each sequence by the number of 2s in it yields the table:

@@ -313,6 +298,6 @@

This is the exact same function that appears on ProofWiki!

- + \ No newline at end of file diff --git a/forgejno.html b/forgejno.html index a8a970c..e36b358 100644 --- a/forgejno.html +++ b/forgejno.html @@ -2,23 +2,27 @@ You don't need Forgejo + -
-

You don't need Forgejo

-

published May 3, 2026

-
- -

People - ditching - GitHub after recent issues - are looking to self-hosting Forgejo as an alternative.

+
+
+ +

+ People + ditching + GitHub after + recent issues + are looking to self-hosting Forgejo as an alternative. +

Despite some (poorly disclosed) security issues, I think there's a more fundemental reason why you shouldn't use Forgejo.

If you're just a single person trying to publish your independent coding projects, why do you need: CI, Issue Tracking, Pull Requests, Multi-user account management, etc?

just use cgit.

+
+

How do git forges (like GitHub) even work?

A git forge is basically just a directory of git repositories, served over http or ssh, with a fancy web frontend.

On the server, since the repos don't need to store mutable state (local commits, staged files, etc) they're stored in a special format that's basically just the .git @@ -32,10 +36,10 @@

cgit Setup Guide

The cgit setup process is fairly involved, but if you were planning on self-hosting Forgejo, then you can handle it.

-

I'll be using Caddy as the webserver for this setup, but I'll link tutorials for NGINX and Apache if prefer xml configuration files and manual https certificates ;)

+

I'll be using Caddy as the webserver for this setup, but this could be modified for NGINX and Apache if you prefer xml configuration files and manual https certificates ;)

To keep everything organized, I'll run cgit under a user called "git". Our git repos will be stored in this directory.

- $ useradd -r -d /var/lib/cgit -s /usr/bin/git-shell git +
$ useradd -r -d /var/lib/cgit -s /usr/bin/git-shell git

fcgiwrap

cgit is served as a cgi application.

@@ -45,7 +49,7 @@
  1. Install fcgiwrap with a package manager. This will create two systemd files: fcgiwrap.service and fcgiwrap.socket.

  2. Open fcgiwrap.service in a text editor ($ systemctl edit fcgiwrap.service) and set the User and Group to be our "git" user.

  3. -
  4. Open fcgiwrap.socket ($ systemctl edit fcgiwrap.socket) and add "SocketUser=caddy" and "SocketGroup=caddy" under the [Socket] section. (replace with the correct Caddy user for your system)

  5. +
  6. Open fcgiwrap.socket ($ systemctl edit fcgiwrap.socket) and add "SocketUser=caddy" and "SocketGroup=caddy" under the [Socket] section. (replace with the correct Caddy user for your system)

cgit

@@ -108,8 +112,10 @@ snapshots=tar.gz zip

In return, you get a tiny stack, fewer moving parts, lower attack surface, lower resource use, and a setup you can understand.

For my personal projects, that trade is acceptable.

-
-

Discuss on Bluesky

-
+ + diff --git a/no_ai.html b/no_ai.html index 3a4cabe..3a142a0 100644 --- a/no_ai.html +++ b/no_ai.html @@ -1,30 +1,58 @@ - - Taking a break - - - - -
-

Prolonged access to LLMs is making me stupid. I can't code as fluently as I used to. (and I'm not the only one)

-

- I experienced the drunkening power from the speed at which development is possible with Claude Code (I built an Emacs clone in a day; it was amazing). - But when the tokens ran out, I felt the overwhelming discomfort of not having Claude's help when starting a new project. That feeling was a wakeup call. -

-

- At heart, I am a programmer. Not a manager, and not the CEO of a multi-million dollar corperation. I don't require ultimate productivity and perfection. - To me, what matters most is familiarity with the tools and techniques (languages, libraries, frameworks), and bossing around agents all day does not give me those skils. -

+ + Taking a break + + + + +
+

+ Prolonged access to LLMs is making me stupid. + I can't code as fluently as I used to. + (and I'm not the only one) +

+

+ I experienced the drunkening power from the speed at which development is possible with Claude Code (I built an Emacs clone in a day; it was amazing). + But when the tokens ran out, I felt the overwhelming discomfort of not having Claude's help when starting a new project. + That feeling was a wakeup call. +

+

+ At heart, I am a programmer. + Not a manager, and not the CEO of a multi-million dollar corperation. + I don't require ultimate productivity and perfection. + To me, what matters most is familiarity with the tools and techniques (languages, libraries, frameworks), and bossing around agents all day does not give me those skils. +

-

What this isn't

-

I don't hate LLMs. In fact, I still see them a very impressive tool! I love to see people building awesome things, and I think AI empowers more builders than any other piece of technology. I'm not "falling for the anti-AI hype"

-

People who believe that AI is the right tool for their workflow should keep using it. And who knows? Maybe I'll continue vibecoding in the future. I just don't think AI is the right tool for me, for now.

+

What this isn't

+

+ I don't hate LLMs. + In fact, I still see them a very impressive tool! + I love to see people building awesome things, and I think AI empowers more builders than any other piece of technology. + I'm not "falling for the anti-AI hype" +

+

+ People who believe that AI is the right tool for their workflow should keep using it. + And who knows? Maybe I'll continue vibecoding in the future. + I just don't think AI is the right tool for me, for now. +

-

Taking a Break

-

So I'm taking a break. I'm uninstalling OpenCode and Pihole-ing ChatGPT. My programming language project this summer will be 0% vibecoded.

-

Is it a good idea? I don't even know. How long will it last? I'm not really sure.

-

All I know is that I never want to feel out of place without the help of an LLM ever again. And hopefully this is one way of achieving that.

-
- - \ No newline at end of file +

Taking a Break

+

+ So I'm taking a break. + I'm uninstalling OpenCode and Pihole-ing ChatGPT. + My programming language project this summer will be 0% vibecoded. +

+

+ Is it a good idea? + I don't even know. + How long will it last? + I'm not really sure. +

+

+ All I know is that I never want to feel out of place without the help of an LLM ever again. + And hopefully this is one way of achieving that. +

+ + + diff --git a/prose.css b/prose.css index 9c812fd..bc0ad5d 100644 --- a/prose.css +++ b/prose.css @@ -1,9 +1,12 @@ @import url('https://fonts.googleapis.com/css2?family=Gelasio:ital,wght@0,400..700;1,400..700&display=swap'); @import url('style.css'); -.content { +article { width: 100%; max-width: 75ch; + margin: 0 auto; + position: relative; + z-index: 1; } p { @@ -15,33 +18,27 @@ p { a { color: #4DA3FF; - transition: color 0.2s ease, background-color 0.2s ease; - border-radius: 3px; + text-decoration: none; &:hover { - background-color: #4DA3FF; - color: #121212; - text-decoration: none; + text-decoration: underline; } } -code { - font-family: 'Roboto Mono'; - font-size: 0.9rem; - color: #00ff78; -} - li { + color: #e6e6e6; font-family: 'Gelasio'; -} + line-height: 1.3; + font-size: 1.1rem; -li::marker { - color: #e6e6e6; + &::marker { + color: #e6e6e6; + } } h1 { color: #e6e6e6; - line-height: 1; + line-height: 1.15; margin-bottom: 0; font-family: 'Gelasio'; font-weight: 600; @@ -65,8 +62,24 @@ h3 { font-weight: 600; } -p.comment { +.comment { + color: #888; +} + +hr { + display: block; + height: 2px; + border: 0; + border-top: 2px solid #888; + margin: 1em 0; + padding: 0; +} + +time { + display: block; + font-family: 'Gelasio'; color: #888; + margin-top: 0.5rem; } @media (max-width: 1000px) { @@ -89,6 +102,55 @@ summary { } } +table { + border-collapse: collapse; + margin: 0 auto; + th { + color: #e6e6e6; + text-align: center; + border: solid; + padding: 0 1.5ch; + border-width: 0 0 1px 0; + + &[scope="row"] { + border-width: 0 1px 0 0; + } + } + td { + color: #00ff78; + text-align: center; + padding: 0 1.5ch; + } +} + code .path { color: #888; } + +/* inline code */ +code { + color: #00ff78; + font-family: 'Roboto Mono'; +} + +/* CODEBLOCKS */ +pre { + color: #00ff78; + font-family: 'Roboto Mono'; + + code { + color: #e6e6e6; + } + + kw { color: #6cb8ff; } /* keywords */ + ty { color: #4ec9b0; } /* types */ + fn { color: #dcdcaa; } /* functions */ + mac { color: #c586c0; } /* macros */ + str { color: #ce9178; } /* strings */ + num { color: #b5cea8; } /* numbers */ + com { color: #7f848e; font-style: italic; } + prop { color: #9cdcfe; } /* fields */ + mod { color: #e6e6e6; } /* modules */ + id { color: inherit; } /* local variables */ +} + diff --git a/reversible.html b/reversible.html new file mode 100644 index 0000000..378045f --- /dev/null +++ b/reversible.html @@ -0,0 +1,242 @@ + + + + + + + Reversible Programming Blew my Mind (Reversible Part 1) + + + + + + +
+
+ +

+ Okay, a few days ago I learned about Reversible Programming. + I decided to write a little Rust DSL to implement some of the stuff I learned. + In this article I walk through the code for my implementation and talk about some of the stuff you could do with + it. +

+ +
+ +

Math basis

+

So remember in math class when you learned to take the inverse of a function?

+

For example, say you have y = 3*x + 6.

+

You can take the inverse by solving for x. In this case, the inverse of our function is x = y/3 - 2.

+

The idea behind reversible programming is that there's a specific way to write your code such that it can automatically compute the inverse.

+

This has some wild implications:

+
    +
  • Writing a parser automatically gives you a pretty-printer implementation
  • +
  • Database migrations are only half as scary (writing your up script automatically gives you your down script)
  • +
  • Writing operations on an image (ex. Photoshop clone) gives you free undo and redo.
  • +
+

+ These are just a few of the crazy things you could theoretically do with Reversible Programming. + But how does it actually work? +

+ +

Idea of the implementation

+

+ The idea behind this is that we can build small blocks of computation that we manually code the inverse logic for. + Then we use higher-level combinators to compose these small blocks together. + As long as the building-blocks are reversible and the combinators are implemented correctly, we'll have a fully reversible program. +

+ +

Foundations

+

As any Rust project starts, we'll define some shared behavior that our initial blocks of computation must implement.

+
trait Rev {
+    type Input;
+    type Output;
+
+    fn forward(&self, i: Self::Input) -> Self::Output;
+    fn reverse(&self, o: Self::Output) -> Self::Input;
+}
+ +

+ As a nice starter, let's write a block that would compute addition. + We can model this with a struct and an impl of the Rev trait. +

+
struct Addition<A> {
+    amount: A
+}
+
+impl<A> Rev for Addition<A>
+where
+    A: std::ops::Add<Output = A>
+        + std::ops::Sub<Output = A>
+        + Copy
+{
+    type Input = A;
+    type Output = A;
+
+    fn forward(&self, a: A) -> A {
+        a + self.amount
+    }
+
+    fn reverse(&self, b: A) -> A {
+        b - self.amount
+    }
+}
+ +

+ While the generics might look scary, this is just saying "the input and output can be any type that you can add and subtract with." + Honestly these are light work compared to what's coming up. +

+

Oh, and it will look nicer if we have an easy way to construct an Addition block:

+
fn plus<A>(amount: A) -> Addition<A> {
+    Addition { amount }
+}
+ +

So let's see it in action!

+
fn main() {
+    let operation = plus(5);
+
+    println!("{}", operation.forward(6)); // prints 11 (6 + 5 = 11)
+    println!("{}", operation.reverse(11)); // prints 6 (11 - 5 = 6)
+}
+ +

Obviously nothing special so far. We're just using the inverse of addition, which is subtraction, just like our + trait impl said.

+

I've implemented a few more in the source code:

+
    +
  • neg() negates a number
  • +
  • times(x) multiplies a number by x
  • +
  • discrete(x: Vec<(A, B)>) creates a reversible block out of a set of discrete inputs and outputs.
  • +
+ +

First Combinators

+

+ The first combinator we'll implement is Pipe. + This takes in two Revs and and makes one that applies them in sequential order. +

+
struct Pipe<X, Y>(X, Y);
+impl<X, Y> Rev for Pipe<X, Y>
+where
+    X: Iso,
+    Y: Iso<Input = X::Output>
+{
+    type Input = X::Input;
+    type Output = Y::Output;
+
+    fn forward(&self, a: Self::Input) -> Self::Output {
+        self.1.forward(self.0.forward(a))
+    }
+
+    fn reverse(&self, c: Self::Output) -> Self::Input {
+        self.0.reverse(self.1.reverse(c))
+    }
+}
+ +

+ Additionally, we'll implement Inverse (not to be confused with .reverse()). + This flips the forward and reverse methods. +

+
struct Inverse<X>(X);
+impl<X: Iso> Rev for Inverse<X> {
+    type Input = X::Output;
+    type Output = X::Input;
+
+    fn forward(&self, a: Self::Input) -> Self::Output {
+        self.0.reverse(a)
+    }
+
+    fn reverse(&self, b: Self::Output) -> Self::Input {
+        self.0.forward(b)
+    }
+}
+ +

+ And finally, it will look nicer if we can use a chaining syntax to apply these combinators. + We'll do that using an extension trait: +

+
trait RevExt: Rev + Sized {
+    fn then<N: Rev<Input = Self::Output>>(self, other: N) -> Pipe<Self, N> {
+        Pipe(self, other)
+    }
+
+    fn inverse(self) -> Inverse<Self> {
+        Inverse(self)
+    }
+}
+impl<T: Rev> RevExt for T {}
+ +

Playing around

+

+ So, what can we do with these combinators? + Quite a lot, actually! + Our math example from the beginning is trivial now: +

+
fn main() {
+    let y = times(3).then(plus(6));
+    let x = y.inverse(); // We don't have to know how this gets computed!
+
+    println!("{}", y.forward(8)); // prints 30
+    println!("{}", x.forward(30)); // prints 8
+}
+ +

Here's a little celsius to farenheit calculator that only encodes the transformation one way:

+
fn main() {
+    // F = (C * 1.8) + 32
+    let celsius_to_fahrenheit = times(1.8).then(add(32.0));
+
+    println!("{}", celsius_to_fahrenheit.forward(0.0));   // prints 32.0
+    println!("{}", celsius_to_fahrenheit.forward(100.0)); // prints 212.0
+
+    println!("{}", celsius_to_fahrenheit.reverse(212.0)); // prints 100.0
+
+    let fahrenheit_to_celsius = celsius_to_fahrenheit.inverse();
+    println!("{}", fahrenheit_to_celsius.forward(32.0));  // prints 0.0
+}
+ +

Under

+

+ under is a higher order combinator, which means it's implemented in terms of other combinators. + Under basically says "do a thing, do another thing, then undo the first thing. + With this foundation built up, we can implement it quite easily: +

+
fn under<Op, Val>(op: Op, val: Val) -> impl Rev<
+    Input = Op::Input,
+    Output = Op::Input
+>
+where
+    Op: Rev + Clone,
+    Val: Rev<Input = Op::Output, Output = Op::Output>
+{
+    op.clone().then(val).then(op.inverse())
+}
+ +

+ We can use this to encode "setup operations." + For example, this code takes in a number string, and returns a number string, but does an operation on integers in-between. +

+
fn main() {
+    let num_as_str = discrete(vec![
+        ("one", 1),
+        ("two", 2),
+        ("three", 3),
+        ("four", 4),
+        ("five", 5),
+        ("six", 6)
+    ]);
+
+    let operation = under(num_as_str, add(3));
+
+    println!("{}", operation.forward("three")); // prints "six"
+    println!("{}", operation.reverse("five")); // prints "two"
+}
+ +

Next Up

+

+ Well, that's all I've got for today. + Next time, we'll look at data-structures, like sum and product types, and maybe some other stuff. + The source code for this project so far is available here. + Stay tuned! +

+
+ + + \ No newline at end of file diff --git a/reversible.rs b/reversible.rs new file mode 100644 index 0000000..19e6f98 --- /dev/null +++ b/reversible.rs @@ -0,0 +1,158 @@ +use std::marker::PhantomData; + +pub trait Rev { + type Input; + type Output; + + fn forward(&self, i: Self::Input) -> Self::Output; + fn reverse(&self, o: Self::Output) -> Self::Input; +} + +#[derive(Clone)] +pub struct Addition { + amount: A +} +impl Rev for Addition +where + A: std::ops::Add + std::ops::Sub + Copy +{ + type Input = A; + type Output = A; + + fn forward(&self, a: A) -> A { + a + self.amount + } + + fn reverse(&self, b: A) -> A { + b - self.amount + } +} +pub fn plus(amount: A) -> Addition { + Addition { amount } +} + +// more blocks + +#[derive(Clone)] +pub struct Negate(PhantomData); +impl> Rev for Negate { + type Input = A; + type Output = A; + + fn forward(&self, a: Self::Input) -> Self::Output { + -a + } + + fn reverse(&self, b: Self::Output) -> Self::Input { + -b + } +} +pub fn neg() -> Negate { + Negate(PhantomData) +} + +#[derive(Clone)] +pub struct Multiplication { + amount: A +} +impl Rev for Multiplication +where + A: std::ops::Mul + std::ops::Div + Copy, +{ + type Input = A; + type Output = A; + + fn forward(&self, a: A) -> A { + a * self.amount + } + + fn reverse(&self, b: A) -> A { + b / self.amount + } +} +pub fn times(amount: A) -> Multiplication { + Multiplication { amount } +} + +#[derive(Clone)] +pub struct DiscreteMap(Vec<(A, B)>); +impl Rev for DiscreteMap { + type Input = A; + type Output = B; + + fn forward(&self, a: A) -> B { + for i in self.0.iter() { + if i.0 == a { + return i.1.clone() + } + } + panic!() + } + + fn reverse(&self, b: B) -> A { + for i in self.0.iter() { + if i.1 == b { + return i.0.clone() + } + } + panic!() + } +} +pub fn discrete(vec: Vec<(A, B)>) -> DiscreteMap { + DiscreteMap(vec) +} + +// combinators + +#[derive(Clone)] +pub struct Pipe(X, Y); +impl Rev for Pipe +where + X: Rev, + Y: Rev +{ + type Input = X::Input; + type Output = Y::Output; + + fn forward(&self, a: Self::Input) -> Self::Output { + self.1.forward(self.0.forward(a)) + } + + fn reverse(&self, c: Self::Output) -> Self::Input { + self.0.reverse(self.1.reverse(c)) + } +} + +#[derive(Clone)] +pub struct Inverse(X); +impl Rev for Inverse { + type Input = X::Output; + type Output = X::Input; + + fn forward(&self, a: Self::Input) -> Self::Output { + self.0.reverse(a) + } + + fn reverse(&self, b: Self::Output) -> Self::Input { + self.0.forward(b) + } +} + +pub trait RevExt: Rev + Sized { + fn then>(self, other: N) -> Pipe { + Pipe(self, other) + } + + fn inverse(self) -> Inverse { + Inverse(self) + } +} +impl RevExt for T {} + +pub fn under(op: Op, val: Val) -> impl Rev +where + Op: Rev + Clone, + Val: Rev +{ + op.clone().then(val).then(op.inverse()) +} \ No newline at end of file diff --git a/soplt.html b/soplt.html index 0835c4b..a80c81e 100644 --- a/soplt.html +++ b/soplt.html @@ -2,14 +2,13 @@ Summer of PLT + - + + \ No newline at end of file diff --git a/stars.js b/stars.js index 347d7c0..cb823ac 100644 --- a/stars.js +++ b/stars.js @@ -40,6 +40,7 @@ document.addEventListener("DOMContentLoaded", () => { position: fixed; inset: 0; pointer-events: none; + z-index: 0; } .star { diff --git a/style.css b/style.css index 0172983..19c33e7 100644 --- a/style.css +++ b/style.css @@ -6,15 +6,12 @@ html, body { background-color: #121212; - margin: 0; + margin: 0 auto; padding: 0; min-height: 100vh; } body { - display: flex; - justify-content: center; - align-items: flex-start; padding-top: 2rem; padding-left: 2rem; padding-right: 2rem; diff --git a/writing.html b/writing.html index 83dcf5f..dcb6d52 100644 --- a/writing.html +++ b/writing.html @@ -8,24 +8,22 @@ - + -- cgit v1.3.1