HN user

ekipan

30 karma

Tinkerer. Forthwright. Nonpracticing Haskell weenie. Author of a dozen very specific unpublished userscripts. Battle-scarred 4channer learning to be constructive.

User of LLMs to improve my writing. The copy and code they give me are shit, but I value the perspective and holes to fill in my copy. The advice to replace prose with examples is always golden.

Lately, though, I've grown a revulsion to LLM sycophancy. It feels bad to use them. Addictive. I might try to detox soon, idk.

I do what I find fun.

Posts1
Comments27
View on HN

Doc excerpts, as a teaser:

  - SDF JKL layout keeps me on home row, ready to type Forth.
  - 39 columns source text to fit the C64 screen.
  - `0 prof` patches the first instruction to an `rts`,
    disabling the profiler. `1 prof` restores the `eor`.
  - The `dup 0= +` phrase ensures nonzero seed, harmless but
    it's cute and I've grown fond of it.
  - Might be able to spread `land` work across frames but the
    complexity isn't worth it.
  - I choose the extra `rdrop` cognitive load just for its
    aesthetic.
  - <!-- It's better left unsaid. -->
I would be tickled if this was the thing to bring at least a few people from vaguely Forth-curious to writing something substantial. Tell me what delights or confuses you. What could be better, what needs examples.

- Function composition was being used as a binary operator between two functions. You just replaced the infix notation with prefix notation.

That's incorrect. Here's the definition of function compose:

  (f . g) x = f (g x)
So in `foldr (+) 0 . map (const 1)`, the author gives `f = foldr (+) 0` and `g = map (const 1)` but doesn't supply `x`. That's a partial application. Similarly for const:
  const x y = x
Even if I concede length and (+), these two are partially applied.

you would only have to use it in partial applications of that function.

So why not const and (.)? If they're allowed to curry, why not foldr and map?

From the article:

    length = foldr (+) 0 . map (const 1)
    length2d = foldr (+) 0 . map length

    -- and the proposed syntax the author calls more readable:
    length = foldr((+), 0, $) . map(const(1), $)
    length2d = foldr((+), 0, $) . map(length, $)
But I don't understand. Why does the author think it's confusing to partially apply foldr and map but not (+), (.), const, and length? Applied consistently:
    length = (.)(foldr((+)($, $), 0, $), map(const(1, $), $), $)
    length2d = (.)(foldr((+)($, $), 0, $), map(length($), $), $)
And clearly no-one thinks this strawman is clearer. Further, it's impossible to make it 100% consistent: any function you write with polymorphic result could instantiate as a function needing more arguments. I think currying is the better default.

Yeah, this. Plus a mistake from the article:

  $ echo '*\n!.gitignore' > build/.gitignore
The \n won't be interpreted specially by echo unless it gets the -e option.

Personally if I need a build directory I just have it mkdir itself in my Makefile and rm -rf it in `make clean`. With the article's scheme this would cause `git status` noise that a `/build/` line in a root .gitignore wouldn't. I'm not really sure there's a good tradeoff there.

"Agreement" of time is probably nonsense, yeah. I realized after posting so I edited in the parenthetical, but as [3] notes, locality probably makes this less of a real issue.

Apparently with the birthday paradox 32 bit random IDs only allow some tens of thousands per second before collision chance passes 50%. Maybe that's acceptable?

[3] https://news.ycombinator.com/item?id=47065241

I forget the context but the other day I also learned about Snowflake IDs [1] that are apparently used by Twitter, Discord, Instagram, and Mastodon.

Timestamp + random seems like it could be a good tradeoff to reduce the ID sizes and still get reasonable characteristics, I'm surprised the article didn't explore there (but then again "timestamps" are a lot more nebulous at universal scale I suppose). Just spitballing here but I wonder if it would be worthwhile to reclaim ten bits of the Snowflake timestamp and use the low 32 bits for a random number. Four billion IDs for each second.

There's a Tom Scott video [2] that describes Youtube video IDs as 11-digit base-64 random numbers, but I don't see any official documentation about that. At the end he says how many IDs are available but I don't think he considers collisions via the birthday paradox.

[1] https://en.wikipedia.org/wiki/Snowflake_ID

[2] https://youtu.be/gocwRvLhDf8

I think everyone agrees with the sentiment but practically speaking that ship sailed decades ago. Browsing the web without uBlock or similar is ill-advised.

Morbidly curious, I turned off my blockers and it's not as bad as I expected honestly. I can still read the article copy.

Oh, it's a list you can add to your uBO. I thought it was a descriptive headline: "(The) uBlock filter list (is going) to hide ..."

I only watch Youtube via browser now for both adblocking and my own custom userscripts, both on my laptop and my phone. A couple creators I watch mostly do shorts so I tolerate them but I wrote a userscript that changes all /shorts/<videoid> URLs into normal /watch?v=<videoid> to lessen the temptation to doomscroll.

It seems to me that AI is mostly optimized for tricking suits into thinking they don't need people to do actual work. If I hear "you're absolutely right!" one more time my eyes might roll all the way back into my head.

