God bless the person that invented the command line history. They just saved me 15 minutes of work.
And while we’re handing out praises, thank you to the person that added RunAndReturn
to the Mockery mock generator. I might be able to climb out of this mocking hell with this before the day is up.
One other thing from Rec Diffs #233: it’s amusing to hear Siracusa being as frustrated with Britishism seeping into American English as I am with Americanisms seeping into Australian English.
John, I know how you feel. π
Favourite Comp. Sci. Textbooks
John Siracusa talked about his two favourite textbooks on Rec Diffs #233: Modern Operation Systems, and Computer Networks, both by Andrew S. Tanenbaum. I had those textbooks at uni as well. I still do, actually. They’re fantastic. If I were to recommend something on either subject, it would be those two.
I will add that my favourite textbook I had during my degree was Compilers: Principal, Techniques and Tools by Alfred V. Aho, et al. also known as the “dragon book.” If you’re interested in compiler design in any way, I can definitely recommend this book. It’s a little old, but really, the principals are more or less the same.
And that makes it the third time this week that I encountered a bug involving DynamoDB that was avoidable with a unit test that actually used a proper database.
(To be fair, this time is was my fault: I haven’t got around to writing the unit test yet).
Github/Gitlab code search is fine, but have you ever tried grep -l -r methodName projects/*
? Seems to be not that much slower and like 100x more reliable.
I wish Ghost allowed readers to choose a different email address to send newsletters, rather than just send them to the email address associated with the account itself. I’ve got news for you: send reading material to my personal inbox and I’ll never see it. That’s just not where I read stuff: it’s all in Feedbin.
Even better would be a private RSS feed. I know Gruber had issues with doing way back during the Google Reader days. But those days are gone, so it might be worth trying this again. Seems to work for Stratechery.
For the last few years, I’ve been using 4/24 as the expiry date of test credit cards within Stripe. Well those days are literally in the past now.
UCL: Breaking And Continuation
I’ve started trying to integrate UCL into a second tool: Dynamo Browse. And so far it’s proving to be a little difficult. The problem is that this will be replacing a dumb string splitter, with command handlers that are currently returning a tea.Msg type that change the UI in some way.
UCL builtin handlers return a interface{}
result, or an error
result, so there’s no reason why this wouldn’t work. But tea.Msg
is
also an interface{}
types, so it will be difficult to tell a UI
message apart from a result that’s usable as data.
This is a Dynamo Browse problem, but it’s still a problem I’ll need to
solve. It might be that I’ll need to return tea.Cmd types β which
are functions returning tea.Msg
β and have the UCL caller detect these
and dispatch them when they’re returned. That’s a lot of function
closures, but it might be the only way around this (well, the
alternative is returning an interface type with a method that returns a
tea.Msg
, but that’ll mean a lot more types than I currently have).
Anyway, more on this in the future I’m sure.
Break, Continue, Return
As for language features, I realised that I never had anything to exit
early from a loop or proc. So I added break
, continue
, and return
commands. They’re pretty much what you’d expect, except that break
can optionally return a value, which will be used as the resulting value
of the foreach
loop that contains it:
echo (foreach [5 4 3 2 1] { |n|
echo $n
if (eq $n 3) {
break "abort"
}
})
--> 5
--> 4
--> 3
--> abort
These are implemented as error types under the hood. For example,
break
will return an errBreak
type, which will flow up the chain
until it is handled by the foreach
command (continue
is also an
errBreak
with a flag indicating that it’s a continue). Similarly,
return
will return an errReturn
type that is handled by the proc
object.
This fits quite naturally with how the scripts are run. All I’m doing
is walking the tree, calling each AST node as a separate function call
and expecting it to return a result or an error. If an error is return,
the function bails, effectively unrolling the stack until the error is
handled or it’s returned as part of the call to Eval()
. So leveraging
this stack unroll process already in place makes sense to me.
I’m not sure if this is considered idiomatic Go. I get the impression
that using error types to handle flow control outside of adverse
conditions is frowned upon. This reminds me of all the arguments against
using expressions for flow control in Java. Those arguments are good
ones: following executions between try
and catch
makes little sense
when the flow can be explained more clearly with an if
.
But I’m going to defend my use of errors here. Like most Go projects,
the code is already littered with all the if err != nil { return err }
to exit early when a non-nil error is returned. And since Go developers
preach the idea of errors simply being values, why not use errors here
to unroll the stack? It’s better than the alternatives: such as
detecting a sentinel result type or adding a third return value which
will just be yet another if bla { return res }
clause.
Continuations
Now, an idea is brewing for a feature I’m calling “continuations” that might be quite difficult to implement. I’d like to provide a way for a user builtin to take a snapshot of the call stack, and resume execution from that point at a later time.
The reason for this is that I’d like all the asynchronous operations to
be transparent to the UCL user. Consider a UCL script with a sleep
command:
echo "Wait here"
sleep 5
echo "Ok, ready"
sleep
could simply be a call to time.Sleep()
but say you’re running
this as part of an event loop, and you’d prefer to do something like
setup a timer instead of blocking the thread. You may want to hide this
from the UCL script author, so they don’t need to worry about
callbacks.
Ideally, this can be implemented by the builtin using a construct similar to the following:
func sleep(ctx context.Context, arg ucl.CallArgs) (any, error) {
var secs int
if err := arg.Bind(&secs); err != nil {
return err
}
// Save the execution stack
continuation := args.Continuation()
// Schedule the sleep callback
go func() {
<- time.After(secs * time.Seconds)
// Resume execution later, yielding `secs` as the return value
// of the `sleep` call. This will run the "ok, ready" echo call
continuation(ctx, secs)
})()
// Halt execution now
return nil, ucl.ErrHalt
}
The only trouble is, I’ve got no idea how I’m going to do this. As mentioned above, UCL executes the script by walking the parse tree with normal Go function calls. I don’t want to be in a position to create a snapshot of the Go call stack. That a little too low level for what I want to achieve here.
I suppose I could store the visited nodes in a list when the ErrHalt
is raised; or maybe replace the Go call stack with an in memory stack,
with AST node handlers being pushed and popped as the script runs. But
I’m not sure this will work either. It would require a significant
amount of reengineering, which I’m sure will be technically
interesting, but will take a fair bit of time. And how is this to work
if a continuation is made in a builtin that’s being called from another
builtin? What should happen if I were to run sleep
within a map
, for
example?
So it might be that I’ll have to use something else here. I could
potentially do something using Goroutines: the script is executed on
Goroutine and args.Continuation()
does something like pauses it on a
channel. How that would work with a builtin handler requesting the
continuation not being paused themselves I’m not so sure. Maybe the
handlers could be dispatched on a separate Goroutine as well?
A simpler approach might be to just offload this to the UCL user, and
have them run Eval
on a separate Goroutine and simply sleeping the
thread. Callbacks that need input from outside could simply be sent
using channels passed via the context.Context
. At least that’ll lean
into Go’s first party support for synchronisation, which is arguably a
good thing.
I’d be curious to know why Microsoft renamed Azure Active Directory to “Entra.” That name isβ¦ not good.
π¨βπ» New post on Databases over at the Coding Bits blog: Counting In DynamoDB
Day 30: hometown
It’s far from perfect, but it’s good to call this place home. #mbapr
After a two month hiatus, the coffee booth at the station has reopened. All is well in the world once more. βπ΅βπ«
Day 29: drift
Drifted a little from the path at this point of my hike today. Could be easy to get lost though, so it was important not to drift too far. #mbapr
πΊ Sugar: Season 1 (2024)
πΊ Mission: Impossible β Fallout (2018)
Day 28: community #mbapr
Manual Moreale hit the nail on the head with this quote from his latest post:
The web is not dying. The web is huge. The web is ever-expanding. The fact that the web is just the same 5 big websites is a fucking lie. Itβs like saying the restaurant industry is the same 5 fast food chains. It is not. Itβs up to you to decide to stop visiting those 5 sites and stop ingesting their fast food content.
It feels like when these people say βthe webβ, they mean βwhatever platform Iβm addicted to.β Might be time they started trying out that URL bar that appears at the top of every browser.
π¨βπ» New post on Go over at the Coding Bits blog: Custom Import Paths In Go
Day 27: surprise
Quite surprised that this photo came out as well as it did. Was purely an accidental press of the shutter button. #mbapr
Interesting to see Google starting to solicit reviews for apps that came with the phone, such as the⦠Phone.