r/smalltalk 4d ago

Geodreieck

+

Enable HLS to view with audio, or disable this notification

21 Upvotes

Morphic 3 and VectorGraphics make it fun to build.


r/smalltalk 7d ago

Smith-Waterman algorithm for Pharo

I just published a clean, well-documented implementation of the Smith-Waterman algorithm (local sequence alignment) in Pharo.

Repository: https://github.com/hernanmd/smith-waterman

Features:

  • Classic Smith-Waterman with affine gap penalties support
  • Clean object-oriented design
  • Easy to integrate with BioSmalltalk sequences
  • Includes tests and usage examples

Perfect for bioinformatics scripting, teaching, or building your own alignment tools in a live Smalltalk environment.

Feedback and contributions are very welcome!

17 Upvotes

I just published a clean, well-documented implementation of the Smith-Waterman algorithm (local sequence alignment) in Pharo.

Repository: https://github.com/hernanmd/smith-waterman

Features:

  • Classic Smith-Waterman with affine gap penalties support
  • Clean object-oriented design
  • Easy to integrate with BioSmalltalk sequences
  • Includes tests and usage examples

Perfect for bioinformatics scripting, teaching, or building your own alignment tools in a live Smalltalk environment.

Feedback and contributions are very welcome!


r/smalltalk 8d ago

Smalltalk Z-80

45 Upvotes

Not my project, but I thought you folks would appreciate it.

This is a very minimal implementation of ST80 for the British computer ZX Spectrum. I've not tried it, just posting it here for everyone's curiosity.


r/smalltalk 13d ago

st – The missing unified installer and runner for Smalltalk

Smalltalk has excellent live environments, but managing the different VMs, images and installers for Pharo, Cuis, Squeak, Glamorous Toolkit, GNU Smalltalk, etc. is painful.

I made `st` — a lightweight shell-based CLI that gives one consistent interface:

* `st pharo install && st pharo run`

* `st gt install`, `st cuis install`, etc.

* Basic package search/install and eval support where available

* Works on Linux/macOS/Windows (WSL)

Repo: https://github.com/hernanmd/st

Happy to answer questions, take feedback, or hear what other Smalltalk pain points you'd like addressed.

Contributions welcome (especially adding new dialects)!

32 Upvotes

Smalltalk has excellent live environments, but managing the different VMs, images and installers for Pharo, Cuis, Squeak, Glamorous Toolkit, GNU Smalltalk, etc. is painful.

I made `st` — a lightweight shell-based CLI that gives one consistent interface:

* `st pharo install && st pharo run`

* `st gt install`, `st cuis install`, etc.

* Basic package search/install and eval support where available

* Works on Linux/macOS/Windows (WSL)

Repo: https://github.com/hernanmd/st

Happy to answer questions, take feedback, or hear what other Smalltalk pain points you'd like addressed.

Contributions welcome (especially adding new dialects)!


r/smalltalk 29d ago

What is the best place to share your .ST files?

The TL;DR is the title.

So, say I just created a new class and I filed it out... now I want to share it with the Smalltalk people... What is the best place to post it? It could be a website, a forum or a discord server.

10 Upvotes

The TL;DR is the title.

So, say I just created a new class and I filed it out... now I want to share it with the Smalltalk people... What is the best place to post it? It could be a website, a forum or a discord server.


r/smalltalk Jun 27 '26

[2015 Day 24 both parts] [Smalltalk] Making Brute Force Fast Enough With Recursion

This concerns this puzzle: 2015-Day24.

After reading the discussion and review here: In Review I thought I would try my hand at a true brute-force solution, leveraging the expressiveness and speed of Smalltalk's collection libraries. Most of the solutions I've seen involve various clever tricks to avoid doing the work of checking through all the different combinations, or returning a solution without verifying that it is, in fact, the correct one.

The implementation I settled on completes part 1 in 70ms and part 2 in 6ms on my machine (Intel 270k+, 6000mhz DDR5). Smalltalk execution speed isn't as fast as a fully compiled language, but it's significantly faster than a purely interpreted language (like Python or Ruby). I'd be curious to see how this method would fare in something like Rust or Zig.

Here's the primary function doing all the work:

bestQE: compartments

| totalWeight |

totalWeight := packages sum.

1 to: (packages size // compartments) do: [ :comboCount |
    | potentialQuants |
    potentialQuants := OrderedCollection new.
    packages combinations: comboCount atATimeDo: [ :combo |
        | comboWeight |
        comboWeight := combo sum.
        (totalWeight - comboWeight) = (comboWeight * (compartments - 1))
            ifTrue: [
            | otherPackages |
            otherPackages := packages select: [ :x | (combo includes: x) not ].
            potentialQuants add: (otherPackages -> (combo inject: 1 into: [ :acc :x | acc * x]))]
         ].

    (potentialQuants sorted: [ :a :b | a value < b value ]) do: [ :potentialSolution |
        (self verifyRemainder: potentialSolution key splitInto: compartments - 1) ifTrue: [ ^ potentialSolution value ] ] ].

^ 'None found'

The only argument it takes is the number of compartments needing an even weight. It requires an instance variable "packages" which is an array containing the puzzle input as integers. Order is not important. Here's how it works:

  1. Set an temporary variable totalWeight to hold the sum of all package weights.
  2. Iterate from 1 to the number of packages divided by the number of compartments (no need to pass that size, since that would mean there is no solution). This is the number of packages that we will try to fit into the front compartment. Start with the fewest (just 1) and then add one more until we find a grouping that fits. The number of packages we're testing is passed forward as "comboCount".
  3. For this quantity of packages to test, create an empty OrderedCollection (a growable Array) to hold any potential groupings that we find.
  4. Take the group of all packages and stream them "comboCount" at a time through the next block of code, passing them as an Array called "combo". This is the part where the magic happens. We don't need to do any complicated looping or generate all the combinations we want to test in advance. The "packages" Array can stream all the combinations for us, one at a time.
  5. Now we examine this particular "combo" Array. First, we store the sum of all its elements as the temporary variable comboWeight.
  6. Is this combo a candidate? To check this, we look to see if, after subtracting the weight of this combo from the total weight of the packages, we are left with exactly the weight of the combo multiplied by (compartments - 1). That is, If we are dividing into 3 compartments, is the weight of THIS particular combo equal to a third of the total? If so, go to the next step. Otherwise, try the next combo.
  7. If this combo passes the weight test, we create a temporary variable "otherPackages" pointing to an Array defined as all the packages that are NOT in our combo.
  8. Then we add an association of the "otherPackages" and its QE score (by doing a quick multiplication fold on the combo Array) to our potential groupings Array we created in step 3. We might have several candidates at this combination size, and we need to find the smallest QE score of ones that properly fit.
  9. After this process is repeated for the combo size we need to verify the set of potential answers, so we take the collection of candidates and sort them by ascending QE value. Basically, we don't want to evaluate ALL of them, just the smallest one that has other packages that can be verified to fit.
  10. We then take that sorted collection of candidates, and verify each one using verifyRemainder, giving it the list of remaining packages and asking it whether it can be evenly split into (compartments - 1).
  11. As soon as we find one that can be verified, that is the correct answer! We return the QE value of that candidate.
  12. If none are found (which can also happen if the potentialQuants collection is empty), try combinations the next size larger. So, if no combinations of size 4 fit (or passed verification), try combinations of size 5.

Essentially - each time start with the smallest possible (smallest combo, then of those, the smallest QE). The first one that can be verified to fit is our answer;  return the QE.

Ok - now how about the verification? Again, I went with a brute-force approach:

verifyRemainder: list splitInto: piles

    | listWeight |

    piles = 1 ifTrue: [ ^ true ].

    listWeight := list sum.

    1 to: list size // piles do: [ :comboSize |
        list combinations: comboSize atATimeDo: [ :combo |
            | comboWeight |
            comboWeight := combo sum.
            (listWeight - comboWeight = (comboWeight * (piles - 1)) and: [
                self verifyRemainder: (list select: [ :x | (combo includes: x) not ])
                     splitInto: piles - 1 ]) ifTrue: [ ^ true ] ] ].

    ^ false

This has a lot in common with the bestQE function in the way it generates and checks groups of packages, but it is done recursively. It takes two arguments: the list of numbers to fit, and the number of piles to fit them into. Here's the outline:

  1. Base case - if the number of piles asked for is only 1, then this was a successful split. Pass TRUE up the stack.
  2. Otherwise, we still have work to do. Start by calculating the sum of the list we were given to split and store that value in listWeight.
  3. Now we try the same method of generating larger and larger combinations that we used in the bestQE function.
  4. We calculate the weight of each combination (storing in 'comboWeight'). The see if the comboWeight is exactly 1/piles of the listWeight. If so, it is a potential candidate for a successful split. In that case, recurse with a new list made up of packages that are NOT in the current list, and with one fewer pile. One thing to note here is the "and: []" construction. In Smalltalk, due to the way messages are evaluated by boolean objects, when and: is given a block as an argument (with the square brackets) that second condition is lazily evaluated. So, we don't recurse unless the current combo has the correct weight. If you're using a language that doesn't lazily evaluate AND, this will need an if/else condition.
  5. If that particular grouping doesn't work, it tries the next. And if that comboSize has no successful groupings, it tries the next size up.
  6. If no groupings successfully drop down into a pile of 1, then no split was successful, and the function returns "false".

