2025-11-08

Sysadminning in Scala, documenting in Claude


TL; DR: Me and Claude code collaborated on some documentation for my Scala sysadmin library. Is it any good? Maybe other people would use the library?

***

I don't like to do anything very complicated in a shell script. It feels loosey-goosey. I don't quite know what's going to happen, how things will behave if anything goes wrong.

I love doing just about anything in Scala, a programming language as tight and neurotic as I am. When I write Scala, I feel like I have pretty complete command over what happens. I find it easy to precisely check important prerequisites and provide detailed information about whatever goes down.

So I love automating sysadmin tasks in Scala.

Sysadmin tasks are high stakes. Often you are letting some piece of code run as root on production servers, doing something that really does need to be done. So I want to write my "scripts" neurotically. I want to have pretty complete and convenient information about what goes down when they run.

A few years ago I wrote a library in Scala to structure my automation of self-reporting sysadmin tasks. I get colorful HTML e-mails when they run, with a big SUCCEEDED or FAILED in the subject line.

Like most of the software I write, I made my library available as open-source, but had little hope or expectation that anyone else would actually use it.

I certainly didn't put in the time to write documentation that would make it easy for other people to use it!

This week I was messing with the library a bit, and it occurred to me that if I didn't have time to write documentation, maybe Claude would. So I asked Claude code to do just that.

I started with a pretty dumb prompt:

> Could you write a comprehensive piece of documentation, in markdown, for developers interested in using TaskRunner?

Like so much in LLM-land, the result was incredibly impressive but also deflating. Claude code seemed to understand my code very well, and did a good job of explaining it. But there were errors, often small surprising things. Overall, I felt like Claude got all the hard-stuff right, but occasionally whiffed trivialities. Discerning and addressing those small mistakes meant the exercise was somewhat time-consuming.

It was not, to be clear, nearly as time-consuming or stressful as writing documentation by hand would have been. But we did go around with a bunch of revisions, and ultimately I saved less time than I'd initially imagined that I would.

At first I played manager to Claude the copy writer. I offered my observations and corrections in dialogue. Claude dutifully updated the text and fixed the errors.

(At one point, Claude was so attentive to my feedback that it offered to misstate the API in order to conform to my simplifying suggestion! I said no to that change.)

Eventually I went in and did some hand-editing.

The documentation that Claude produced is more long-winded than what I'd have written. But it is also more comprehensive.