Still, even though they suck at specific artifacts or copy, I've had success asking an LLM to poke for holes in my documentation. Things that need concrete examples, knowledge assumptions I didn't realize I was making, that sort of thing.

Sweet Gameboy shader!

I actually have the below clashes function in my bashrc to detect overloaded names, and a few compgen aliases for sorted formatted command lists. Clashes takes a few seconds to chew through the 3000ish commands on my Steam Deck.

I also discovered the comma thing myself, and use it for `xdg-open <web-search-url>` aliases, i.e. `,aw xdg` searches archwiki for "xdg", since the SteamOS on this thing doesn't have manpages.

  alias words='iftty xargs'   # join by spaces
  alias c='compgen -c | sort -u | words' # eg: c | grep ^k | chop
  # ... a for aliases, f for functions etc ...
  alias man='g !archman'      # https://man.archlinux.org
  alias ,aw='g !aw'           # https://wiki.archlinux.org
  alias ,mdn='g !mdn'         # https://developer.mozilla.org
  alias ,nix='g !nixpkgs'     # https://search.nixos.org
  # ... a dozen others ...

  g() { local IFS=+; xdg-open "https://ddg.gg/?q=$*"; }
  iftty() { if [ -t 1 ]; then "$@"; else cat; fi; }

  chop() { # [TABS] [STARTCOL] [CUTOPTS..] # eg. ls ~/.co* | chop
    set -- "${1:-2}" "${2:-1}" "${@:3}"
    cut -c "$2"-$(($2+8*$1-2)) "${@:3}" | column #| less
  }

  clashes() { # [CMDS..] # print overloads eg: clashes $(c)
    (($#)) || set -- $(compgen -A alias -A function)
    local ty cmd; for cmd; do ty=($(type -at "$cmd"))
    ((${#ty[@]}>1)) && echo -n "$cmd "; done; echo
  }

(Edit: in the old post title:) "everything is a value" is not very informative. That's true of most languages nowadays. Maybe "exclusively call-by-value" or "without reference types."

I've only read the first couple paragraphs so far but the idea reminds me of a shareware language I tinkered with years ago in my youth, though I never wrote anything of substance: Euphoria (though nowadays it looks like there's an OpenEuphoria). It had only two fundamental types. (1) The atom: a possibly floating point number, and (2) the sequence: a list of zero or more atoms and sequences. Strings in particular are just sequences of codepoint atoms.

It had a notion of "type"s which were functions that returned a boolean 1 only if given a valid value for the type being defined. I presume it used byte packing and copy-on-write or whatever for its speed boasts.

https://openeuphoria.org/ - https://rapideuphoria.com/

Strange. Middle-clicking the link opens a HackerNews frontpage, but copypasting into a new tab shows the article. Presumably the server shoos away referer links? To reduce load maybe somehow? Or maybe something's weird in my own configuration idk.

Mutability is distinct from variability. In Javascript only because it's a pretty widely known syntax:

    const f = (x) => {
      const y = x + 1;
      return y + 1;
    }
y is an immutable variable. In f(3), y is 4, and in f(7), y is 8.

I've only glanced at this Zen-C thing but I presume it's the same story.

That makes me sad. I also love Posy's content, I'm using his cursors right now, and this new knowledge that there is some of it I cannot see gives me real consumerist FOMO anxiety. A little digging and I couldn't find anything. I don't suppose you remember any keywords?

Shoutouts to SimpleFlips: https://www.wario.style/s/BRwFnLDe

TTYD File Select: https://www.wario.style/s/paznaBKf

Chemical Plant Zone: https://www.wario.style/s/L7kD5jJP

Robo's Theme: https://www.wario.style/s/MzeU9PVc

Everyone's beloved Rick: https://www.wario.style/s/byMyoc3Z

Better MIDI search: <https://bitmidi.com/> I guess you could copypaste the 12345.mid id number into the Wario Synth URL, but it's a bit tedious.

Wanted features: (1) a way to preview the original MIDI again from a share link like this so I could still compare. Searching "ABBA Gimme Gimme Gimme" found one with the title "ABBA Gimme Gimme Gimme L" so I can't be 100% sure it's the same. And (2) a seekbar for the preview as well.

Kinda weird that search results seem hard-capped at 5. I guess it keeps things simple.

Cool enough I suppose, but the framing had me expecting the more farty twisted squarenoise instruments like in Wario Lands 1-3, and less smooth sines and squares. I tried out Kimi No Shiranai Monogatari and Daft Punk's Aerodynamic.

https://www.wario.style/s/yWHphmhO

https://www.wario.style/s/BSN15NEs

Maybe you could list examples for midis you thought sounded cool next time you share this, or in a comment.

Edit: though I guess a huge part of Wario flavor is the dissonant intervals in the music, as much as the farty instrumentation.

A fine sentiment, and would probably even be somewhat enforceable, as most AI slop is pretty obvious, but I suspect a lot isn't. You'd have to decide where to draw the line. How much LLM assistance transitions a work from HackerNews into SlopperNews? And how do you tell if the author (or "author") isn't forthcoming?

I'm pretty sure you aren't terribly serious, but I found it interesting enough to give it a little thought.

Edit: I realize now that my assertion "most AI slop is pretty obvious" could be hubris. I'm not actually very confident any more.

I'm sad that Platine Dispositif pulled their games. Chelsea Has to Beat the Seven Devils to Death (known more widely in English as Bunny Must Die) was a lot of fun and one day I'd like to play some of their others, but they mostly seem to just release on Switch and Playstation stores in recent years. Their site[1] is getting pretty bitrotted, and their blog has an entry from a month ago[2] talking about their next game displayed in Digital Games Expo 2025. I don't immediately see any mention of Steam or intent to return anywhere.

These are in Japanese of course.

[1] https://www.platinedispositif.net/

[2] https://murasame.hatenablog.jp/entry/20251130/1764507793

This is a tangent on language semantics (of the English kind) instead of engaging with the (interesting!) narrative on miscompiles. Feel free to skip.

A compiler is a translator that translates between two different languages.

I lament the word "compile" subsuming "translate" for most of tech. Computers interpret instructions and do them now, and translate instructions from one code to another that is later interpreted or translated again.

The English word "compile" means "bring together" and compiler systems usually have lots of interpreting and translating and linking to make some kind of artifact file, to wit:

taking a step back, a compiler is simply a program that reads a file and writes a file

But not necessarily! You don't even need that much! Just source and target codes.

Forth systems (which I enjoy) for example have a single global STATE variable to switch between _execute this word now_ and _compile a call to this word for later_ directly into memory (plus metadata on words that allow them to execute anyway, extending the compiler, but I digress). You could snapshot the memory of the Forth process with its built target program and reload that way, but the usual Forth way is to just store the source and recompile to memory when needed.

Traditional threaded Forths compile calls to a list of routine addresses for a virtual machine interpreter routine (load address then jump to code, not much more work than the processor already does). I prefer subroutine threading, though, where calls are bone fide CALL instructions and the inner interpreter is the processor itself, because it's easier to understand.

Nowadays even the processor translates the instructions you give it to its own microcode for interpreting. It's code all the way down.

(I'm still reading the article.)

I confess I'm still pretty new to git. I haven't had a very long time to wed myself to git's index and its presence makes me do extra ceremony that I resent whenever I want to polish my graph for my personal projects.

The main difference, it seems, is workflow: git's "prepare then commit" vs jj's "commit then revise".

Currently I do make use of `git add -p` just like grandparent, but I also feel a psychological burden: I will often leave like 3 or 4 separate 2-line changes in the worktree because I don't wanna bother with the ceremony right now, but my perfectionism also resists me putting a lump commit. That's jj's main appeal to me. It amends my work in, then supposedly let's me sculpt it when I'm ready with less of that ceremony.

I've not tried jj yet so I can only speak to impressions, and those impressions appeal to me a lot. My understanding is that jj amends all the changes in _now_ so there's only one place where changes live: the commit graph. No index, no uncommitted worktree. Just commits. And then jj supposedly gives you a lot more freedom to move through and mutate the commit graph directly, rebasing dependent work for you automatically.

So instead of having to `git add -p` several times to pick apart changes from the worktree, the worktree always goes into the graph and you can `jj split` to pick them apart after.

    jj split # split latest commit into two
    jj edit @- # go back one
    jj describe # change message
    jj split # whoops, insert commit in the middle
    jj edit @+ # move forward again
    $EDITOR myfile # do some changes
    jj st # amend the changes and show status
    jj edit def # jump elsewhere in the graph
    jj squash # put two commits together and auto-rebase its descendants
    jj new abc # start working after latest
    # etc etc

Hard agree. Literally every Windows editor I've ever used, except Notepad, allows me to configure it to save CRLF or LF or sometimes even CR files. And I always just leave it on LF because it's easier.

Old Notepad support should not be a priority for a language implementer. Get a real text editor, they're varied and plentiful. This was always a non-issue drudged up by nobody actually interested in using Zig and so it was rightfully ignored.

Size of Life 7 months ago

You could try posting both at the LLM and ask it to explain the stuff I changed, if you're interested in learning more Javascript. Assuming you aren't already deep in the JS trenches and only vibed bc you couldn't be bothered.

Size of Life 7 months ago

Cannot stand robot code. Thanks for the genuinely cool thing though. I hope you don't mind me rewriting, if only for my own satisfaction.

  {
    const s = window.scroller = {}
    const press = (key, code) => () =>
      document.dispatchEvent(new KeyboardEvent('keydown', {
        key: key, keyCode: code,
        which: key, code: code, bubbles: true
      }));
    const step = () => {
      const div = document.querySelector('.animal-name')
      const name = div?.textContent.trim()
      if (name === 'Pando Clone') s.dir = s.left
      if (name === 'DNA') s.dir = s.right
      s.dir()
    }
    s.left = press(37, 'ArrowLeft')
    s.right = s.dir = press(39, 'ArrowRight')
    s.start = (ms) => s.ival = setInterval(step, ms)
    s.stop = () => clearInterval(s.ival)
  }
  scroller.start(5000)