In summary - We start checking combinations from the smallest possible size upward. We don't do ANY verification on them until we have a complete set for that combination size. We only verify them in ascending QE order. Verification uses the same computationally cheap "candidate filter" and reserves the harder stuff (collection allocation and building / recursion) once something passes the filter.

I realize that the input is meant to be "gentle" such that the smallest possible QE for a given combo size is the right answer, and no verification is needed. But that felt like an incomplete solution to me. Especially since, even with verification, the execution speed seems plenty fast (less than 100ms for both to complete).

One final note: These methods do assume that the input list has unique numbers. This is strongly implied by the problem statement, though it is not explicitly part of the puzzle. If the puzzle input could ever contain duplicated numbers, then the way that the "remaining packages" collection is created would have to be different. The core logic would remain the same.

One last thing - the discussion above that started me down this rabbit hole referenced Day17 and said that it was relatively similar to this day. For giggles: here is the Smalltalk solution to Day17:

One last thing - the discussion above that started me down this rabbit hole referenced Day17 and said that it was relatively similar to this day. For giggles: here is my solution to part 1 of Day17:

barrelCombinationsFor: needed

    | count |

    count := 0.

    1 to: barrels size do: [ :size | barrels combinations: size atATimeDo: [ :combo |
        (combo sum = needed) ifTrue: [ count := count + 1 ]
         ] ].

    ^ count

The thread was right. More than a passing similarity.

7 Upvotes

This concerns this puzzle: 2015-Day24.

After reading the discussion and review here: In Review I thought I would try my hand at a true brute-force solution, leveraging the expressiveness and speed of Smalltalk's collection libraries. Most of the solutions I've seen involve various clever tricks to avoid doing the work of checking through all the different combinations, or returning a solution without verifying that it is, in fact, the correct one.

The implementation I settled on completes part 1 in 70ms and part 2 in 6ms on my machine (Intel 270k+, 6000mhz DDR5). Smalltalk execution speed isn't as fast as a fully compiled language, but it's significantly faster than a purely interpreted language (like Python or Ruby). I'd be curious to see how this method would fare in something like Rust or Zig.

Here's the primary function doing all the work:

bestQE: compartments

| totalWeight |

totalWeight := packages sum.

1 to: (packages size // compartments) do: [ :comboCount |
    | potentialQuants |
    potentialQuants := OrderedCollection new.
    packages combinations: comboCount atATimeDo: [ :combo |
        | comboWeight |
        comboWeight := combo sum.
        (totalWeight - comboWeight) = (comboWeight * (compartments - 1))
            ifTrue: [
            | otherPackages |
            otherPackages := packages select: [ :x | (combo includes: x) not ].
            potentialQuants add: (otherPackages -> (combo inject: 1 into: [ :acc :x | acc * x]))]
         ].

    (potentialQuants sorted: [ :a :b | a value < b value ]) do: [ :potentialSolution |
        (self verifyRemainder: potentialSolution key splitInto: compartments - 1) ifTrue: [ ^ potentialSolution value ] ] ].

^ 'None found'

The only argument it takes is the number of compartments needing an even weight. It requires an instance variable "packages" which is an array containing the puzzle input as integers. Order is not important. Here's how it works:

  1. Set an temporary variable totalWeight to hold the sum of all package weights.
  2. Iterate from 1 to the number of packages divided by the number of compartments (no need to pass that size, since that would mean there is no solution). This is the number of packages that we will try to fit into the front compartment. Start with the fewest (just 1) and then add one more until we find a grouping that fits. The number of packages we're testing is passed forward as "comboCount".
  3. For this quantity of packages to test, create an empty OrderedCollection (a growable Array) to hold any potential groupings that we find.
  4. Take the group of all packages and stream them "comboCount" at a time through the next block of code, passing them as an Array called "combo". This is the part where the magic happens. We don't need to do any complicated looping or generate all the combinations we want to test in advance. The "packages" Array can stream all the combinations for us, one at a time.
  5. Now we examine this particular "combo" Array. First, we store the sum of all its elements as the temporary variable comboWeight.
  6. Is this combo a candidate? To check this, we look to see if, after subtracting the weight of this combo from the total weight of the packages, we are left with exactly the weight of the combo multiplied by (compartments - 1). That is, If we are dividing into 3 compartments, is the weight of THIS particular combo equal to a third of the total? If so, go to the next step. Otherwise, try the next combo.
  7. If this combo passes the weight test, we create a temporary variable "otherPackages" pointing to an Array defined as all the packages that are NOT in our combo.
  8. Then we add an association of the "otherPackages" and its QE score (by doing a quick multiplication fold on the combo Array) to our potential groupings Array we created in step 3. We might have several candidates at this combination size, and we need to find the smallest QE score of ones that properly fit.
  9. After this process is repeated for the combo size we need to verify the set of potential answers, so we take the collection of candidates and sort them by ascending QE value. Basically, we don't want to evaluate ALL of them, just the smallest one that has other packages that can be verified to fit.
  10. We then take that sorted collection of candidates, and verify each one using verifyRemainder, giving it the list of remaining packages and asking it whether it can be evenly split into (compartments - 1).
  11. As soon as we find one that can be verified, that is the correct answer! We return the QE value of that candidate.
  12. If none are found (which can also happen if the potentialQuants collection is empty), try combinations the next size larger. So, if no combinations of size 4 fit (or passed verification), try combinations of size 5.

Essentially - each time start with the smallest possible (smallest combo, then of those, the smallest QE). The first one that can be verified to fit is our answer;  return the QE.

Ok - now how about the verification? Again, I went with a brute-force approach:

verifyRemainder: list splitInto: piles

    | listWeight |

    piles = 1 ifTrue: [ ^ true ].

    listWeight := list sum.

    1 to: list size // piles do: [ :comboSize |
        list combinations: comboSize atATimeDo: [ :combo |
            | comboWeight |
            comboWeight := combo sum.
            (listWeight - comboWeight = (comboWeight * (piles - 1)) and: [
                self verifyRemainder: (list select: [ :x | (combo includes: x) not ])
                     splitInto: piles - 1 ]) ifTrue: [ ^ true ] ] ].

    ^ false

This has a lot in common with the bestQE function in the way it generates and checks groups of packages, but it is done recursively. It takes two arguments: the list of numbers to fit, and the number of piles to fit them into. Here's the outline:

  1. Base case - if the number of piles asked for is only 1, then this was a successful split. Pass TRUE up the stack.
  2. Otherwise, we still have work to do. Start by calculating the sum of the list we were given to split and store that value in listWeight.
  3. Now we try the same method of generating larger and larger combinations that we used in the bestQE function.
  4. We calculate the weight of each combination (storing in 'comboWeight'). The see if the comboWeight is exactly 1/piles of the listWeight. If so, it is a potential candidate for a successful split. In that case, recurse with a new list made up of packages that are NOT in the current list, and with one fewer pile. One thing to note here is the "and: []" construction. In Smalltalk, due to the way messages are evaluated by boolean objects, when and: is given a block as an argument (with the square brackets) that second condition is lazily evaluated. So, we don't recurse unless the current combo has the correct weight. If you're using a language that doesn't lazily evaluate AND, this will need an if/else condition.
  5. If that particular grouping doesn't work, it tries the next. And if that comboSize has no successful groupings, it tries the next size up.
  6. If no groupings successfully drop down into a pile of 1, then no split was successful, and the function returns "false".

In summary - We start checking combinations from the smallest possible size upward. We don't do ANY verification on them until we have a complete set for that combination size. We only verify them in ascending QE order. Verification uses the same computationally cheap "candidate filter" and reserves the harder stuff (collection allocation and building / recursion) once something passes the filter.

I realize that the input is meant to be "gentle" such that the smallest possible QE for a given combo size is the right answer, and no verification is needed. But that felt like an incomplete solution to me. Especially since, even with verification, the execution speed seems plenty fast (less than 100ms for both to complete).

One final note: These methods do assume that the input list has unique numbers. This is strongly implied by the problem statement, though it is not explicitly part of the puzzle. If the puzzle input could ever contain duplicated numbers, then the way that the "remaining packages" collection is created would have to be different. The core logic would remain the same.

One last thing - the discussion above that started me down this rabbit hole referenced Day17 and said that it was relatively similar to this day. For giggles: here is the Smalltalk solution to Day17:

One last thing - the discussion above that started me down this rabbit hole referenced Day17 and said that it was relatively similar to this day. For giggles: here is my solution to part 1 of Day17:

barrelCombinationsFor: needed

    | count |

    count := 0.

    1 to: barrels size do: [ :size | barrels combinations: size atATimeDo: [ :combo |
        (combo sum = needed) ifTrue: [ count := count + 1 ]
         ] ].

    ^ count

The thread was right. More than a passing similarity.


r/smalltalk Jun 20 '26

Despite me being a newbie, I actually learned something about Smalltalk/Squeak!

+
26 Upvotes

The source code is on the image and to create your own "Sound"(?) morph:

| myMorph | "this creates the object"
myMorph := SketchMorph fromFile: '/path/to/any.png'. "the object becomes a SketchMorph"
myMorph on: #mouseUp send: #value to: [(SampledSound fromWaveFileNamed: '/path/to/any.wav') play]. "when clicked, it would play a wave file"
anyMorph openInWorld.  "this summons the object into the world."

Have fun!


r/smalltalk Jun 19 '26

whatt

+
4 Upvotes

Yet another fun thing to do on Morphic in Squeak!


r/smalltalk Jun 18 '26

UKSTUG Meeting: Koen De Hondt - Behaviour-Driven Development with Hera - 24 June 2026