Looking at Claude's examples, and how cumbersome they were, inspired me to change the API, significantly and for the better. That process seems like a contribution, even if you can't see it in the final product. (Claude cheerfully revised the code to the new, less cumbersome API, so that's all you see.)

Claude was quite ambitious. It created a vast "Complete Examples" section, just inventing common sysadmin tasks and writing scripts for them. I took that out because they were scripts I couldn't run to verify — they relied on file systems and resources that weren't mine — and just inspecting them I pretty quickly found some problems and bugs. I'd rather have fewer examples than mistaken examples.

Claude agreed removing the section was for the best. But Claude pretty much always agrees.

In any case, while I'm vain enough to think that if I'd written the documentation from scratch it would be better, I think what our collaboration produced is pretty good.

And I would not have taken the time to write the documentation from scratch!

It strikes me that a relatively good outcome for LLM usage would be that, rather than replacing labor, we just do more of things that might prove useful and valuable, but that would previously have been too time-consuming and speculative to justify the opportunity cost.

I'd love it if other people found my library useful. But I think the likelihood that even comprehensive developer docs will bring interest to this project is pretty low. Given that, without Claude, I just wouldn't have devoted the time.

But with Claude, (co)authoring decent if not fabulous documentation is faster and easier than it used to be. So maybe I'll document more of my projects this way. The likelihood that other people find something in my portfolio worth using, that library if not this one, might increase.

It wouldn't be the singularity, but I'd score it a win.

Anyway, here are pointers to the library and its Claude co-authored documentation, if you are interested.

2025-11-06

Zip-A-Dee-Doo-Dah


TL; DR: My two static sites (this site, drafts.interfluidity.com) can now be downloaded in full as zip files, for offline reading or archiving. Zip download links: tech, drafts

***

Last week (during "office hours"), I was asked why I was so fond of static sites and static-site generators for publishing writing. Aren't dynamic content management systems like WordPress or Drupal superior?

I really should have an answer to this! I now devote a large share of my time to an attempt to make static-site generation more functional and accessible for less technical authors.

There are a lot of reasons to prefer static sites. About a month ago, the venerable blogging service TypePad went down, and took with it the archives of some sites that are very important to me, like economistsview.typepad.com and stublingandmumbling.typepad.com.

There is no point making those names links. The sites are gone. Every link to them is broken.

(All is not entirely lost. The author of stublingandmumbling.typepad.com has moved to a substack.)

If these had been static sites, their authors could have simply downloaded the sites in full (as a zip file or something like that), and had them served from anywhere. Pretty much anything on the internet can serve a static site. Lots of services will host static sites essentially for free.

The infrastructure that serves static sites is the brainstem of the internet. WordPress constantly changes, is constantly under attack. It and the database that backs it require maintenance. A static site requires almost no maintenance beyond keeping the web server that hosts it on the internet.

If you (1) have a static site, and (2) own the domain name for the site, then the site is yours. You can move it whenever you want, without breaking links. In the age of the internet, it's so easy to "own" almost nothing. Everything is licensed, you are dependent on some vendor. Even when it's possible, it's often costly and inconvenient to extricate yourself and reconstruct what you've lost. Static sites, however, are freedom.

In addition to being movable, the fact that a static-site is basically a self-contained directory that can be wrapped into a zip file offers benefits to readers, not just to the site's owner. Readers can, at least in theory, download the whole site, for archiving purposes (including to keep the site publisher honest), or to read the work offline.

I've been embarrassed by that "at least in theory". My static sites — this one, and drafts.interfluidity.com — did not, in practice, make it easy for users to download the full site. Technical users have always been able to do so, since both are hosted as public git repositories. But there was nothing as simple as "download the site as a zip file".

So, now there is!

From the archive page of either site (tech, drafts), there are now download links, to a zip file updated nightly if there have been any changes. I've written scripts that maintain a zip directory for each site, that are hit every night by systemd timers, and then a service that provides the latest zip it finds in each of those directories. (As I like to do, this service is a script backed by a library, with the script effectively serving as configuration for the library.)

The zip files behind the links update themselves. The static sites need never change to stay current.

***

p.s. Sometime soon I'd like also to offer more-convenient-for-offline-reading epub files. The static sites now feature full-content, all-item RSS, which should be a straightforward source for building those.

2025-10-28

Turn your Bluesky archive into a readable, hostable static site with fossilphant


A couple of years ago, I wrote fossilphant, a utility for converting archives exported from Mastodon accounts into readable, self-hostable static sites. You can see what those look like here or here or here. (I've wandered across three “instances” on Mastodon!)

Now you can turn your Bluesky repo.car file into a static site as well. It supports embedded images, links, and quote-tweets too!

  1. Install scala-cli. It's very easy!
  2. Download two scripts, download-bluesky-images and fossilphant-bluesky. (To be sure you are using the latest version, you can download them directly from github.)

    Make the scripts executable:

    % chmod +x download-bluesky-images
    % chmod +x fossilphant-bluesky

    Try ./download-bluesky-images --help and ./fossilphant-bluesky --help to get a sense of the many options you won't need to use.

    Note: The first time you hit these scripts, they will download a bunch of stuff. That will only happen once.

  3. Download your repo.car file. In Bluesky go to Settings > Account > Export my data.
  4. Download your images into an images directory:

    % ./download-bluesky-images --repo repo.car --output images

    That may take a minute. When its done, you'll have a lot of images. Check out:

    % ls images
  5. Now, build your static site!

    % ./fossilphant-bluesky --handle=interfluidity.com --images=images repo.car

    Note that you have to provide the DNS handle that you use for your Bluesky account. Your archive does not include this, just your AT-proto "did" ("decentralized identifier").

  6. You'll find a directory called public has now appeared. Open its index.html file and behold!

You can take a peek at my archive here. That's it!

2025-08-30

Updated: The 'iffy' XML namespace


A significant update of The 'iffy' XML namespace was made on 2025-08-30 @ 06:50 PM EDT.

→ Revise definition of curation, so it has only three types: all, selection, and single. iffy:recent becomes a query under selection.

The post was originally published 2024-05-13 @ 04:10 AM EDT.

2025-07-27

Using Tailscale to proxy traffic to the US


I'm traveling, and found that some sites I needed to access forbid traffic from my location. In order to, like, pay a utility bill and renew some insurance, I needed my requests to appear to be coming from the United States.

Obviously, the easy thing would be just be to install one of a zillion VPNs that would let one do this. But they usually involve some subscription fee, this is a rare issue for me and I'm cheap, and I've been curious about Tailscale, which has a free personal-use tier.

Anyway, here's what I did:

  1. Download the Tailscale app on my Mac (from the app store)

  2. Run the app

  3. Authenticate (I chose Apple as authentication provider) and give Tailscale VPN permissions

  4. Install Tailscale on a US-based linux machine

    1. Check the linux version I'm running

      • I know it's a Debian, so run cat /etc/*-release
    2. I'm skittish about curl -fsSL whatever | sh style installations, which Tailscale first suggests for linux, so I ran (the not all that much safer) version-specific Tailscale installation.

    3. The sudo apt-get commands installed a new kernel. I rebooted after this part of the installation.

    4. Running tailscale up prints an authetication URL and then hangs. I copy the URL, then authenticate the Linux machine from my Mac laptop, by pasting it into my browser.

  5. Enable IP forwarding for Tailscale, see the docs

    • For me this was the following, but do read the docs, it's system dependent:
      % echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
      % echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
      % sudo sysctl -p /etc/sysctl.d/99-tailscale.conf   
      
  6. Allow my linux box to become an exit node with tailscale up --advertise-exit-node

    • Note there was no need to do a tailscale down before rerunning tailscale up with the --advertise-exit-node flag
  7. On my Tailscale dashboard (link will only work when you're authenticated into Tailscale) my linux box is visitable and is marked as a potential exit node. Click "..." in the entry for the linux box, then "Edit route settings...", and check "Use as exit node."

  8. In the Tailscale menu visible in the Mac menu bar when Tailscale is running, go to the "Exit Nodes" submenu, then select the linux box

  9. Traffic is now being proxied through the US-located linux machine

  10. Since I don't want (for now) to use Tailscale all the time, I turn Tailscale off:

    1. On the linux box tailscale down

    2. On the Mac, via the Tailscale menu, reset the exit node to "None"

    3. Turn the green "Connected" switch to gray "Not Connected"

    4. Via the Tailscale menu again, quit the Tailscale app

    Hopefully now I am back to ordinary networking.

I think (hope) that at any time, I can rerun the Tailscale app on my Mac, restore the "Not Connected" switch to "Connected", rerun tailscale up --advertise-exit-node on the linux box, then continue from Step 7 above.

On linux tailscale status will tell you whether tailscale is running. I reboot and check, to ensure Tailscale is not set to start-up on boot.

2025-05-26

Zero-ish overhead logging facade in Scala 3


The JVM has a lot of logging libraries. There's the built-in java.util.logging package, the venerable log4j project (now log4j2), logback, Scala-specific libraries like scribe, etc.

As a library author, I've long been partial to logging facades, where you write to one API, and can choose which among the multiple "backends" will log the messages.

The idea was pioneered by Apache Commons Logging. slf4j is I think the most popular logging facade these days. I long ago wrote my own facade, mlog, initially in support of the c3p0 library, but I've used it for many, many projects. I've also wrapped it beneath a concise Scala API that I really like.

JVM logging facades, if written with care, are quite performant in practice. They are not a meaningful bottleneck. Nevertheless, there is a bit of overhead. You call the facade API, bytecode is generated for those calls, which then transform inputs and forward them to a logging library.

I've been intrigued for a while by the possibility in Scala 3 of writing logging facades that eliminate this small bit of overhead. Scala 3 has a souped up inline functionality, by which facade API calls can be transfomed into back-end API calls at compile-time, eliminating any facade overhead at runtime.

I'm building a bunch of "daemon"-style services now, for which logging will be important, so it seemed like a good time to give this a try. The result is called LogAdapter.

The logging API style I developed for mlog-scala, which I am very happy with, is to call log methods directly on logging levels. The messages get routed to "loggers" determined by context (more specifically, by what Scala given or implicit values are in scope). For example, I might call:

SEVERE.log("This is bad.")

SEVERE is a logging level defined in my logadapter package, but which is not defined in log4j, the backend I am using.

Although it looks like I'm calling a method on SEVERE, there is no sign of such a method in the generated bytecode:

     236: aload_0
     237: invokevirtual #103                // Method logAdapter:()Llogadapter/log4j2/LogAdapter;
     240: astore        22
     242: aload         22
     244: astore        23
     246: aload         23
     248: invokevirtual #107                // Method logadapter/log4j2/LogAdapter.logger:()Lorg/apache/logging/log4j/Logger;
     251: astore        24
     253: getstatic     #113                // Field org/apache/logging/log4j/Level.ERROR:Lorg/apache/logging/log4j/Level;
     256: astore        25
     258: aload         24
     260: aload         25
     262: invokeinterface #119,  2          // InterfaceMethod org/apache/logging/log4j/Logger.isEnabled:(Lorg/apache/logging/log4j/Level;)Z
     267: ifeq          284
     270: aload         24
     272: aload         25
     274: ldc           #138                // String This is bad.
     276: invokeinterface #125,  3          // InterfaceMethod org/apache/logging/log4j/Logger.log:(Lorg/apache/logging/log4j/Level;Ljava/lang/String;)V

The bit of bytecode referencing logadapter constructs is just a field lookup, which would be necessary even using the logging libraries directly. (Typically logging libraries reference a "logger" cached as a field of an object.) The field lookup is nested behind two accessors, it's like this.logAdapter().logger().

While in Java, accessing a logger might mean accessing a static field directly, in Scala, even using the native libraries, the field lookup would be behind an accessor. So the only additional overhead is the second, inner accessor call. Using a back-end library directly might provoke only a single hit to an accessor. But the cost of the additional accessor call is negligible.

(And, a bit overzealously, the library actually economizes accessor calls on the backend by caching in ways typical library users do not. Note that logger and Level.ERROR are looked up once, rather than twice, in the bytecode above, although they are ultimately referred to twice, as in the code snippet below. These accessor calls are likely optimized away by modern JVMs at runtime anyway.)

Beyond looking up the logger, the bytecode refers only to construct of the backend library, org.apache.logging.log4j constructs in this case. There is no method call on a logadapter.Level. There are method calls on an org.apache.logging.log4j.Logger supplying an org.apache.logging.log4j.Level as arguments.

It's exactly as if we'd called the following methods directly on the log4j2 library (referencing that library's Level.ERROR, since it does not support a level SEVERE):

import org.apache.logging.log4j.*

if logger.isEnabled( Level.ERROR ) then logger.log( Level.ERROR, "This is bad." )

Again, since logging facades are not really a performance bottleneck, there's not any spectacular improvement over using mlog. In informal testing of just the facade — that is, excluding the cost of actual logging, by sending messages at levels below the loggable threshold — I see a roughly 25% improvement using this resolved-at-compile-time facade over resolved-at-runtime mlog. In practice, the improvement would be even less, because many calls will hit logging IO (or add requests for logging IO to an asynchronous queue), rendering the facades, static and dynamic, a smaller part of the overhead.

Of course one of the benefits of facades — a reason why library authors often choose them! — is precisely because which backend gets hit can be resolved at runtime. If your library will be used as part of a larger application, it's nice if it can be configured to log to the same backend that the larger application will, without having to modify and recompile library code.

But the logadapter facade is so thin, there's very little cost to using it even if its "backend" will be a runtime-configurable facade like slf4j or mlog.

And for most applications, it's fine for the logging backend to be chosen at compile time. In which case the (small) runtime performance hit of a facade can be circumvented almost entirely.

LogAdapter is much simpler and smaller than traditional logging facades, and can be layered on top of them at low cost where that makes sense. I think it will be a pretty good foundation for logging.

2025-05-22

Scala 3 inline vs implicit ordering


I've been playing around with Scala 3's souped up inline construct, which is a very cool, relatively approachable, bit of metaprogramming.

(See "Rock the JVM" for a quick explainer.)

A thing that confused me, though, is the ordering of inlining vs implicit resolution. Consider...

object InlineImplicitOrdering:
  given Int = 10

  inline def printIt(using i : Int) : Unit =
    println(summon[Int])

  object LocalContext:
    given Int = 22

    def printItLocally : Unit = printIt

  @main
  def go() =
    printIt
    LocalContext.printItLocally

It's straightforward that calling printIt prints 10.

It's not so straightforward what LocalContext.printItLocally will do.

Implicit resolution is also a compile-time operation. If implicit resolution happens before the inlining, then LocalContext.printItLocally might print 10. If implicit resolution happens after the inlining is resolved, then LocalContext.printItLocally should print 22.

In reality, it prints 22.

The output of this program is

10
22

I wondered whether this would always be the case, or whether adding modifiers might change this ordering. In particular I know that it's possible to declare inline given, rather than straight given, and I wondered whether this might cause the printIt function to collapse to println(10).

The answer is no. As far as I can tell, there is no set of modifiers that would cause the implicit resolution to occur before the final inlining. Neither inline given, nor transparent inline given, nor marking the implicit argument inline has this effect.

As far as I can tell, the hard and fast rule is that inline resolution is completed prior to any implicit resolution. Implicits will be resolved at the ultimate, inlined call site, and never before.

Which is the behavior I find that I want! I am glad it does not seem to be fragile.

(I'd still like to understand what inline given is for, though.)

2025-04-03

Updated: Neonix


A significant update of Neonix was made on 2025-04-03 @ 09:10 PM EDT.

→ Add update regarding 'gum', ht Bill Mill

The post was originally published 2024-06-06 @ 03:35 PM EDT.

2025-01-28

Adolescent DeepSeek on a Mac laptop


TL; DR: DeepSeek-R1-14B seems pretty "smart". For tasks that, um, don't involve syllable counting, it might be sophisticated enough to be useful. (Or not! It'd take more experimentation to decide.)

But it runs too slowly on my 5-year-old Intel-based MacBookPro, even tricked out with 64GB of RAM. Smaller models run with better performance, but the result quality is likely too poor to be useful. For whatever I do with LLMs, I'm afraid I'm stuck with the online services for now.


In the previous post I ran the "baby" DeepSeek generative AI model on my Mac laptop.

(For instructions on how I installed ollama, and information about its basic use, see that previous post and its postscript.)

The model ran fine, used a lot of CPU but not a lot of memory, generated output at a reasonable pace. Unfortunately, that model is not so "smart". It tells you how it's thinking, and it's not thinking very well. It misunderstands simple things and draws poor inferences.

So now I've decided to try out a still small but bigger, less toy-ish model. The baby model had 1.5B parameters. This one has 14B, so almost 10 times as big.

Downloading the model was trivial with ollama:

% ollama pull deepseek-r1:14b

It's actually a bit less 10 times as big as the baby model, which was about 1GB. Presumably there's some shared, fixed infrastructure, beyond the parameters. (I'm storage constrained, so I'm not retaining models in ~/.ollama, there's just the one model there.)

% du -sh ~/.ollama
8.4G	/Users/swaldman/.ollama

Running the model was also trivial:

% ollama run deepseek-r1:14b
>>> Send a message (/? for help)

I first tried the same prompt as yesterday, "Write a 'Hello, world!' haiku."

The model did run, but...

  1. It ran very, very slowly. The "baby" model ran quite briskly on my laptop. The 14B-parameter version ran like syrup.

  2. The compute load was similar (5-6 full cores saturated), but this model also occupied ~10GB of memory when running. The 1.5B-parameter version used too little memory for me to notice or care.

The 1.5B-parameter model failed to generate a haiku before I grew frustrated and quit it. The 14B-parameter also ultimately failed.

I think I chose a hard "Hello, world!" prompt for all of these models! Haikus are defined by lines with set numbers of syllables. The 1.5B-parameter model mistook letters for syllables. The 14B parameter mostly (but not always!) correctly identified syllables, but often miscounted them.

Even though it too ultimately went 'round and 'round until I interrupted it, the 14B-parameter model's <think>...</think> process was much more relevant and coherent than the 1.5B-parameter model:

>>> Write a 'Hello, world!' haiku.
<think>
Okay, so I need to write a 'Hello, world!' haiku. Hmm, I remember that a haiku is a traditional Japanese poem with three lines. The structure is usually 5 syllables in the first line, then 7 syllables in the second, and ending with another 5 syllables. 

Alright, so the theme here is "Hello, world!" which is pretty much about greeting the world or introducing oneself. I should think of imagery that represents a welcoming or introductory moment.

Let me brainstorm some words related to saying hello and the world. Maybe morning, sunrise, nature sounds like a good setting for a greeting. Dawn could symbolize a new beginning or a fresh start, which fits with saying hello to the world.

For the first line, I want something that captures attention and sets the scene. "Hello, gentle dawn" comes to mind. That's 5 syllables: He-ll-o, gen-tle, dawn. Yep, that works.

Next, the second line needs to be 7 syllables and connect with the first line while expanding on the theme. I want something that shows interaction or response from the world. Maybe "The world whispers back." Let's count: The -world-whis-pers-back. That's 5 syllables? Wait, no. Let me 
break it down.

"The" (1), "world" (2), "whis-pers" (3), "back" (4). Oh, only 4 syllables. Hmm, I need three more. Maybe rephrase it. How about "The world hums a welcome"? That's 6 syllables: The-world-hums-a-wel-come. Wait, that might be stretching it.

Alternatively, "The world greets me softly." Let's see: the (1), world (2), greets (3), me (4), soft-ly (5). No, still not 7. Maybe "The world responds with a smile." That's longer but syllables-wise: The (1), world (2), re-sponds (3), with (4), a (5), smile (6). Still only 6.

Wait, maybe I'm overcomplicating it. Let me think of the action after the hello. Maybe something like "A new day begins." But that's more about the day starting. Alternatively, "Echoes of greeting fill the air." That's longer than 7 syllables though.

Hmm, perhaps "The world says hello too." Wait, let me count: The (1), world (2), says (3), hel-lo (4), too (5). No, that's only 5. I need two more syllables. Maybe "The world hums in reply." That's He-world-hums-in-reply. Let me break it down: the (1), world (2), hums (3), in (4), re ply 
(5 and 6). Wait, no, that's only 6.

Wait, maybe I can use "The world answers back." The (1), world (2), an-sers (3), back (4). That's 4 syllables. Not enough. Hmm.

Alternatively, think about nature sounds. Maybe "Birds sing in reply." That's 7 syllables: Birds (2), sing (1), in (1), reply (2). Wait, that doesn't add up. Let me count each word as syllables:

"Birds" - 1
"sing" - 1
"in" -1
"reply" -2

Total: 5. Not enough.

Wait, perhaps "Birdsong answers back." That's 7 syllables: Bird-song (2), an-swers (2), back (1). Wait, no:

"Birdsong" is two syllables, "answers" is two, "back" is one. Total 5 again.

I'm getting stuck on the second line. Maybe I should think of something else. How about a sunrise? "Sunrise breaks the silence." That's 7 syllables: Sun-rise (2), breaks (1), the (1), si-lence (2). Wait, that adds up to 6 syllables.

Wait, no: Sunrise is two, breaks is one, the is one, silence is two. Total 6. Hmm.

Alternatively, "Gentle breeze returns." That's 7? Gentle (2), breeze (1), re-turns (2). Wait, gentle is two syllables, breeze is one, returns is two: total 5.

Hmm, maybe I'm overcomplicating. Let me try to think differently. Maybe the second line can be about reaching out or communication. "I reach out to you." That's 6 syllables: I (1), rea-ech (2), out (1), to (1), you (1). No, that's only 5.

Wait, maybe "I send a greeting far and wide." That's way too long. Let me think about natural elements again. Maybe water? "Waves whisper hello." That's waves (1), whisper (2), hel-lo (2). Wait: waves (1), whisper (2), hello (2). Total 5 syllables, but I need 7.

Alternatively, "Ripples greet the shore." Ripples (2), greet (1), the (1), shore (1). That's 5 again.

I'm getting a bit stuck. Let me look back at the first line: "Hello, gentle dawn." Maybe the second line can continue that imagery. Dawn brings light, so perhaps something like "Light breaks the silence." That's 6 syllables: Light (1), breaks (1), the (1), si-lence (2). Total 5 again.

Wait, maybe "Dawn greets the morning sky." That's dawn (1), greets (1), the (1), morn-ing (2), sky (1). Total is 6 syllables. Hmm.

Alternatively, "A new day starts with a smile." New (1), day (1), starts (1), with (1), a (1), smile (1). That's 6.

I'm still not hitting that 7-syllable mark for the second line. Maybe I should try something else. How about focusing on the feeling of greeting? "Heart beats in rhythm." Heart (1), beats (1), in (1), rhyth-m (2). Total is 5.

Wait, maybe "Echoes fill the air." Echoes (2), fill (1), the^C

As before, my next step was to see if the model could code a simple "Hello, world!" program in Scala. The 1.5B-parameter model failed abysmally. Even when presented with compiler errors, and then my suggestions to tutor it, it was not making progress towards a correct solution.

The 14B parameter produced a correct program on its first attempt. It's <think>...</think>-ing was not perfect, but was mostly pretty good.

>>> Please write a Scala program that prints "Hello, world!"
<think>
Okay, I need to write a Scala program that prints "Hello, world!". Hmm, where do I start? Well, I know that in Scala, you can print something using the println function. So maybe I just call println("Hello, world!")?

Wait, but how does aScala script work? Do I need to have an object or is it simpler than that? Oh right, for a simple program like this, I think you can just write it in a single line. But wait, when compiling with scalac, do I need to structure it as an object?

Let me think. If I'm writing a script, maybe I can use the main method inside an object. So perhaps the code should be something like:

object HelloWorld {
  def main(args: Array[String]): Unit = {
    println("Hello, world!")
  }
}

Yes, that makes sense. Because when you run a Scala program, it looks for an object with a main method. So this structure is correct.

Alternatively, I remember that in older versions of Scala, you could just write the code without defining an object if using the scalaz script compiler or something like that. But perhaps the more standard way is to define an object with the main method.

So putting it all together, the code should be an object named HelloWorld (or any name really), and inside the main function, we call println with the desired string.

Let me double-check: When I run this program, either by compiling with scalac and then running with scala, or using a Scala IDE, it should print "Hello, world!" correctly. Yeah, that should work.
</think>

To write a Scala program that prints "Hello, world!", follow these steps:

1. **Create a new file** for your Scala program. It can be named `HelloWorld.scala`.

2. **Write the code**: The code will define an object with a main method that calls `println` to output the desired message.

Here's the complete Scala code:

```scala
object HelloWorld {
  def main(args: Array[String]): Unit = {
    println("Hello, world!")
  }
}
``` 

3. **Run the program**:
   - **Using the command line**:
     - Compile the file using `scalac HelloWorld.scala`.
     - Run it with ` scala HelloWorld`.
   - **Using an IDE**: Simply run the script through your preferred Scala IDE.

This program will output:

``` 
Hello, world!
``` 

And so it did.

% scala-cli run hello-ds14b.scala 
Compiling project (Scala 3.6.3, JVM (17))
Compiled project (Scala 3.6.3, JVM (17))
Hello, world!

Since it got it right, I thanked it! Its "thought-process" in response was kind of fun:

>>> Great! Thank you.
<think>
Alright, so the user just thanked me for the previous explanation on writing a "Hello, world!" program in Scala. They were probably satisfied with the answer, but I should think about how to respond appropriately. 

I want to be polite and encouraging, so I'll acknowledge their gratitude. Maybe add an emoji to keep it friendly. Also, since they might be new to programming orScala, offering further help is a good idea. I can ask if there's anything else they need assistance with.

Hmm, perhaps I should also provide options for what they might want next—like solving another problem or explaining more concepts. That way, it shows I'm ready to assist them in various ways.

I should keep the response concise and positive, making sure they feel comfortable asking more questions if needed.
</think>

You're welcome! If you have any other questions or need help with anything else, feel free to ask! 😊

I really want to do whatever AI-using I eventually do on my own infrastructure, or infrastructure of someone I trust, rather than a distant megafirm. But this current family of models, on my current best hardware, won't get me there.

Sometime soon I may upgrade to an Apple Silicon Mac, and then retry these exercises.

I'd consider a cloud server I rent and fully control (as opposed to API access) "my own infrastructure", but cloud servers with the resources of my laptop are too pricey for me, for now. The trendy AI-ready GPU-endowed servers some cloud providers are renting are way beyond my price point.

So, for now, I think I'm stuck with the megafirms, to the degree that I use this stuff at all. But the DeepSeek models are a big step closer towards LLMs more consistent with our independence, and I'm grateful for that.

Baby DeepSeek on a Mac laptop


TL; DR: It's really fun to watch DeepSeek-R1-1.5B, <think>...</think>.

But this tiniest DeepSeek model is, um, not so smart. It seems to misinterpret the prompt input a fair amount, and draws inferences that are not quite coherent, even though in some superficial but nonsensical way you can understand the connections it's drawing.


For the heck of it, I decided to try to run the smallest DeepSeek generative AI model on my laptop. My laptop is an Intel MacBook Pro purchased in 2020 (but I think a 2019 model), 8 cores. It's tricked out with 64GB of memory.

After checking out a DigitalOcean tutorial for clues, Running the model was beyond trivial:

% brew install ollama
% brew services start ollama
% ollama run deepseek-r1:1.5b
pulling manifest 
pulling aabd4debf0c8... 100% ▕██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 1.1 GB                         
pulling 369ca498f347... 100% ▕██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏  387 B                         
pulling 6e4c38e1172f... 100% ▕██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ 1.1 KB                         
pulling f4d24e9138dd... 100% ▕██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏  148 B                         
pulling a85fe2a2e58e... 100% ▕██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏  487 B                         
verifying sha256 digest 
writing manifest 
success 
>>> Send a message (/? for help)

That last line is prompting me for a prompt.

When I run prompts, I find that the model does not stress out my memory at all, but does use up 5 to 6 cores of CPU while it is active.

For the purpose of this post, I tried what I thought would be a simple prompt:

>>> Write a 'Hello, world!' haiku.

The model generates a <think>...</think> section in which it describes its own "thought" process. But it is so neurotic! Endearingly neurotic! It went around in circles. I eventually grew impatient and aborted it with <ctrl-c>.

<think>
Okay, so I need to write a haiku in lowercase for "Hello, world!" that reads backwards both in the traditional way and when reversed again. Let me start by recalling what a haiku is. It's a three-line poem where the first line has five characters, the second eight, and the third five, 
all in iambic pentameter.

First, I'll break down "Hello, world!". The words are H, E, L, L, O, ,, W, O, R, L, D. But since we're writing it in lowercase as "hello, world!", that's h e l l o , w o r l d. Now, for the haiku structure, I need to arrange these into three lines with specific syllable counts and stress 
patterns.

The first line should have five syllables. So I'll pick a part of "hello" that has five letters, counting each vowel as one syllable. H-e-l-ll-o is 1+1+2+2+1=7 syllables. Hmm, too many. Maybe h-e-llo would be better—h (1) + e (1) + l (1) + l (1) + o (1) = 5 syllables.

Wait, but "hello" is H E L L O, so five letters with four consonants and one vowel. So that's correct for the first line: h-e-l-l-o.

Now, I need to make it into a haiku. The second line should have eight syllables, and the third line back to five again. Also, when reversed in both ways, they should form "hello, world!" correctly.

Let me think about how to mirror "hello" in some way. Maybe using symmetrical patterns or mirrors. Alternatively, considering the punctuation: comma and exclamation mark. The second word is a bit tricky with the comma—perhaps that's intentional as part of the haiku structure.

Looking at "hello", the next word after the first line would be two syllables for eight in total, but including the comma it might need to be longer. Maybe h-e-ll-o-, which has four letters plus the comma. That could work: h-e-l-l-o- (four characters) and then another line.

Wait, let me try that. First line: h-e-l-l-o—5 syllables. Second line: h-e-llo- (but wait, h-e-l-l-o is five letters; maybe I'm overcomplicating). Alternatively, the second word could be "ll" with a hyphen or something? But in haikus, usually words are separated by spaces.

Wait, perhaps it's better to think of the syllables. The first line: 1+1+2+2+1=7 syllables—need to adjust to 5. So maybe h-e-l-l-o is correct as five letters, but I need to represent it with five syllables in a four-letter base? Or perhaps use an alliteration.

Alternatively, think about "hello" and mirror the words somehow. Maybe H-e-llo-world? But that's eight letters for two lines and five again. Wait, no—haiku is three lines: 5-8-5.

So first line: h-e-l-l-o (h + e + l + l + o = 5 syllables). Second line needs to have eight syllables. Maybe h-e-ll-o-w (but that's only seven letters; maybe with an exclamation or hyphen? Alternatively, think of the second word as a two-syllable word but longer in terms of letters.

Wait, perhaps I'm approaching this wrong. Let me try to mirror "hello" around the center. So H E L L O would be mirrored as D O L L E, but that doesn't fit into the haiku structure. Alternatively, think of it in a way where each word relates somehow.

Alternatively, consider that the second line is a contraction or reflection. Maybe h-e-ll-o-, then w-o-r-l-d. But I'm not sure about the syllables here. Let me count:

First line: h-e-l-l-o → 5 syllables.
Second line: h-e-llo-w (h + e + l + l + o + w → six letters, which is more than eight; maybe this approach isn't right.)

Alternatively, perhaps using alliteration or a more specific pattern. Let me think of "hello" and how it can be mirrored with the second word being two syllables but having more letters.

Wait, another approach: The first line is 5 syllables (h-e-l-l-o), then for eight, maybe h-e-ll-o-w (but that's six characters; perhaps including a hyphen or something). Alternatively, think of "hello" and "world" as related in some way, like the second word being two syllables but 
longer.

Wait, let me consider "h-e-llo-world"—but no, it would be 12 letters. Maybe h-e-ll-o-w (but I'm getting confused with syllable counts).

Alternatively, perhaps think of each word as having a certain number of stressed vowels or consonants. For example, in the second line, the stress pattern might require two stressed syllables.

Wait, maybe it's better to look for existing haikus and see if any are similar to "hello" that can be mirrored. But since I'm supposed to create this from scratch, let me try again.

First line: h-e-l-l-o → 5 syllables.
Second line needs to have eight syllables. Maybe h-e-llo-w (h + e + l + l + o + w = six letters) but that's seven characters with an exclamation mark—maybe h-e-ll-o-w? Let me check the stress pattern for "hello". The word is h-e-l-l-o, which has four consonants and one vowel. So it 
sounds like a single syllable maybe.

Wait, no: 'Hello' has two syllables: H-e and llo, but in reality, it's pronounced as one syllable. But I need to make the haiku with five syllables for the first line.

Alternatively, perhaps h-e-ll-o would have four consonants, so that's 4 letters which is too few for a five-syllable word. Wait, no: h (1) + e (2) + l (3) + l (4) + o (5). So it's five syllables.

So first line: H E L L O → 5 syllables.

Second line needs to have eight syllables. Maybe h-e-ll-o-w? Let me check the syllable count for "hello, world": two lines of 5 and 8 respectively. Wait, no—the haiku is five, then eight, then five. But in this case, first line: hello (h-e-l-l-o) → five syllables.

Second line: h-e-ll-o-w? Let's see: h (1), e (2), l (3), l (4), o (5), w (6). That's six letters, which would be eight syllables if each word is broken down. Wait, that doesn't add up.

Alternatively, perhaps "h-e-ll-o-w" as the second line but considering more syllables. Hmm, I'm getting stuck here. Maybe I should look for existing haikus in the style of "hello, world!" and see how they mirror each other.

Wait, another idea: The second word is two syllables, so maybe h-e-ll-o-w? That's six letters but seven characters (including the comma). Alternatively, perhaps it's h-e-llo-w with a hyphen or something. But I'm not sure.

Alternatively, think of "h-e-ll-o" as one line and then mirroring with "w-o-r-l-d" in reverse. So maybe h-e-ll-o-, w-o-rld? Let me check the syllables:

First line: h-e-l-l-o → five syllables.
Second line: h-e-ll-o-w (h + e + l + l + o + w = six letters) but that's seven characters with a comma. Maybe hyphenated or something else, but it's tricky.

Alternatively, perhaps the second word is "h-e-llo", which is four syllables, and then "world" as two? No, that doesn't fit.

Wait, maybe I'm overcomplicating this. Let me try to write the lines step by step:

First line: h-e-l-l-o → 5 syllables.
Second line needs to be eight syllables. Maybe h-e-ll-o-w (six letters) but with a hyphen or something, making it seven characters. Alternatively, think of "h-e-ll-o-w" as the second word, which would make the entire line longer.

Alternatively, maybe I'm supposed to mirror the words somehow. So H-E-L-L-O could be mirrored around the center, perhaps h-e-llo-world, but that's too long.

Wait, another idea: The first line is 5 syllables, and the second word is two syllables with more letters. For example, h-e-ll-o (five letters) vs. w-o-r-l-d (but that would be in reverse). Alternatively, think of "h-e-ll-o" as one line and then "world" mirrored.

Wait, perhaps I'm stuck. Let me try to write the second line as h-e-ll-o-w with a hyphen or something, making it 6 characters. But no, haikus have strict syllable counts.

Alternatively, think of each word in terms of syllables: first line is five, second is eight, third is five.

So second line needs to be eight syllables. Maybe h-e-llo-w (h + e + l + l + o + w = six letters) but that's seven characters. Alternatively, maybe "h-e-ll-o" and "w-d", making it two lines: h-e-ll-o- and w-o-rld? No, that doesn't make sense.

Wait, I'm getting stuck here. Let me try to write the second line as h-e-llo-w with a hyphen in the middle or something, but^C

>>>

Then I tried a simpler variation:

>>> Write me a joyful greeting, please?
<think>
Okay, so I just received this message: "Write me a joyful greeting, please?" Hmm, that's interesting. At first glance, it sounds like someone is trying to ask for a greeting but maybe they don't realize how important it is. Let me try to unpack what's going on here.

So, the person wrote a greeting that ends with a question mark. That's a bit unusual because usually, greetings are polite and direct without any questions. Maybe they're testing something or just curious about why they're asking for this specific type of greeting. Perhaps they want to 
see if I can recognize different types of requests.

I should consider what kind of response they might expect. Since it ends with a question mark, maybe the next step is to acknowledge their request and provide a friendly reply. But how? A simple "Yes! Thank you for your kindness!" would be appropriate. It's concise and acknowledges their 
message without being repetitive or too formal.

I wonder if there are any underlying reasons behind their query. Maybe they're trying to get my attention quickly or set a context where I can respond further. Without more information, it's hard to say exactly what they need next, but focusing on a heartfelt greeting seems like the best 
approach right now.

Also, considering the tone, it should feel warm and caring. Using words that convey gratitude and appreciation is key here. Phrases like "I appreciate your kind words" or "Thank you for your kind gesture" can make the response more genuine.

Another angle to think about is the purpose of the greeting. If this is part of a larger conversation, maybe they need something specific for that context. But since I don't have any additional details, it's safer to keep it general and flexible.

I should also consider if there are alternative greetings that might be more suitable based on their message. For example, "How can I help you today?" or "Could you please let me know how I can assist you?" Each of these has its own merits, but the original question ends with a question 
mark, so focusing back on a simple, heartfelt response makes sense.

In summary, my approach should be to acknowledge their message, provide a genuine reply that shows appreciation, and keep it concise. This way, they know I'm listening without overcomplicating things.
</think>

Certainly! Here's a heartfelt greeting in the requested format:

---

Hey there! I hope you're doing well today. It sounds like your kind words are always appreciated—thanks for taking the time to connect with me!

---

This response is warm, genuine, and acknowledges their message while keeping it concise.

A bit off. But not terrible!

Next I tried to see if it could code:

>>> Please write a Scala program that prints "Hello, world!"

This... didn't go so well.

To be fair, Scala is not likely to be its most prominant training language, and some of its attempts could be excused as not quite knowing whether to write Scala 2 or Scala 3. Its first attempt was closer to Scala 3 than Scala 2. (It offered a naked top-level function, supported as main entry points only in Scala 3.) So I tried to tutor it towards s Scala 3 solution.

It came pretty close! But after three or four back and forths over ~25 minutes, it still wasn't quite right and I gave up.

This is the tiniest DeepSeek model. I wonder if I can run a bigger one, and if it performs a bit better?

Stay tuned on that, I guess.


Postscript:

I'm pretty short storage space on the ol' laptop, so I want to clean up after this exercise. A bit of searching tells me that on a Mac, models will be stored under a ~/.ollama directory.

% find ~/.ollama 
/Users/swaldman/.ollama
/Users/swaldman/.ollama/id_ed25519
/Users/swaldman/.ollama/id_ed25519.pub
/Users/swaldman/.ollama/models
/Users/swaldman/.ollama/models/blobs
/Users/swaldman/.ollama/models/blobs/sha256-6e4c38e1172f42fdbff13edf9a7a017679fb82b0fde415a3e8b3c31c6ed4a4e4
/Users/swaldman/.ollama/models/blobs/sha256-369ca498f347f710d068cbb38bf0b8692dd3fa30f30ca2ff755e211c94768150
/Users/swaldman/.ollama/models/blobs/sha256-aabd4debf0c8f08881923f2c25fc0fdeed24435271c2b3e92c4af36704040dbc
/Users/swaldman/.ollama/models/blobs/sha256-a85fe2a2e58e2426116d3686dfdc1a6ea58640c1e684069976aa730be6c1fa01
/Users/swaldman/.ollama/models/blobs/sha256-f4d24e9138dd4603380add165d2b0d970bef471fac194b436ebd50e6147c6588
/Users/swaldman/.ollama/models/manifests
/Users/swaldman/.ollama/models/manifests/registry.ollama.ai
/Users/swaldman/.ollama/models/manifests/registry.ollama.ai/library
/Users/swaldman/.ollama/models/manifests/registry.ollama.ai/library/deepseek-r1
/Users/swaldman/.ollama/models/manifests/registry.ollama.ai/library/deepseek-r1/1.5b
/Users/swaldman/.ollama/history
% du -sh ~/.ollama 
1.0G	/Users/swaldman/.ollama

It's not huge, but I'm going to remove it anyway for now.

% ollama help
Large language model runner

Usage:
  ollama [flags]
  ollama [command]

Available Commands:
  serve       Start ollama
  create      Create a model from a Modelfile
  show        Show information for a model
  run         Run a model
  stop        Stop a running model
  pull        Pull a model from a registry
  push        Push a model to a registry
  list        List models
  ps          List running models
  cp          Copy a model
  rm          Remove a model
  help        Help about any command

Flags:
  -h, --help      help for ollama
  -v, --version   Show version information

Use "ollama [command] --help" for more information about a command.

% ollama list
NAME                ID              SIZE      MODIFIED
deepseek-r1:1.5b    a42b25d8c10a    1.1 GB    About an hour ago

% ollama rm deepseek-r1:1.5b
deleted 'deepseek-r1:1.5b'

% du -sh ~/.ollama
 12K	/Users/swaldman/.ollama

All cleaned up!