"When we start cataloging the gains in tools sitting on a computer, the benefits of software are amazing. But, if the benefits of software are so great, why do we worry about making it easier—don’t the ends pay for the means? We worry because making such software is extraordinarily hard and almost no one can do it—the detail is exhausting, the creativity required is extreme, the hours of failure upon failure requiring patience and persistence would tax anyone claiming to be sane. Yet we require that people with such characteristics be found and employed and employed cheaply."
—Richard P. Gabriel, Patterns of Software
Tag: code
Design Patterns
It is sometimes suggested by well-meaning language enthusiasts that "My language is complete and powerful, so design patterns don't apply here!" Sadly, they are incorrect.
Design patterns happen in every language. The "Gang of Four" Design Patterns book just collected the ones observed in Smalltalk, and ported them to C++, later rewrites to Java, etc. These are not recipes to blindly follow, but examples meant to show you how to find and regularize the ones in your code.
It's somewhat difficult to see them unless you've read Christopher Alexander's books, and written a lot of programs in some language, and specifically looked for the places where you repeat a structure for livability's sake. Just as it's hard for an architect to make a path where people will want it, unless they first observe how people live and get around that space, and then convert the ad-hoc trails people follow into paths.
Smalltalk is an extremely expressive language (it failed in the market because every ST program is IDE-specific), it has closures, allows you to very trivially make new control structures; it doesn't need a hack like macros because the entire language is that freeform. And this is where the GoF authors observed these paths being made by themselves and other developers, not just in limited BDSM languages like Java.
So, a little light reading:
- "The Perfection of Imperfection", by Christopher Alexander
- Notes on the Synthesis of Form, by Christopher Alexander
- A Pattern Language, by Christopher Alexander
- The Timeless Way of Building, by Christopher Alexander
- Patterns of Software, by Richard P. Gabriel
- Design Patterns, by Gamma, Helm, Johnson, Vlissides
Tower of Babble
Programmers almost compulsively make new languages; within just a few years of there being computers, multiple competing languages appeared:
- 1948-1952: First stored-program computers
- 1952: Laning & Zierler System; IBM Speedcoding by John W. Backus
- 1955: FLOW-MATIC, by Grace Hopper
- 1957: FORTRAN, by John W. Backus
- 1958: LISP, by John McCarthy
- 1958: ALGOL, by ACM-GAMM committee
- 1959: COBOL, by CODASYL committee, which I talked about recently
It proliferated from there into millions; probably half of all programmers with 10+ years of experience have written one or more.
I've written several, as scripting systems or toys. I really liked my Minimal script in Hephaestus 1.0, which was like BASIC+LISP, but implemented as it was in Java the performance was shitty and I had better options to replace it. My XML game schemas in GameScroll and Aiee! were half programmer humor, but very usable if you had a good XML editor. Multiple apps have shipped with my tiny lisp interpreter Aspic, despite the fruit company's ban on such things at the time. A Brainfuck/FORTH-like Stream, working-but-incomplete tbasic, and a couple PILOT variants (I think PILOT is hilariously on the border of "almost useful").
Almost every new language is invented as marketing bullshit based on a few Ur-languages:
- C++: Swift
- Java: Javascript (sorta), C#, Go
- Awk: Perl, Python, PHP, Julia
- C: Rust
- Smalltalk: Objective-C
- Prolog: Erlang, Elixir
- ALGOL: C, Pascal, PL/1, Simula, Smalltalk, Java
- LISP: Scheme, ML, Haskell, Clojure, Racket
- BASIC: None, other than more dialects of BASIC.
- FORTRAN: None in decades, but is the direct ancestor of ALGOL & BASIC.
- COBOL: None in decades.
A few of these improve on their ancestors in some useful way, often performance is better, but most do nothing new; it's plausible that ALGOL 68 is a better language than any of its descendants, it just has mediocre compiler support these days.
Certainly I've made it clear I think Swift is a major regression, less capable, stable, fast, or even readable than C++, a feat I would've called impossible except as a practical joke a decade ago. When Marzipan comes out, I'll be able to rebuild all my 15 years of Objective-C code and it'll work on 2 platforms. The Swift 1.0 app I wrote and painfully ported to 2.0 is dead as a doornail, and current Swift apps will be uncompilable in 1-2 years; and be lost when Apple abandons Swift.
When I want to move my Scheme code to a new version or any other Scheme, it's pretty simple, I made only a handful of changes other than library importing from MIT Scheme to Chez to Chicken 4 to Chicken 5. When I tested it in Racket (which I won't be using) I had to make a handful of aliases. Probably even CLISP (which is the Swift of LISPs, except it fossilized in 1994) would be 20 or 30 aliases; their broken do
iterator would be hard but the rest is just naming.
Javascript is a pernicious Herpes-virus-like infection of browsers and desktops, and nothing can ever kill it, so where it fits the problem, there's no reason not to use it. But there's a lot it doesn't do well.
I was leery of using FreePascal because it has a single implementation (technically Delphi still exists, but it's $X,000 per seat on Windows) and minimal libraries, and in fact when it broke on OS X Mojave, I was disappointed but I-told-you-so.
I'm not saying we should quit making new Brainfuck and LOLCODE things, I don't think it's possible for programmers to stop without radical brain surgery. But when you're evaluating a language for a real-world problem, try moving backwards until you find the oldest and most stable thing that works and will continue to work, not piling more crap into a rickety new framework.
The Biblical reference in the title amuses me, because we know now that it requires no malevolent genocidal war deity scared of us invading Heaven to magically confuse our languages and make us work at cross purposes; anyone who can write and think splinters their thought into a unique language and then argues about it.
Lost Treasure
In 1979, I learned to program in BASIC on a TRS-80 Model I. Sometime in the next year, I read one of my first programming books:
- Stimulating Simulations by C. William Engel (I'm not sure which edition; it had the dungeon game, but I think a TRS-80 version, not Atari? Maybe PET, tho I was never a Commode-odor user.)
I played Monster Chase and Lost Treasure, modified them extensively, and combined them, so the cave on the island had a monster chase to reach the exit. I recall having problems getting Starship Alpha and Devil's Dungeon to work, but they joined my software library eventually.
One of my earliest and happiest programming memories was sitting at the dining room table, reading Monster Chase, and writing out a smarter movement system and obstacles in a notebook; at the time the only computers were at school, so I wrote code on paper and typed them in later.
So when I found the book again on archive.org last night, I was very excited, and had to reimplement it. I actually typed this into Pythonista on my phone with the PDF open on an iPad, only moved it to the computer to do some final cleanup and upload it.
The book suggests some modifications, and I did some minor ones: Lowered the movement error to 10%, and risk of shark attack to 10%, rising by 1.5x rather than a flat +50% each time; being anywhere near the island edge killed you too often in the original. I also don't move you out of the water automatically, that should cost a turn.
I realized in converting it that I hate, hate, hate Row,Column coordinates instead of Cartesian X,Y; tons of mainframe-era computing resources used Row,Column, and you can still see it in some APIs like Curses. Note that the original program is 74 lines, mine's 214; BASIC is a terrible language, but it's terse.
I could adapt this into another doorgame for my Mystic Dungeon BBS, but I'm not sure what the multiplayer aspect would be, and it has limited replayability without doing some randomization.
Return of the Objective-C Jedi
[[[ These ]]] are your father's square brackets, the weapons of a Jedi Knight.
Not as clumsy or random as C++.
Elegant weapons for a more civilized age.
- Apple's Objective-C books used to be top-notch, but they've been neglected and the new doc site doesn't link to them. There's PDFs at various sites.
- Cocoa Dev Central: Tutorials, books, and aggregation of other Cocoa-programming sites. You may need to use the wayback machine to read some of them.
What's Different in Mulle-ObjC
- MulleFoundation
- blog post
- feature list
ObjC 1.0: C plus Smalltalk <- appeasement of the Java Noobs ObjC++ ObjC 2.0 <- usurpation by the C++ Flagellants
This is like Objective-C circa 2010(?), good but not fully baked. Far better than circa 1986-2009, when it was a very thin translation layer over C.
- No ARC (Automatic Reference Counting). This is just invisible sugar to hide retain/release/autorelease, and while ARC's convenient, it's trivial if you actually know how reference counting works. Don't really miss it.
- No dot property syntax.
[[myObj name] length]
instead ofmyObj.name.length
, and[myObj setName:newName]
instead ofmyObj.name = newName
. I can live with it, but I really did like dot syntax, even if it does "overload" the . operator and hide the distinction between methods and variables.- When dot syntax came out, Objective-C nerds came close to fistfights over this. You would not believe the venom some people had for it. Most of those nerds died or quit or got old & tired before fucking Swift came around, I guess.
- No array syntax.
[myList objectAtIndex:i]
instead ofmyList[i]
. This is a pain in the ass, I'll have to write some shorthand macros (or rather, go dig them out of my very oldest code). - No blocks. This one hurts, but it's a reasonable pick-your-battles decision. Classic: Write a method, dispatch to it, and call back success somehow. Blocks: create a weakSelf reference, enclose it, search-replace self in your block, pick one of a half-dozen complex GCD methods, get a memory leak because you retained something across the block boundary. This is annoying but logically simpler:
[self performSelectorInBackground:@selector(computeData) withObject:inputData]; - (void)computeData:(id)inputData { // create outputData [self setOutputData:outputData]; [[NSNotificationCenter defaultCenter] postNotification:NOTI_DataComputed]; }
- Has object literals:
@42
and@(var)
create anNSNumber
,@[]
creates anNSArray
,@{}
creates anNSDictionary
; dicts use key:value order, not the reverse order used in-[NSDictionary dictionaryWithObjectsAndKeys:]
, and array and dicts don't need a trailingnil
, which was a constant source of mystifying bugs back in the day. Big win!- Hmn, crashes if you do something janky like
[@[] mutableCopy]
:mulle_objc_universe 0x1006adef0 fatal: unknown method 5e1b0403 "-getObjects:range:" in class 7aa0d636 "_MulleObjCEmptyArray"
- Hmn, crashes if you do something janky like
- Has
for (id x in container)
loops, using NSFastEnumeration. The 1.0 process of looping enumerations was awful, so this is very nice. - Huh, does have
@autoreleasepool
, so maybe I should use that instead of NSAutoreleasePool like a caveman? It compiles and seems to work. - Properties have properties assign/retain nonatomic/atomic nonnullable readonly, default is assign nonatomic, no "nullable" or "readwrite" flags needed. As it should be.
- Weird
isa
define instead of pointer: blog post
TODO
- I haven't set up an NSRunLoop or the equivalent of NSApplication (which is in AppKit, not Foundation), need to do that and then I'll have a working app template.
Writing Objective-C with Mulle-Objc
- mulle-objc #MakeObjCGreatAgain is apparently functional, so let's see how that goes.
mkdir CLICalc
cd CLICalc
mulle-sde init -m foundation/objc-developer executable
This takes more or less forever.
… Still going. OK, finally done. I hate to think it's gonna do that every new project? Or whenever it updates?
Anyway, bbedit .
(fuck Xcode), and add at the bottom of import.h
and import-private.h
:
#import <Foundation/Foundation.h>
Make src/main.m
useful:
// main.m
#import "import-private.h"
#import "CLICalc.h"
int main(int argc, char *argv[]) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
CLICalc *calc = [[CLICalc alloc] init];
[calc push:42.0];
[calc push:69.0];
double a = [calc pop];
double b = [calc pop];
NSLog(@"a=%f, b=%f", a, b);
[pool release];
return 0;
}
Create an Objective-C class, src/CLICalc.h
:
// CLICalc.h
#import "import-private.h"
@interface CLICalc : NSObject
@property (retain) NSMutableArray *stack;
- (void)push:(double)n;
- (double)pop;
@end
and src/CLICalc.m
:
// CLICalc.m
#import "CLICalc.h"
@implementation CLICalc
@synthesize stack = _stack;
- (id)init {
self = [super init];
_stack = [[NSMutableArray alloc] init];
return self;
}
- (void)dealloc {
NSLog(@"CLICalc dealloc");
[_stack release];
[super dealloc];
}
- (void)push:(double)n {
[_stack addObject:@(n)];
}
- (double)pop {
if ( ! [_stack count]) {
// ERROR: stack underflow
return 0.0;
}
double n = [[_stack lastObject] doubleValue];
[_stack removeLastObject];
return n;
}
@end
Doing that without a template was a little hard on the old memory, and I had to use DDG to look up some method names without autocompletion. But I'm pretty sure that's fine.
In mulle-ide, type update
to add the new class to cmake: If you look in cmake/_Sources.cmake you should now see CLICalc.m
listed.
Now craft
to compile. You'll get a spew of crap, but hopefully no errors.
I am getting this, which I can't resolve:
/Users/mdh/Code/CodeMac/CLICalc/src/main.m:21:55: warning: 'NSAutoreleasePool'
may not respond to 'init'
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
~~~~~~~~~~~~~~~~~~~~~~~~~ ^
1 warning generated.
But NSAutoreleasePool certainly has init, and it seems to not die?
% ./build/Debug/CLICalc
a=69.000000, b=42.000000
Hooray!
Yeah, this isn't amazing. Except: It's supposedly portable now. I can maybe rebuild this on Linux, or Windows? I dunno.
This is almost classic Objective-C, slightly enhanced from 1.0: We didn't have property/synthesize, or nice object wrappers like @() when I were a lad. I typed so many [NSNumber numberWithInteger:n]. So get used to the retain/release/autorelease dance. There's no dot-syntax for property access, type them [] like old-school. But hey, it's a proper compiled language with a nice object system and no GC pausing.
I tried importing Cocoa and got a ludicrous spew of errors, so Mac GUI is gonna be a challenge. But I could import SDL and use that for portable UI, since Objective-C is just C.
Sweet. I'll finish up the calculator's parser in a bit, but then see about doing something useful in it.
Writing a Doorgame
So, how did I write this? Mystic BBS has a Pascal-based scripting, but I didn't really like using that; it's very powerful, but a pain in the ass to code/compile/test on, and it's just different enough from normal FreePascal to annoy me. And then tried to use Mystic's "integrated Python", which just doesn't work; I tried everything to make it find Python. And it's 2.7, so bleh. BUT: Adding a doorgame with the menu system lets you specify any executable, including a Python 3.7 script, and pass in a few command-line arguments about user and directories, so that's what I did (though the server I'm using only has Python 3.6, which hasn't been a problem yet, but be aware)…
The BBS handles all the redirection from network to stdin/stdout, otherwise I could use inetd
for the same thing, but then I'd have to deal with all the login and networking state. Doing this as a door simplifies programming back to 1970s level!
I'll eventually open source it, but while I'm iterating and early on, I want to keep some of my secrets.
ANSI Color
There's ANSI color everywhere, of course. curses is fucking awful and archaic, and built to handle many incompatible terminals, which isn't a problem anymore. Mystic uses the same trick I'm using, and many others back in the day: Strings have escape codes |XX
in them. Mystic uses numbers for colors, which is awful, and has hundreds of BBS-specific strings, which I have no need of. So all I did is this:
# set to 1200, 9600, etc. to simulate modem transmission rate
kModemSpeed = 0
kLinesPerScreen = 24
kAnsiEsc = "\x1b["
# 0 1 2 3 4 5 6 7 8 9 10
kAnsiBoxLine = ["─", "│", "┌", "┬", "┐", "├", "┼", "┤", "└", "┴", "┘",]
kAnsiBoxDouble =["═", "║", "╔", "╦", "╗", "╠", "╬", "╣", "╚", "╩", "╝",]
kAnsiBoxHeavy = ["━", "┃", "┏", "┳", "┓", "┣", "╋", "┫", "┗", "┻", "┛",]
# ansiFilter, called by write/writeln, replaces |XX with escape codes:
kAnsiEscapeCode = {
"CD": kAnsiEsc+"J", # Clear Down
"CL": kAnsiEsc+"2K", # Clear Line
"CS": kAnsiEsc+"2J"+kAnsiEsc+"H", # Clear Screen
"DI": kAnsiEsc+"2m", # Dim
"ES": "\x1b", # Escape ASCII 27
"GG": "\x07", # Bell ASCII 7
"HR": kAnsiBoxDouble[0] * 79, # Horizontal Ruler
"LI": kAnsiEsc+"1m", # Light/Bright
"PI": "|", # Pipe char |
"RE": kAnsiEsc+"0m", # Reset Attributes
"UN": kAnsiEsc+"4m", # Underscore
"FK": kAnsiEsc+"30m", # Foreground Black
"FR": kAnsiEsc+"31m", # Foreground Red
"FG": kAnsiEsc+"32m", # Foreground Green
"FY": kAnsiEsc+"33m", # Foreground Yellow
"FB": kAnsiEsc+"34m", # Foreground Blue
"FM": kAnsiEsc+"35m", # Foreground Magenta
"FC": kAnsiEsc+"36m", # Foreground Cyan
"FW": kAnsiEsc+"37m", # Foreground White
"LK": kAnsiEsc+"1;30m", # Foreground Light Black
"LR": kAnsiEsc+"1;31m", # Foreground Light Red
"LG": kAnsiEsc+"1;32m", # Foreground Light Green
"LY": kAnsiEsc+"1;33m", # Foreground Light Yellow
"LB": kAnsiEsc+"1;34m", # Foreground Light Blue
"LM": kAnsiEsc+"1;35m", # Foreground Light Magenta
"LC": kAnsiEsc+"1;36m", # Foreground Light Cyan
"LW": kAnsiEsc+"1;37m", # Foreground Light White
"BK": kAnsiEsc+"40m", # Background Black
"BR": kAnsiEsc+"41m", # Background Red
"BG": kAnsiEsc+"42m", # Background Green
"BY": kAnsiEsc+"43m", # Background Yellow
"BB": kAnsiEsc+"44m", # Background Blue
"BM": kAnsiEsc+"45m", # Background Magenta
"BC": kAnsiEsc+"46m", # Background Cyan
"BW": kAnsiEsc+"47m", # Background White
}
def ansiFilter(s):
"""Replaces |XX codes with values defined in `kAnsiEscapeCode`."""
out = ""
slen = len(s)
i = 0
while i < slen:
c = s[i]
i += 1
if c == "|":
esc = s[i:i+2]
i += 2
out += kAnsiEscapeCode.get(esc, esc)
else:
out += c
return out
def ansiGoto(pt):
return "%s%s;%sH" % (kAnsiEsc, pt[1], pt[0])
def readln(prompt):
"""Calls input after filtering prompt."""
return input(ansiFilter(prompt))
def write(s, end=None):
"""Filters `s` and writes to stdout, writes `end` if needed, flushes stdout."""
if type(s) != str:
s = str(s)
lines = 0
out = ansiFilter(s)
for c in out:
sys.stdout.write(c)
if kModemSpeed:
time.sleep(1.0/kModemSpeed)
if c == '\n':
lines += 1
if lines >= kLinesPerScreen:
readln("|RE|FC[MORE]|RE")
lines = 0
if end:
sys.stdout.write(end)
sys.stdout.flush()
def writeln(s):
"""Calls write with `end='\n'`."""
write(s, end='\n')
Now if I want someone to talk in bold cyan (which I chose as my standard NPC color), I just
writeln("|RE|LC%s says, \"Mellow greetings, sir!\"|RE" % (npc["name"],))
RE before and after text is important, resetting colors from previous strings.
The one problem is I can't use | in ASCII art, so to generate my big character logos, I write a script mysticFiglet.zsh
, making : the vertical char:
figlet -f small "$*"|sed "-es/|/:/g"
Flow Control
The great part about a doorgame is, no event loop. Oh, there's a REPL of sorts:
def run(self):
self.running = True
try:
#TODO: Python 3.8: while line := self.prompt():
while self.running:
line = self.prompt()
if line == None:
break
words = line.split()
if len(words) == 0:
continue
self.parseWords(words)
self.save()
self.doHelp("?", "_outro")
return 0
except (EOFError, KeyboardInterrupt):
self.save()
return 0
except Exception:
writeln("|GG|RE|BR|FWERROR! Something went wrong.|RE")
logging.error("", exc_info=True)
self.save()
return 1
It just blocks at input (called by readln, called by prompt), and continues on until self.running = False, or an exception is thrown. Whee! The usual game loop makes it impossible to get stateful input without leaving giant memos of current state, so the event loop can keep cycling at 60fps.
Data Files
One solution would be to just make everything in classes, and use Python's pickle to archive everything. But the problem is that's pure binary, you can't easily hack on it or see what's happened, and on load you can't change any values. So instead I only use primitive types and archive everything to and from JSON. Floor maps and Players have a toData() method which produces a JSON-compatible data structure, and their initializers take a data structure and pull out or translate the values they need. It's a little more work, but means I can debug faster and add new fields faster.
The floors use a super-simple data file format, with an ASCII roguelike map and a series of command lines to create NPCs, traps, text, etc. The weapons and armor are listed in an even simpler format, with space-delimited fields.
Both the command lines and item formats are just run thru a tokenizer I wrote, that reads words, "strings", numbers, and [lists]:
type name cost stat bonus damage inflicts twoh
wpn "Club" 5 Str 0 3 [blunt] 0
becomes:
{'type': 'wpn', 'name': 'Club', 'cost': 5, 'bonus': 0, 'damage': 3, 'inflicts': ['blunt'], 'twoh': 0}
Bookmarklets
I just fixed a bug and re-uploaded these. They're little scripts that sit in a bookmark, like on your favorites bar, and do something useful to the web page. Monochrome Dark & Light change the page style; View Source puts source and plain text in boxes you can easily view and copy-paste from.
Learn2JS Updates
Added to my Learn2JS project, and it's fairly usable now for rapid development, I can move over the application logic of little tools and they just show up in the catalog and work. Still no live editor, you have to drop a script in lib or a user dir, but it's getting closer to instant-on coding!
Try it out, then look at the scripts, alien
is just block sprites but it's a decently hard shooter (there's some oddness about hit detection, and it needs upgrade drops to be a real game). maze
is the usual maze generator; drawing text right now because I haven't hooked up the sprite graphics. Both are about as close to minimal code needed for the task as you can get.
Runes
A text filter to convert ASCII sequences into nice Unicode or emoji. Call it from your favorite editor, or on the command line:
% echo "BEFORE {circle:this is some hollow text.} AFTER" |runes.py
BEFORE ⓉⒽⒾⓈ ⒾⓈ ⓈⓄⓂⒺ ⒽⓄⓁⓁⓄⓌ ⓉⒺⓍⓉ⊙ AFTER
More instructions in the README file.