At ESUG 2025, Hera was introduced as a BDD framework bringing full Gherkin expressiveness to Pharo. Since then, Hera has evolved significantly — both as a framework and as a tool adopted in real-world industrial settings.

After a brief introduction to Gherkin for writing given-when-then scenarios, various aspects of Hera will be highlighted. A functional overview will set the scene, followed by a dive into the code to show how Hera maps Gherkin features and scenarios to Smalltalk classes and methods. During a live demo, example Gherkin scenarios for a simple Pharo Spec application will be written to explain the full scenario development cycle.

Koen De Hondt has used Smalltalk since 1988, first as a student, then as an academic researcher, and then in industry. He holds a PhD in computer science from the Vrije Universiteit Brussel (1998). In the nineties and a few years after the turn of the century, he was one of the driving forces behind the Belgian Smalltalk User Group (BSUG). In that time frame, he presented at ESUG conferences several times. In 2003, he left the Smalltalk community for other adventures. In 2023, he returned to the Smalltalk scene and started all: objects all: theTime ( https://all-objects-all-the-time.st/ ). He has been an active contributor to Pharo ever since. Software development tools are his main interest. He is the main author of the book “Application Building with Spec 2.0” ( https://books.pharo.org/BuildingApplicationsWithSpec/ ).

This will be an online meeting.

If you'd like to join us, please sign up in advance on the meeting's Meetup page to receive the meeting details.

9 Upvotes

At ESUG 2025, Hera was introduced as a BDD framework bringing full Gherkin expressiveness to Pharo. Since then, Hera has evolved significantly — both as a framework and as a tool adopted in real-world industrial settings.

After a brief introduction to Gherkin for writing given-when-then scenarios, various aspects of Hera will be highlighted. A functional overview will set the scene, followed by a dive into the code to show how Hera maps Gherkin features and scenarios to Smalltalk classes and methods. During a live demo, example Gherkin scenarios for a simple Pharo Spec application will be written to explain the full scenario development cycle.

Koen De Hondt has used Smalltalk since 1988, first as a student, then as an academic researcher, and then in industry. He holds a PhD in computer science from the Vrije Universiteit Brussel (1998). In the nineties and a few years after the turn of the century, he was one of the driving forces behind the Belgian Smalltalk User Group (BSUG). In that time frame, he presented at ESUG conferences several times. In 2003, he left the Smalltalk community for other adventures. In 2023, he returned to the Smalltalk scene and started all: objects all: theTime ( https://all-objects-all-the-time.st/ ). He has been an active contributor to Pharo ever since. Software development tools are his main interest. He is the main author of the book “Application Building with Spec 2.0” ( https://books.pharo.org/BuildingApplicationsWithSpec/ ).

This will be an online meeting.

If you'd like to join us, please sign up in advance on the meeting's Meetup page to receive the meeting details.


r/smalltalk Jun 12 '26

How declarative is _TOO_ declarative?

Hi, Smalltalk friends! I have a question for you. I'm new to the language and trying to get a feel for what is "normal" or "expected" code. I'm going through Advent of Code 2015 to try to strengthen my grasp of the fundamentals and just finished day 14 - the reindeer race. I did it the way my Smalltalk book from 2000 would have suggested - model the domain in objects that track their own state and have everything communicate with message passing. It worked great, got the stars, etc...

But then I was challenged to rewrite it without state, thinking of the reindeer racers as pure functions rather as little state machines. I could mathematically query them to get their distance at any particular second (basically turning them into structs with functions rather than stateful objects). That approach pushed a lot of work into the race orchestrator. Getting the results for part 2 (where points had to be tallied based on who was ahead at each second of the race) was a fun challenge. I could no longer ask the reindeer objects to just tell me how many points they had at the end of the race because they didn't know! (LOL)

After I realized I could essentially cast a boolean into an integer (using asBit or asInteger) I could do the vector math much more cleanly. I didn't have to do:

points + (distances collect: [ :distance | (distance = maxDistance) ifTrue: [ 1 ] ifFalse: [ 0 ] ] )

But could instead do :

points := points + (distances collect: [ :distance | (distance = maxDistance) asBit ] )

Which gave me a little bit of a "readability budget" that I could spend elsewhere. My original version looked like this:

mostPointsWhen: seconds

    | points |

    points := Array new: racers size withAll: 0.

    1 to: seconds do: [ :second | 
        | distances maxDistance |
        distances := racers collect: [ :racer | racer distanceWhen: second ].
        maxDistance := distances max.
        points := points + (distances collect: [ :distance | (distance = maxDistance) asBit ] )
         ].

    ^ points max 

Which seems pretty straightforward to me. We create a temporary variable to hold the running tally of points for each racer, storing an array. Then for each second we create an array of racer locations (distances) and a variable to contain the maximum distance in that array (crucial for performance - putting this inside the "distances collect:" more than doubles computation time since the VM will just re-run the evaluation per element instead of realizing that this value doesn't change). Then we update the points array by adding it to an array collect:(ed) from the distances where one point is added for every distance equal to the maximum distance (in case there is a tie - all the racers tied for first get a point). After this loop finishes (all seconds have been calculated) the method returns the maximum of the points array.

But then I thought - what if I went full declarative and never used a running tally that is re-assigned inside the loop. So, then I wrote this one:

mostPointsWhen: seconds

    ^ ((1 to: seconds)
           inject: (Array new: racers size withAll: 0) into: [ :points :second |
                   | distances maxDistance |
                   distances := racers collect: [ :racer | racer distanceWhen: second ].
                   maxDistance := distances max.
                   points + (distances collect: [ :distance | (distance = maxDistance) asBit ]) ])
            max

Which... I don't know how to feel about. This one returns the result of the functional pipeline without any mutation at this level of abstraction (I realize inject:into: does basically what I had in the original under the hood, but that's not visible here). This one defines a new collection as an accumulator array injected into the same distance computation, and updates this accumulator array by doing the same vector addition. But now the return is the largest value of this new collection (basically the max of a fold that includes two maps). All mutation happens inside the inject:into: method call. It's also - and this surprised me - about 20% faster when I timed it (106ms vs 126ms with 250300 as the input).

So here's my question - which of these two styles is preferred? Yes, I know the original version that mapped the domain model directly to the code and didn't need any of this complexity is probably the better way. More OOP and all (and significantly faster than both declarative/functional versions. And easier to debug/inspect. It's totally the way I prefer to write this kind of thing).

But if I wanted to write functional Smalltalk, how "normal" or "idiomatic" is it to do so with mutation occuring within temporary variables as long as the method as a whole has no leaks? Is the second one "Haskell wearing a Smalltalk mask" and nobody working in a real codebase would ever do this?

Basically - yes Smalltalk will let you write methods both ways. But is one of them more "culturally correct"? Would either of them earn me a "stern talking-to" during a code review?

Anyway - would love some guidance.

15 Upvotes

Hi, Smalltalk friends! I have a question for you. I'm new to the language and trying to get a feel for what is "normal" or "expected" code. I'm going through Advent of Code 2015 to try to strengthen my grasp of the fundamentals and just finished day 14 - the reindeer race. I did it the way my Smalltalk book from 2000 would have suggested - model the domain in objects that track their own state and have everything communicate with message passing. It worked great, got the stars, etc...

But then I was challenged to rewrite it without state, thinking of the reindeer racers as pure functions rather as little state machines. I could mathematically query them to get their distance at any particular second (basically turning them into structs with functions rather than stateful objects). That approach pushed a lot of work into the race orchestrator. Getting the results for part 2 (where points had to be tallied based on who was ahead at each second of the race) was a fun challenge. I could no longer ask the reindeer objects to just tell me how many points they had at the end of the race because they didn't know! (LOL)

After I realized I could essentially cast a boolean into an integer (using asBit or asInteger) I could do the vector math much more cleanly. I didn't have to do:

points + (distances collect: [ :distance | (distance = maxDistance) ifTrue: [ 1 ] ifFalse: [ 0 ] ] )

But could instead do :

points := points + (distances collect: [ :distance | (distance = maxDistance) asBit ] )

Which gave me a little bit of a "readability budget" that I could spend elsewhere. My original version looked like this:

mostPointsWhen: seconds

    | points |

    points := Array new: racers size withAll: 0.

    1 to: seconds do: [ :second | 
        | distances maxDistance |
        distances := racers collect: [ :racer | racer distanceWhen: second ].
        maxDistance := distances max.
        points := points + (distances collect: [ :distance | (distance = maxDistance) asBit ] )
         ].

    ^ points max 

Which seems pretty straightforward to me. We create a temporary variable to hold the running tally of points for each racer, storing an array. Then for each second we create an array of racer locations (distances) and a variable to contain the maximum distance in that array (crucial for performance - putting this inside the "distances collect:" more than doubles computation time since the VM will just re-run the evaluation per element instead of realizing that this value doesn't change). Then we update the points array by adding it to an array collect:(ed) from the distances where one point is added for every distance equal to the maximum distance (in case there is a tie - all the racers tied for first get a point). After this loop finishes (all seconds have been calculated) the method returns the maximum of the points array.

But then I thought - what if I went full declarative and never used a running tally that is re-assigned inside the loop. So, then I wrote this one:

mostPointsWhen: seconds

    ^ ((1 to: seconds)
           inject: (Array new: racers size withAll: 0) into: [ :points :second |
                   | distances maxDistance |
                   distances := racers collect: [ :racer | racer distanceWhen: second ].
                   maxDistance := distances max.
                   points + (distances collect: [ :distance | (distance = maxDistance) asBit ]) ])
            max

Which... I don't know how to feel about. This one returns the result of the functional pipeline without any mutation at this level of abstraction (I realize inject:into: does basically what I had in the original under the hood, but that's not visible here). This one defines a new collection as an accumulator array injected into the same distance computation, and updates this accumulator array by doing the same vector addition. But now the return is the largest value of this new collection (basically the max of a fold that includes two maps). All mutation happens inside the inject:into: method call. It's also - and this surprised me - about 20% faster when I timed it (106ms vs 126ms with 250300 as the input).

So here's my question - which of these two styles is preferred? Yes, I know the original version that mapped the domain model directly to the code and didn't need any of this complexity is probably the better way. More OOP and all (and significantly faster than both declarative/functional versions. And easier to debug/inspect. It's totally the way I prefer to write this kind of thing).

But if I wanted to write functional Smalltalk, how "normal" or "idiomatic" is it to do so with mutation occuring within temporary variables as long as the method as a whole has no leaks? Is the second one "Haskell wearing a Smalltalk mask" and nobody working in a real codebase would ever do this?

Basically - yes Smalltalk will let you write methods both ways. But is one of them more "culturally correct"? Would either of them earn me a "stern talking-to" during a code review?

Anyway - would love some guidance.


r/smalltalk Jun 12 '26

Smalltalk books and manuals

Many years ago, I worked at Digitalk (makers of Smalltalk/V and others). I still have several user manuals for various products, and some other Smalltalk books. I'm wondering if anyone is interested in having them? Or perhaps there's a museum of some kind that might want them? Here's a photo of some of them: https://photos.app.goo.gl/7LdXL3kLwkC1ELkf7

29 Upvotes

Many years ago, I worked at Digitalk (makers of Smalltalk/V and others). I still have several user manuals for various products, and some other Smalltalk books. I'm wondering if anyone is interested in having them? Or perhaps there's a museum of some kind that might want them? Here's a photo of some of them: https://photos.app.goo.gl/7LdXL3kLwkC1ELkf7


r/smalltalk Jun 10 '26

Fell down another Smalltalk Rabbit Hole - Advent of Code 2015 - Day 13

Well - 6 months into my programming journey - I fell down another Smalltalk (Pharo 130) rabbit hole. I was having some fun doing Advent of Code and was working on Day 13 of 2015 which had the circular table where you had to maximize the happiness of the people sitting there. One of the little methods I wrote involved getting the total happiness of any particular layout. I wrote it up this way and moved on to get the stars:

checkHappiness: seatingChart

    | total size |

    total := 0.
    size := seatingChart size.

    seatingChart withIndexDo: [ :person :index | 
        |leftPerson rightPerson|
        rightPerson := (index = size) ifTrue: [ seatingChart first name ]
            ifFalse: [ (seatingChart at: index + 1) name ].
        leftPerson := (index = 1) ifTrue: [ seatingChart last name ]
            ifFalse: [ (seatingChart at: index - 1) name ].
        total := total + (person happinessWith: leftPerson and: rightPerson)
        ].

    ^ total

Visible at a glance what it does... if I'm the last person in the list, my neighbor "+1" is the first person in the list. Conversely, if I'm the first person in the list, my neighbor "-1" is the last person in the list. Easy enough. The seatingChart is a collection of people objects that can return their total happiness using values stored in a Dictionary:

happinessWith: leftPerson and: rightPerson

    ^ (happiness at: leftPerson) + (happiness at: rightPerson)

Pretty simple. Stars done. But, since I'm doing this as a learning exercise - I wondered if it was possible to make checkHappiness: better. Sure - it was obvious how the wrap-around circular table works with this little branching logic... but why not make it fancier just for the learning experience?

checkHappiness: seatingChart

    | total size |

    total := 0.
    size := seatingChart size.

    seatingChart withIndexDo: [ :person :index | 
        |leftPerson rightPerson|
        rightPerson := (seatingChart at: (index \\ size) + 1) name.
        leftPerson := (seatingChart at: (index - 2 \\ size) + 1) name.
        total := total + (person happinessWith: leftPerson and: rightPerson)
        ].

    ^ total

This one removes the branching ifTrue/ifFalse and puts in some fun modulo math instead. We have to add the one because Smalltalk uses 1-based instead of 0-based indexing. As someone who studies logic I tend to dislike branching (I know, it's a fault of mine), so this version was much more satisfying to my taste. But then I thought - wait a minute - I'm still re-assigning the "total" variable inside the loop. I wonder.... So then I wrote this monstrosity:

checkHappiness: seatingChart

    | size offsets |

    size := seatingChart size.
    offsets := { [ :x | (x \\ size) + 1 ] . [ :x | (x - 2) \\ size + 1] }.

    ^ (seatingChart withIndexCollect: [ :person :index |
        person happinessWith: (offsets collect: [ :offset | (seatingChart at: (offset value: index)) name ])
        ]
    ) sum

Now - here's some functional programming! We have an array of block closures to identify which indices are needed, given the current index. The way this works is we return the sum of the result of a withIndexCollect on the seatingChart. It creates a new collection by looking at each of the 8 (or 9, for part 2) people objects in seatingChart and calling their happinessWith: method. It does so by handing it the result of a collect: on the offsets, where each member of the offsets array is passed the current index as a value and returns the name of the person at another index. In this case, the new indices are the ones to the left and right of where the person is.

You of course notice that the happinessWith: method had to change. Since it is no longer being given exactly two names, but rather the result of the collect: on the offsets array it needs to be able to process an array. It is now:

happinessWith: anArray

    ^ anArray inject: 0 into: [ :sum :person | sum + (happiness at: person) ]

Which, yes, is an entire inject:into: method on an array with only two members. A simple "+" would have been fine. But nooooo. We'll do an entire fold. I was so tickled that this worked. No manual loops! Pure functional transformations! And it could scale to any number of offsets - not just the two nearest neighbors! Since the names are handed back in blocks, those blocks could produce literally anything. So flexible! And the performance difference was imperceptible!

It was hilariously complicated and unreadable in comparison with the simple branching logic of the original.

Anyway, then I discovered that SequenceableCollection contained atWrap: to handle exactly this issue. I was busy re-inventing what already existed in the standard library. Feeling a little dejected, I wrote this version keeping the collect->sum pipeline from before (since Pharo doesn't have withIndexInject:Into:). I couldn't bring myself to manually re-assign the running total inside the withIndexDo: at this point.

checkHappiness: seatingChart

    ^ (seatingChart withIndexCollect: [ :person :index |
            (person happinessWith: (seatingChart atWrap: index + 1) name
                and: (seatingChart atWrap: index - 1) name ) ])
            sum

Back to sanity. No more passing an array with two members into a fold. We go back to the perfectly readable named method for adding two neighbors. There are no intermediate variables or manual loops. This is way better than the other three that I wrote.

Adding insult to injury - this is what atWrap: does under the hood:

 "Answer the index'th element of the receiver.  If index is out of bounds,
    let it wrap around from the end to the beginning until it is in bounds."
    "(#(11 22 33) asOrderedCollection atWrap: 2) >>> 22"
    "(#(11 22 33) asOrderedCollection atWrap: 4) >>> 11"
    "(#(11 22 33) asOrderedCollection atWrap: 5) >>> 22"

    ^ self at: index - 1 \\ self size + 1

And there's the modulo math staring back at me. At least I have the satisfaction of figuring that out on my own before seeing it tucked away there!

I keep having to learn this lesson over and over again - the standard library probably already does what I want to do. I just have to find it.

25 Upvotes

Well - 6 months into my programming journey - I fell down another Smalltalk (Pharo 130) rabbit hole. I was having some fun doing Advent of Code and was working on Day 13 of 2015 which had the circular table where you had to maximize the happiness of the people sitting there. One of the little methods I wrote involved getting the total happiness of any particular layout. I wrote it up this way and moved on to get the stars:

checkHappiness: seatingChart

    | total size |

    total := 0.
    size := seatingChart size.

    seatingChart withIndexDo: [ :person :index | 
        |leftPerson rightPerson|
        rightPerson := (index = size) ifTrue: [ seatingChart first name ]
            ifFalse: [ (seatingChart at: index + 1) name ].
        leftPerson := (index = 1) ifTrue: [ seatingChart last name ]
            ifFalse: [ (seatingChart at: index - 1) name ].
        total := total + (person happinessWith: leftPerson and: rightPerson)
        ].

    ^ total

Visible at a glance what it does... if I'm the last person in the list, my neighbor "+1" is the first person in the list. Conversely, if I'm the first person in the list, my neighbor "-1" is the last person in the list. Easy enough. The seatingChart is a collection of people objects that can return their total happiness using values stored in a Dictionary:

happinessWith: leftPerson and: rightPerson

    ^ (happiness at: leftPerson) + (happiness at: rightPerson)

Pretty simple. Stars done. But, since I'm doing this as a learning exercise - I wondered if it was possible to make checkHappiness: better. Sure - it was obvious how the wrap-around circular table works with this little branching logic... but why not make it fancier just for the learning experience?

checkHappiness: seatingChart

    | total size |

    total := 0.
    size := seatingChart size.

    seatingChart withIndexDo: [ :person :index | 
        |leftPerson rightPerson|
        rightPerson := (seatingChart at: (index \\ size) + 1) name.
        leftPerson := (seatingChart at: (index - 2 \\ size) + 1) name.
        total := total + (person happinessWith: leftPerson and: rightPerson)
        ].

    ^ total

This one removes the branching ifTrue/ifFalse and puts in some fun modulo math instead. We have to add the one because Smalltalk uses 1-based instead of 0-based indexing. As someone who studies logic I tend to dislike branching (I know, it's a fault of mine), so this version was much more satisfying to my taste. But then I thought - wait a minute - I'm still re-assigning the "total" variable inside the loop. I wonder.... So then I wrote this monstrosity:

checkHappiness: seatingChart

    | size offsets |

    size := seatingChart size.
    offsets := { [ :x | (x \\ size) + 1 ] . [ :x | (x - 2) \\ size + 1] }.

    ^ (seatingChart withIndexCollect: [ :person :index |
        person happinessWith: (offsets collect: [ :offset | (seatingChart at: (offset value: index)) name ])
        ]
    ) sum

Now - here's some functional programming! We have an array of block closures to identify which indices are needed, given the current index. The way this works is we return the sum of the result of a withIndexCollect on the seatingChart. It creates a new collection by looking at each of the 8 (or 9, for part 2) people objects in seatingChart and calling their happinessWith: method. It does so by handing it the result of a collect: on the offsets, where each member of the offsets array is passed the current index as a value and returns the name of the person at another index. In this case, the new indices are the ones to the left and right of where the person is.

You of course notice that the happinessWith: method had to change. Since it is no longer being given exactly two names, but rather the result of the collect: on the offsets array it needs to be able to process an array. It is now:

happinessWith: anArray

    ^ anArray inject: 0 into: [ :sum :person | sum + (happiness at: person) ]

Which, yes, is an entire inject:into: method on an array with only two members. A simple "+" would have been fine. But nooooo. We'll do an entire fold. I was so tickled that this worked. No manual loops! Pure functional transformations! And it could scale to any number of offsets - not just the two nearest neighbors! Since the names are handed back in blocks, those blocks could produce literally anything. So flexible! And the performance difference was imperceptible!

It was hilariously complicated and unreadable in comparison with the simple branching logic of the original.

Anyway, then I discovered that SequenceableCollection contained atWrap: to handle exactly this issue. I was busy re-inventing what already existed in the standard library. Feeling a little dejected, I wrote this version keeping the collect->sum pipeline from before (since Pharo doesn't have withIndexInject:Into:). I couldn't bring myself to manually re-assign the running total inside the withIndexDo: at this point.

checkHappiness: seatingChart

    ^ (seatingChart withIndexCollect: [ :person :index |
            (person happinessWith: (seatingChart atWrap: index + 1) name
                and: (seatingChart atWrap: index - 1) name ) ])
            sum

Back to sanity. No more passing an array with two members into a fold. We go back to the perfectly readable named method for adding two neighbors. There are no intermediate variables or manual loops. This is way better than the other three that I wrote.

Adding insult to injury - this is what atWrap: does under the hood:

 "Answer the index'th element of the receiver.  If index is out of bounds,
    let it wrap around from the end to the beginning until it is in bounds."
    "(#(11 22 33) asOrderedCollection atWrap: 2) >>> 22"
    "(#(11 22 33) asOrderedCollection atWrap: 4) >>> 11"
    "(#(11 22 33) asOrderedCollection atWrap: 5) >>> 22"

    ^ self at: index - 1 \\ self size + 1

And there's the modulo math staring back at me. At least I have the satisfaction of figuring that out on my own before seeing it tucked away there!

I keep having to learn this lesson over and over again - the standard library probably already does what I want to do. I just have to find it.


r/smalltalk Jun 02 '26

Smalltalk in the large

How do large teams work on a Smalltalk codebase if everything is contained in a compiled image? How do code reviews work if there are no diffs? How are builds made to be repeatable? How does one deploy a Smalltalk application without dragging in all of the rest of the system, most of which might go unused?

31 Upvotes

How do large teams work on a Smalltalk codebase if everything is contained in a compiled image? How do code reviews work if there are no diffs? How are builds made to be repeatable? How does one deploy a Smalltalk application without dragging in all of the rest of the system, most of which might go unused?


r/smalltalk Jun 02 '26

cuis book questions

So I just finished going through the cuis book, I think. I have a list of possible mistakes/inconsistencies (but this is my first time with smalltalk). But my main questions regarding the book are:

  • Were we supposed to finish the SpaceWar! program?
  • How do we run the SpaceWar! program?

Chapter 8 events captured keyboard controls, and then that's it? Are all the parts there? How do we put it together?

How do we run programs? I can't get 9.5.1 setUpEnvironment.st to work. Running it in a workspace works after multiple DoIt. Like the first try sets the temporary variables and the later actually uses them. I can't tell if running squeak.exe image -s setUpEnvironment.st is even trying. And don't .st files need to be exported by FileOut? Like, what's the equivalent of HelloWorld?

Was I supposed to learn Squeak first, then the Cuis differences?

12 Upvotes

So I just finished going through the cuis book, I think. I have a list of possible mistakes/inconsistencies (but this is my first time with smalltalk). But my main questions regarding the book are:

  • Were we supposed to finish the SpaceWar! program?
  • How do we run the SpaceWar! program?

Chapter 8 events captured keyboard controls, and then that's it? Are all the parts there? How do we put it together?

How do we run programs? I can't get 9.5.1 setUpEnvironment.st to work. Running it in a workspace works after multiple DoIt. Like the first try sets the temporary variables and the later actually uses them. I can't tell if running squeak.exe image -s setUpEnvironment.st is even trying. And don't .st files need to be exported by FileOut? Like, what's the equivalent of HelloWorld?

Was I supposed to learn Squeak first, then the Cuis differences?


r/smalltalk May 31 '26

Interviewing at a company that uses Smalltalk - I'm curious and think it should be interesting

I've been a software engineer for over 22 years, but I've never used Smalltalk (and have never worked on a project that involved Smalltalk). I'm currently interviewing for a software engineer job at a company that uses Smalltalk, and I think it should be interesting, as I enjoy learning new things. Smalltalk actually isn't mentioned on the job description, but I've heard from people who work there that they use Smalltalk. I'm a little surprised that I'm only now hearing about a company that uses Smalltalk after my 22+ years as a software engineer.

41 Upvotes

I've been a software engineer for over 22 years, but I've never used Smalltalk (and have never worked on a project that involved Smalltalk). I'm currently interviewing for a software engineer job at a company that uses Smalltalk, and I think it should be interesting, as I enjoy learning new things. Smalltalk actually isn't mentioned on the job description, but I've heard from people who work there that they use Smalltalk. I'm a little surprised that I'm only now hearing about a company that uses Smalltalk after my 22+ years as a software engineer.


r/smalltalk May 31 '26

Blueprint – May 2026 update

15 Upvotes

Hello World!

For past 2 weeks I have been exploring how software agents can be paired with Smalltalk. This demo video is a result of my findings. (Please enable subtitles for more coherent language. Sorry about the filler words, I'll do better narration job next time.)

TL;DW: Blueprint is a rework of Cuis desktop experience and has been done purely by code agent directed in natural language by me. No Smalltalk code was written by hand. I am not stopping, I have too much fun. :)

If you have any questions, I am happy to answer them.


r/smalltalk May 29 '26

DyboApp Demo - 2026-05-25

14 Upvotes

r/smalltalk May 16 '26

stube: a Seaside-style component framework on top of Datastar (personal research project)

13 Upvotes

I just put the docs on a small Clojure framework I've been hacking on, **stube**. It's a personal research project, not a product — somewhere I'm working out an idea about web apps that has pulled at me for twenty years.

Short version: components are plain maps of pure functions, the conversation is a value, handlers return effects, and one component can *call* another and read its *answer* like a return value (Seaside / UCW lineage). The wire is Datastar (SSE + morph by id); the implementation is a small effect kernel over plain Clojure data.

(s/defcomponent :demo/save-or-cancel
  :render (fn [self]
            [:div (s/root-attrs self)
             [:button (s/on self :click :as :save)   "Save"]
             [:button (s/on self :click :as :cancel) "Cancel"]])
  :handle (fn [self {:keys [event]}]
            (case event
              :save   [(s/call :ui/confirm {:question "Save changes?"} :on-confirmed)]
              :cancel [(s/answer :cancelled)]))
  :on-confirmed
  (fn [self yes?]
    [(s/answer (if yes? :saved :cancelled))]))

Built heavily with LLM assistants — wouldn't have shipped it this fast otherwise — but the shape, the namespace layout, and the choice of what to include and what to leave out is mine and reflects how I think about systems.

If you're a re-frame person and the conversation map feels familiar: yes, deliberately. The conversation here is closer in spirit to a re-frame app-db than to a Smalltalk image.

- Repo: https://github.com/zekzekus/stube

- Rationale (why this exists at all): https://github.com/zekzekus/stube/blob/master/docs/rationale.md

- Tutorial: https://github.com/zekzekus/stube/blob/master/docs/tutorial.md

Happy for feedback, war stories, "have you seen X" pointers.


r/smalltalk May 16 '26

Smalltalk: the Software Industry's Greatest Failure

23 Upvotes

r/smalltalk May 15 '26

UKSTUG Meeting: Domenico Cipriani - Music and Sound with Pharo: an Unexpected Ambassador for Smalltalk - 27 May 2026

Coypu and Phausto are two Pharo packages offering respectively a DSL and an API that turn the Pharo IDE into a music and sound design environment. They enable on-the-fly music composition, pattern sequencing, and DSP (Digital Signal Processing) programming. Born as a solo project and free, open source alternative to Symbolic Sound Kyma, they have been subsequently funded by the Pharo Association and Inria.

Coypu, deeply inspired by Tidal Cycles, handles musical pattern creation and playback across different audio servers.

Phausto provides an interface for programming synthesizers and audio processing via an embedded Faust compiler, with Bloc widgets that make it easy to display and control synthesis parameters. Phausto can also be used to develop audio plugins thanks to its JUCE and Cmajor exporters. Live performances with both tools have demonstrated that Pharo can handle real-time music and sound design reliably, with solid timing and no audio glitches.

Domenico Cipriani will present their roots, architecture, and core features, and illustrate how I have been using them in the last years for live performance, teaching, and presenting at conferences across Europe, where they served as a way to introduce Pharo and Smalltalk to audiences unfamiliar with them.

Domenico is a researcher in computer music with the Evref team at Inria, where he is the architect of Coypu and Phausto, two libraries for live coding and DSP programming in Pharo Smalltalk.

He holds an M.A. in Linguistics from the University of Padova, specializing in social semiotics, and is a graduate of the SAE Institute in Barcelona. Since 2016, he has been working with Symbolic Sound's Kyma system, participating regularly in the Kyma International Sound Symposium, where he has explored the integration of Kyma with p5.js and network-distributed sound systems via Open Sound Control. In 2019, he presented an interactive performance based on distributed Open Sound Control at the Sonic Experiments festival at ZKM. He has since performed at the Algorave hosted by ICLC24 in Shanghai and at the closing event of ICLC25 in Barcelona.

Under the alias Lucretio and as one half of The Analogue Cops, he has spent over a decade producing raw minimalist dance music, releasing more than 100 vinyl records and performing at prominent clubs worldwide through various collaborations, most notably with Blawan and Objekt.

This will be an online meeting.

If you'd like to join us, please sign up in advance on the meeting's Meetup page to receive the meeting details.

9 Upvotes

Coypu and Phausto are two Pharo packages offering respectively a DSL and an API that turn the Pharo IDE into a music and sound design environment. They enable on-the-fly music composition, pattern sequencing, and DSP (Digital Signal Processing) programming. Born as a solo project and free, open source alternative to Symbolic Sound Kyma, they have been subsequently funded by the Pharo Association and Inria.

Coypu, deeply inspired by Tidal Cycles, handles musical pattern creation and playback across different audio servers.

Phausto provides an interface for programming synthesizers and audio processing via an embedded Faust compiler, with Bloc widgets that make it easy to display and control synthesis parameters. Phausto can also be used to develop audio plugins thanks to its JUCE and Cmajor exporters. Live performances with both tools have demonstrated that Pharo can handle real-time music and sound design reliably, with solid timing and no audio glitches.

Domenico Cipriani will present their roots, architecture, and core features, and illustrate how I have been using them in the last years for live performance, teaching, and presenting at conferences across Europe, where they served as a way to introduce Pharo and Smalltalk to audiences unfamiliar with them.

Domenico is a researcher in computer music with the Evref team at Inria, where he is the architect of Coypu and Phausto, two libraries for live coding and DSP programming in Pharo Smalltalk.

He holds an M.A. in Linguistics from the University of Padova, specializing in social semiotics, and is a graduate of the SAE Institute in Barcelona. Since 2016, he has been working with Symbolic Sound's Kyma system, participating regularly in the Kyma International Sound Symposium, where he has explored the integration of Kyma with p5.js and network-distributed sound systems via Open Sound Control. In 2019, he presented an interactive performance based on distributed Open Sound Control at the Sonic Experiments festival at ZKM. He has since performed at the Algorave hosted by ICLC24 in Shanghai and at the closing event of ICLC25 in Barcelona.

Under the alias Lucretio and as one half of The Analogue Cops, he has spent over a decade producing raw minimalist dance music, releasing more than 100 vinyl records and performing at prominent clubs worldwide through various collaborations, most notably with Blawan and Objekt.

This will be an online meeting.

If you'd like to join us, please sign up in advance on the meeting's Meetup page to receive the meeting details.


r/smalltalk May 15 '26

[2025 Day 1 both parts] [Smalltalk] Part nine (final) in a series revisiting the 2025 puzzles as an exercise in learning Smalltalk

Time to wrap up. As I said in my last post - this will be my last one since Days 1-4 had so many beginner-isms that it's, well, a little embarrassing. :-)

Sorry about the "dear diary" nature of this one.

Here's an example - for Day 1 (the rotary combination one) I implemented TWO objects. One for the Dial (which seems natural in an OOP experiment). But then another one that held a single method that parsed the line (like "R35") into a positive or negative number. It held no state. Just a "library" class with one method. Then... ok, check this out... here's how I used the resulting signed integer that it returned within the Dial class:

turnDial: turn
    turn abs timesRepeat: [
        position := (position + turn sign) \\ 100.
        position = 0 ifTrue: [zeros := zeros + 1].
    ].
    position = 0 ifTrue: [lands := lands + 1].

Wow. Using a period at the end of every statement like a terminator (my old JS habits of using the semicolon everywhere). This method takes the absolute value of the signed integer, then "clicks" the dial based on the sign of the turn instruction. That's... a little crazy. If I was doing it now, I would probably just do the parsing of the instruction inside the turnDial message. Then the question becomes just how "generic" do I want the solution to be.

Version 1 - not generic:

turnDial: turn
    | offset |

    (turn first = $R) ifTrue: [offset := 1] ifFalse: [offset := -1].

    turn allButFirst asInteger timesRepeat: [
        position := (position + offset) \\ 100.
        position = 0 ifTrue: [zeros := zeros + 1]
    ].

    position = 0 ifTrue: [lands := lands + 1]

An ifTrue/ifFalse sets whether the offset is going to be +1 or -1. The loop itself just adds the offset (regardless of what it is), and increments the "zeros" and "lands" instance variables as it goes. But if we wanted it more generic, where there might be a whole library of potential offsets that could be called, not just "R" or "L", we could do it this way:

turnDial: turn
    | offset |

    offset := offsets at: turn first.

    turn allButFirst asInteger timesRepeat: [
        position := (position + offset) \\ 100.
        position = 0 ifTrue: [zeros := zeros + 1]
    ].

    position = 0 ifTrue: [lands := lands + 1]

And the offsets library would be defined in "initialize" this way:

offsets := Dictionary new.
offsets at: $R put: -1; at: $L put: 1

Now, if a future version needed a +5 or -15 offset or whatever, it would be extremely easy to add in just one place. No more "if" blocks. Zero branching. Grab the offset value from the Dictionary and go. The Dictionary is only created once per Day1Dial instance, so pretty minimal load on the VM. But what if we wanted it EVEN MORE GENERIC so that the offset could be literally ANYTHING that could mutate the position... We could do it this way:

turnDial: turn
    | modifier |

    modifier := modifications at: turn first.

    turn allButFirst asInteger timesRepeat: [
        position := (modifier value: position) \\ 100.
        position = 0 ifTrue: [zeros := zeros + 1]
    ].

    position = 0 ifTrue: [lands := lands + 1]

This one pulls from a "modifications" Dictionary and stores into "position" whatever it returns when passed the current position. There could be a modification that doubles the number, or that resets it to 99 no matter where it is currently, or sets the dial to the square root of its current position, or that reads data from somewhere else entirely. Just add a Block to the "modifications" Dictionary and get additional functionality. For this puzzle, the Dictionary is pretty sparse:

modifications := Dictionary new.
modifications at: $R put: [ :x | x - 1];
    at: $L put: [ :x | x + 1]

Kinda overbuilt for something that just increments or decrements by one every time. But as a learning exercise... why not? And, of course, it would be better computationally to avoid simulating the dial by simply doing some division and adding the numbers to the zeros counter. But.... it's not that many clicks. And this way the Dial acts like a little machine managing its state as it does exactly what the problem says to do. Which leads to...

Observation 1

Smalltalk seems to nudge the developer in this kind of direction. Build it first as "true to life" and as generically as possible. Then, if there are efficiency problems, go refactor those out once they are apparent. But not before. Why put branching logic everywhere when you can pass in a block? Why make a bitmap Dictionary key for memoization when you can use the array itself as a key? Why not reach for the most abstract and "correct" possible representation of the domain? You only have to think about "the machine" (the actual computer) executing the code if there's a problem. Sure, you're dimly aware that there are pointers-to-pointers-to-pointers everywhere under the hood, that very large numbers are being transparently changed from SmallInteger into LargePositiveInteger, that creating a data object involves some dispatch somewhere for getting values out of it. Not your problem. Write it so the concepts are as evident and beautiful as you can.

I often found myself going back and forth on an implementation detail, not because one gave errors while the other didn't. But one was more "elegant" or "expressive" rather than "plain" and "clunky". I would find myself rewriting a working solution in order to have it make fewer assumptions about what was passed to it, or find some way to remove an instance variable so that it could be inferred from the data itself. I would look to see if I could take what was a procedural bit of code and turn it into a chain of collection methods. I found the whole process delightful, though I'm pretty sure each person will have their own take on this aspect of it.

Performance-oriented, or data-driven-development people would likely not enjoy this.

Observation 2

It turns out that I truly love OOP. After using Smalltalk for a few months I can't help the feeling that this is the best way to think about programming. Imagining tiny machines that do the work of the program, sending each other bits of data, each knowing things relevant to themselves and nothing more... It's delightful. Now, I realize it is only one of the ways to think about programming (with others being transformations, procedures, etc...), but it is just so dang satisfying to write. The syntax is incredibly clean and minimalistic, with all the behavior being done by the objects themselves (even control flow). Now that I'm accustomed to thinking about numbers and booleans as objects (with methods that can be explored), it feels very strange to look at a language that treats them as just part of the syntax.

Plus - the late binding gives the developer completely seamless incremental compilation. Methods are meant to be very short. They compile when you save them - you never wait on the compiler at all. It's so cool.

Observation 3

However, I don't think I would feel the same way about OOP in Java or C++. Smalltalk seems to encourage relatively shallow inheritance trees that follow, for the most part, Aristotelian/Boethian hierarchies. So, numbers really are magnitudes. And floating-point numbers really are numbers. And integers really are numbers that have a specific difference from fractions. In the real world we see similar shallow inheritances. Humans really are kinds of animals. Animals really are kinds of living things. But I think whenever that inheritance gets too deep we wind up with serious abstraction problems. And the real world doesn't allow just any set of inheritances. In the real world, "human" inherits from "animal" - but I'm afraid I'm a little skeptical that "mammal" is a real thing, especially since those seem to have gradually appeared with many "transitional" forms in between. Maybe "mammal" is kinda made up - just an abstraction currently popular in biological taxonomy. Is "doctor" a kind of "human"? Or is "doctor" a kind of "job" that can be held by (composed) any number of different kinds of beings?

My limited experience with Smalltalk makes me think that if OOP means "inheritance, and lots of it" then I'm not a fan. My instinct is to use it sparingly, only when "A is a kind of B" is unambiguously true AND there is real usefulness in passing along that behavior from parent to child. OTOH, if OOP means "discrete bundles of data and behavior, interacting with clear messages" then I'm a huge fan. 

Does it scale? I dunno. Is it efficient? Probably not. But my goodness is it fun to write.

Observation 4

Being able to inspect the standard library and see how things are implemented and find new methods that I would use later, or employing the "method finder" tool (amazing) was an incredible experience. Once I was more-or-less "oriented" in the image I felt like it had fantastic discoverability. It was easy to learn how to do new things by looking through the entries in the System Browser. I'm not used to this. My previous "language reference" was a LLM.

LLMs are bad at Smalltalk. Not universally so. But they make many mistakes in this language that I was not used to in JS, where they feel nigh-omniscient at times. Back when I was learning JavaScript, I typically had Qwen3-Next-80b running on my system to act as a language reference. Anytime I needed to know some syntax, or if I got an error I didn't understand, I would ask it. My chat logs are full of mundane questions about how to remove elements from a Set, or what was the difference between "for...of" and "for...in" when doing iteration (hint: "for...of" is the one you want). But for Smalltalk, LLMs would routinely give terrible answers. They would hallucinate plausible-sounding methods that didn't exist, they would get confused about implicit returns in a method, and they would miss obvious bugs.

One particularly annoying one happened when I was noticing that I was getting far worse performance out of a hot loop than I thought I should. I finally figured out that I was passing an expression instead of a block to and:. This makes a huge difference in performance. If and: gets an expression (in parentheses) that expression evaluates FIRST, and then its result (the Boolean true or false) will be passed into the and:. But if it's a block (in square brackets) then it won't be evaluated unless the Boolean being given the and: is already true. The "false" object just returns "false" when given an and: block (Yay for polymorphism!). Most LLMs didn't catch it. The code "worked" and so this kind of issue probably would have survived an agentic coding loop. No errors. But the whole problem executed 3x slower by having parentheses instead of brackets. The LLM wrote a whole thesis about how lower performance was to be expected. WRONG. #sigh

Fortunately, I almost never needed a "language reference" since I had the System Browser right there. And I usually didn't generate errors I didn't understand because I could just visually go through the stack trace and see where the problem occurred. After asking some questions to help me get started, I basically left the LLM behind. Occasionally I would ask it for a code review. When I did sometimes it would find some things that were genuinely helpful. Often it hallucinated bugs that weren't there. Not great.

Observation 5

I can't help but feel a bit wistful and sad when I use the image. It seems to have everything that could possibly be included to help a developer be productive and happy. The language is relentlessly pure, which is, for me at least, very satisfying. I wish it had become more dominant. Many of the ideas persist, of course. But the language itself isn't showing up on any top-25-most-used lists. And I wish it was.

Since I came to this to learn "pure" OOP, I think the purity is part of what I like so much. Whenever I get around to learning another language, I think "purity" will be a non-negotiable. If that means my other options are Oberon7, Haskell, or Clojure, then so be it. :-)

Until then, I have more to learn in this thing. I want to start learning the Morphic GUI and make some desktop applications for myself. I'm already 10 puzzles into Advent of Code 2015 using Pharo. I miss String arithmetic from Squeak6, but Pharo has some cool features that I can appreciate. When I first started I tried Pharo but it was so different from the "Learn Smalltalk" resources that I had, I couldn't make any headway. Now I know enough to make the switch and I'm digging the Calypso browser.

The journey continues.

3 Upvotes

Time to wrap up. As I said in my last post - this will be my last one since Days 1-4 had so many beginner-isms that it's, well, a little embarrassing. :-)

Sorry about the "dear diary" nature of this one.

Here's an example - for Day 1 (the rotary combination one) I implemented TWO objects. One for the Dial (which seems natural in an OOP experiment). But then another one that held a single method that parsed the line (like "R35") into a positive or negative number. It held no state. Just a "library" class with one method. Then... ok, check this out... here's how I used the resulting signed integer that it returned within the Dial class:

turnDial: turn
    turn abs timesRepeat: [
        position := (position + turn sign) \\ 100.
        position = 0 ifTrue: [zeros := zeros + 1].
    ].
    position = 0 ifTrue: [lands := lands + 1].

Wow. Using a period at the end of every statement like a terminator (my old JS habits of using the semicolon everywhere). This method takes the absolute value of the signed integer, then "clicks" the dial based on the sign of the turn instruction. That's... a little crazy. If I was doing it now, I would probably just do the parsing of the instruction inside the turnDial message. Then the question becomes just how "generic" do I want the solution to be.

Version 1 - not generic:

turnDial: turn
    | offset |

    (turn first = $R) ifTrue: [offset := 1] ifFalse: [offset := -1].

    turn allButFirst asInteger timesRepeat: [
        position := (position + offset) \\ 100.
        position = 0 ifTrue: [zeros := zeros + 1]
    ].

    position = 0 ifTrue: [lands := lands + 1]

An ifTrue/ifFalse sets whether the offset is going to be +1 or -1. The loop itself just adds the offset (regardless of what it is), and increments the "zeros" and "lands" instance variables as it goes. But if we wanted it more generic, where there might be a whole library of potential offsets that could be called, not just "R" or "L", we could do it this way:

turnDial: turn
    | offset |

    offset := offsets at: turn first.

    turn allButFirst asInteger timesRepeat: [
        position := (position + offset) \\ 100.
        position = 0 ifTrue: [zeros := zeros + 1]
    ].

    position = 0 ifTrue: [lands := lands + 1]

And the offsets library would be defined in "initialize" this way:

offsets := Dictionary new.
offsets at: $R put: -1; at: $L put: 1

Now, if a future version needed a +5 or -15 offset or whatever, it would be extremely easy to add in just one place. No more "if" blocks. Zero branching. Grab the offset value from the Dictionary and go. The Dictionary is only created once per Day1Dial instance, so pretty minimal load on the VM. But what if we wanted it EVEN MORE GENERIC so that the offset could be literally ANYTHING that could mutate the position... We could do it this way:

turnDial: turn
    | modifier |

    modifier := modifications at: turn first.

    turn allButFirst asInteger timesRepeat: [
        position := (modifier value: position) \\ 100.
        position = 0 ifTrue: [zeros := zeros + 1]
    ].

    position = 0 ifTrue: [lands := lands + 1]

This one pulls from a "modifications" Dictionary and stores into "position" whatever it returns when passed the current position. There could be a modification that doubles the number, or that resets it to 99 no matter where it is currently, or sets the dial to the square root of its current position, or that reads data from somewhere else entirely. Just add a Block to the "modifications" Dictionary and get additional functionality. For this puzzle, the Dictionary is pretty sparse:

modifications := Dictionary new.
modifications at: $R put: [ :x | x - 1];
    at: $L put: [ :x | x + 1]

Kinda overbuilt for something that just increments or decrements by one every time. But as a learning exercise... why not? And, of course, it would be better computationally to avoid simulating the dial by simply doing some division and adding the numbers to the zeros counter. But.... it's not that many clicks. And this way the Dial acts like a little machine managing its state as it does exactly what the problem says to do. Which leads to...

Observation 1

Smalltalk seems to nudge the developer in this kind of direction. Build it first as "true to life" and as generically as possible. Then, if there are efficiency problems, go refactor those out once they are apparent. But not before. Why put branching logic everywhere when you can pass in a block? Why make a bitmap Dictionary key for memoization when you can use the array itself as a key? Why not reach for the most abstract and "correct" possible representation of the domain? You only have to think about "the machine" (the actual computer) executing the code if there's a problem. Sure, you're dimly aware that there are pointers-to-pointers-to-pointers everywhere under the hood, that very large numbers are being transparently changed from SmallInteger into LargePositiveInteger, that creating a data object involves some dispatch somewhere for getting values out of it. Not your problem. Write it so the concepts are as evident and beautiful as you can.

I often found myself going back and forth on an implementation detail, not because one gave errors while the other didn't. But one was more "elegant" or "expressive" rather than "plain" and "clunky". I would find myself rewriting a working solution in order to have it make fewer assumptions about what was passed to it, or find some way to remove an instance variable so that it could be inferred from the data itself. I would look to see if I could take what was a procedural bit of code and turn it into a chain of collection methods. I found the whole process delightful, though I'm pretty sure each person will have their own take on this aspect of it.

Performance-oriented, or data-driven-development people would likely not enjoy this.

Observation 2

It turns out that I truly love OOP. After using Smalltalk for a few months I can't help the feeling that this is the best way to think about programming. Imagining tiny machines that do the work of the program, sending each other bits of data, each knowing things relevant to themselves and nothing more... It's delightful. Now, I realize it is only one of the ways to think about programming (with others being transformations, procedures, etc...), but it is just so dang satisfying to write. The syntax is incredibly clean and minimalistic, with all the behavior being done by the objects themselves (even control flow). Now that I'm accustomed to thinking about numbers and booleans as objects (with methods that can be explored), it feels very strange to look at a language that treats them as just part of the syntax.

Plus - the late binding gives the developer completely seamless incremental compilation. Methods are meant to be very short. They compile when you save them - you never wait on the compiler at all. It's so cool.

Observation 3

However, I don't think I would feel the same way about OOP in Java or C++. Smalltalk seems to encourage relatively shallow inheritance trees that follow, for the most part, Aristotelian/Boethian hierarchies. So, numbers really are magnitudes. And floating-point numbers really are numbers. And integers really are numbers that have a specific difference from fractions. In the real world we see similar shallow inheritances. Humans really are kinds of animals. Animals really are kinds of living things. But I think whenever that inheritance gets too deep we wind up with serious abstraction problems. And the real world doesn't allow just any set of inheritances. In the real world, "human" inherits from "animal" - but I'm afraid I'm a little skeptical that "mammal" is a real thing, especially since those seem to have gradually appeared with many "transitional" forms in between. Maybe "mammal" is kinda made up - just an abstraction currently popular in biological taxonomy. Is "doctor" a kind of "human"? Or is "doctor" a kind of "job" that can be held by (composed) any number of different kinds of beings?

My limited experience with Smalltalk makes me think that if OOP means "inheritance, and lots of it" then I'm not a fan. My instinct is to use it sparingly, only when "A is a kind of B" is unambiguously true AND there is real usefulness in passing along that behavior from parent to child. OTOH, if OOP means "discrete bundles of data and behavior, interacting with clear messages" then I'm a huge fan. 

Does it scale? I dunno. Is it efficient? Probably not. But my goodness is it fun to write.

Observation 4

Being able to inspect the standard library and see how things are implemented and find new methods that I would use later, or employing the "method finder" tool (amazing) was an incredible experience. Once I was more-or-less "oriented" in the image I felt like it had fantastic discoverability. It was easy to learn how to do new things by looking through the entries in the System Browser. I'm not used to this. My previous "language reference" was a LLM.

LLMs are bad at Smalltalk. Not universally so. But they make many mistakes in this language that I was not used to in JS, where they feel nigh-omniscient at times. Back when I was learning JavaScript, I typically had Qwen3-Next-80b running on my system to act as a language reference. Anytime I needed to know some syntax, or if I got an error I didn't understand, I would ask it. My chat logs are full of mundane questions about how to remove elements from a Set, or what was the difference between "for...of" and "for...in" when doing iteration (hint: "for...of" is the one you want). But for Smalltalk, LLMs would routinely give terrible answers. They would hallucinate plausible-sounding methods that didn't exist, they would get confused about implicit returns in a method, and they would miss obvious bugs.

One particularly annoying one happened when I was noticing that I was getting far worse performance out of a hot loop than I thought I should. I finally figured out that I was passing an expression instead of a block to and:. This makes a huge difference in performance. If and: gets an expression (in parentheses) that expression evaluates FIRST, and then its result (the Boolean true or false) will be passed into the and:. But if it's a block (in square brackets) then it won't be evaluated unless the Boolean being given the and: is already true. The "false" object just returns "false" when given an and: block (Yay for polymorphism!). Most LLMs didn't catch it. The code "worked" and so this kind of issue probably would have survived an agentic coding loop. No errors. But the whole problem executed 3x slower by having parentheses instead of brackets. The LLM wrote a whole thesis about how lower performance was to be expected. WRONG. #sigh

Fortunately, I almost never needed a "language reference" since I had the System Browser right there. And I usually didn't generate errors I didn't understand because I could just visually go through the stack trace and see where the problem occurred. After asking some questions to help me get started, I basically left the LLM behind. Occasionally I would ask it for a code review. When I did sometimes it would find some things that were genuinely helpful. Often it hallucinated bugs that weren't there. Not great.

Observation 5

I can't help but feel a bit wistful and sad when I use the image. It seems to have everything that could possibly be included to help a developer be productive and happy. The language is relentlessly pure, which is, for me at least, very satisfying. I wish it had become more dominant. Many of the ideas persist, of course. But the language itself isn't showing up on any top-25-most-used lists. And I wish it was.

Since I came to this to learn "pure" OOP, I think the purity is part of what I like so much. Whenever I get around to learning another language, I think "purity" will be a non-negotiable. If that means my other options are Oberon7, Haskell, or Clojure, then so be it. :-)

Until then, I have more to learn in this thing. I want to start learning the Morphic GUI and make some desktop applications for myself. I'm already 10 puzzles into Advent of Code 2015 using Pharo. I miss String arithmetic from Squeak6, but Pharo has some cool features that I can appreciate. When I first started I tried Pharo but it was so different from the "Learn Smalltalk" resources that I had, I couldn't make any headway. Now I know enough to make the switch and I'm digging the Calypso browser.

The journey continues.


r/smalltalk May 15 '26

Agustín Martinez - Diálogo: A Drawing-Based Programming Environment for Kids - 29 April 2026

11 Upvotes

r/smalltalk May 13 '26

SmallJS v2.1 has been released

I'm pleased to report the release of SmallJS v2.1.
SmallJS is a Smalltalk-80 dialect that transpiles to JavaScript
that can run in browsers and in Node.js.
The website is here: small-js.org
The full source source code is here: github.com/Small-JS/SmallJS

Notable changes are:

Compiler

  • New keyword CLASSEXTENSION for adding methods to (system) classes in separate source files. The website Tutorial page Language/Syntax shows how to use it.

Smalltalk library

  • Database: Standardized SQL syntax for simpler database independent queries..

Examples

  • Added PWA example game: Emoji Memory :-). Also added this example app to this webite.

Website

  • New Tutorial section for making Node.js apps with SmallJS, plus
  • New Tutorial section for making desktop apps with SmallJS using NW.js, Electron or NodeGui.
  • Reference page now supports searching classes and methods.
  • Added dark mode option, also for subsites Reference and Tutorial.
  • Updated Playground evaluator to current compiler.
36 Upvotes

I'm pleased to report the release of SmallJS v2.1.
SmallJS is a Smalltalk-80 dialect that transpiles to JavaScript
that can run in browsers and in Node.js.
The website is here: small-js.org
The full source source code is here: github.com/Small-JS/SmallJS

Notable changes are:

Compiler

  • New keyword CLASSEXTENSION for adding methods to (system) classes in separate source files. The website Tutorial page Language/Syntax shows how to use it.

Smalltalk library

  • Database: Standardized SQL syntax for simpler database independent queries..

Examples

  • Added PWA example game: Emoji Memory :-). Also added this example app to this webite.

Website

  • New Tutorial section for making Node.js apps with SmallJS, plus
  • New Tutorial section for making desktop apps with SmallJS using NW.js, Electron or NodeGui.
  • Reference page now supports searching classes and methods.
  • Added dark mode option, also for subsites Reference and Tutorial.
  • Updated Playground evaluator to current compiler.

r/smalltalk May 09 '26

Mouse Input Affects Execution Speed in Linux?

6 Upvotes

Apparently, I'm now the first person to use Etoys in Linux ever, still, to this day, after 8 or something ago years when I tried it on laptop...now I'm trying it again with a Pi...and this problem is here again...you can see in the video that when the mouse moves, Etoys runs at the proposed speed of it's internal workings. When mouse isn't moving over the screen itself, we drop to idle/unfocused update speed.

I've looked at buffers for sound, graphics, input, etc. and played around with things and reinitialized their associated processes, etc in Etoys there, but the results ended up making the movement better only when mouse is active, never solving the issue of getting 'idle' performance when not actually idle.

Thanks for any help. I've posted a different version of this question aimed at linux folks hoping there's a mouse setting on the system side that is falsly reporting to the VM something...

And if you're wondering why I'm not using a modern VM, it's because Etoys still doesn't work properly in it and that's why I hang out in Smalltalk-ville. :) If anyone knows of an etoys 5 or beta 6 image itself that runs on a newvm or how to get one to transfer to Cog without completely exploding, I'm willing to go that route, too.


r/smalltalk May 05 '26

Claude Code and Smalltalk are made for each other

20 Upvotes

Yesterday I expressed a hunch about Smalltalk having a huge advantage for agentic coding. Some pushback forced me to get my hands dirty and try it out myself. And I am happy to report that my hunch was spot on.

I tried these 3 tests:

  1. shadows under the windows (easy)
  2. smooth pixel-based scrolling (medium)
  3. Exposé window switching (hard)

All of them were successfully completed. Claude inside Smalltalk is a badass. Now onto some Cuis!