Compare commits
15 Commits
c689cc58c4
...
f535bac942
Author | SHA1 | Date |
---|---|---|
|
f535bac942 | |
|
ae78412edd | |
|
e03e1712b1 | |
|
dacc7159d7 | |
|
021f70fd90 | |
|
f7aa184c20 | |
|
1dfd47708f | |
|
4d05bd7347 | |
|
22c29ce498 | |
|
1019e42fa7 | |
|
4170e3b78c | |
|
ad2f5c739f | |
|
05135edcbe | |
|
9566b790bc | |
|
67de839da8 |
|
@ -130,9 +130,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.5.0"
|
version = "0.5.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5611d4977882c5af1c0f7a34d51b5d87f784f86912bb543986b014ea4995ef93"
|
checksum = "f4af7447fc1214c1f3a1ace861d0216a6c8bb13965b64bbad9650f375b67689a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum-core",
|
"axum-core",
|
||||||
|
@ -161,9 +161,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-core"
|
name = "axum-core"
|
||||||
version = "0.2.0"
|
version = "0.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "95cd109b3e93c9541dcce5b0219dcf89169dcc58c1bebed65082808324258afb"
|
checksum = "3bdc19781b16e32f8a7200368a336fa4509d4b72ef15dd4e41df5290855ee1e6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -175,9 +175,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-extra"
|
name = "axum-extra"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff3819ded1be91d7ee2cd9f0466aa345cc70ba0b0035ed47e3eac6427f83b81a"
|
checksum = "a00a7085c512df12d4e07a862b23a3b3bfe5326ecfc4185b49fb15c8850ba406"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -655,9 +655,9 @@ checksum = "649cfd341410b1f8906e8ca1b39e5534be9312fda9182edd770cec34dfbce8d7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eyre"
|
name = "eyre"
|
||||||
version = "0.6.7"
|
version = "0.6.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9289ed2c0440a6536e65119725cf91fc2c6b5e513bfd2e36e1134d7cca6ca12f"
|
checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indenter",
|
"indenter",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
@ -929,9 +929,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.6"
|
version = "0.2.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03"
|
checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
|
@ -2363,9 +2363,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-http"
|
name = "tower-http"
|
||||||
version = "0.2.5"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aba3f3efabf7fb41fae8534fc20a817013dd1c12cb45441efb6c82e6556b4cd8"
|
checksum = "f7755f984aa46a9d50829dc84b6a674b2f3b8b4de47c0708923953b090d48acc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-compression",
|
"async-compression",
|
||||||
"base64",
|
"base64",
|
||||||
|
|
|
@ -11,7 +11,7 @@ repository = "https://github.com/Xe/site"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.5"
|
axum = "0.5"
|
||||||
axum-macros = "0.2"
|
axum-macros = "0.2"
|
||||||
axum-extra = "0.2"
|
axum-extra = "0.3"
|
||||||
color-eyre = "0.6"
|
color-eyre = "0.6"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
comrak = "0.12.1"
|
comrak = "0.12.1"
|
||||||
|
@ -55,7 +55,7 @@ version = "0.4"
|
||||||
features = [ "full" ]
|
features = [ "full" ]
|
||||||
|
|
||||||
[dependencies.tower-http]
|
[dependencies.tower-http]
|
||||||
version = "0.2"
|
version = "0.3"
|
||||||
features = [ "full" ]
|
features = [ "full" ]
|
||||||
|
|
||||||
# os-specific dependencies
|
# os-specific dependencies
|
||||||
|
|
|
@ -0,0 +1,565 @@
|
||||||
|
---
|
||||||
|
title: Crimes with Go Generics
|
||||||
|
date: 2022-04-24
|
||||||
|
tags:
|
||||||
|
- cursed
|
||||||
|
- golang
|
||||||
|
- generics
|
||||||
|
vod:
|
||||||
|
twitch: https://www.twitch.tv/videos/1465727432
|
||||||
|
youtube: https://youtu.be/UiJtaKYQnzg
|
||||||
|
---
|
||||||
|
|
||||||
|
Go 1.18 added [generics](https://go.dev/doc/tutorial/generics) to the
|
||||||
|
language. This allows you to have your types take types as parameters
|
||||||
|
so that you can create composite types (types out of types). This lets
|
||||||
|
you get a lot of expressivity and clarity about how you use Go.
|
||||||
|
|
||||||
|
However, if you are looking for good ideas on how to use Go generics,
|
||||||
|
this is not the post for you. This is full of bad ideas. This post is
|
||||||
|
full of ways that you should not use Go generics in production. Do not
|
||||||
|
copy the examples in this post into production. By reading this post
|
||||||
|
you agree to not copy the examples in this post into production.
|
||||||
|
|
||||||
|
I have put my code for this article [on my git
|
||||||
|
server](https://tulpa.dev/internal/gonads). This repo has been
|
||||||
|
intentionally designed to be difficult to use in production by me
|
||||||
|
taking the following steps:
|
||||||
|
|
||||||
|
1. I have created it under a Gitea organization named `internal`. This
|
||||||
|
will make it impossible for you to import the package unless you
|
||||||
|
are using it from a repo on my Gitea server. Signups are disabled
|
||||||
|
on that Gitea server. See
|
||||||
|
[here](https://go.dev/doc/go1.4#internalpackages) for more
|
||||||
|
information about the internal package rule.
|
||||||
|
1. The package documentation contains a magic comment that will make
|
||||||
|
Staticcheck and other linters complain that you are using this
|
||||||
|
package even though it is deprecated.
|
||||||
|
|
||||||
|
<xeblog-conv name="Mara" mood="hmm">What is that package
|
||||||
|
name?</xeblog-conv>
|
||||||
|
|
||||||
|
<xeblog-conv name="Cadey" mood="enby">It's a reference to
|
||||||
|
Haskell's monads, but adapted to Go as a pun.</xeblog-conv>
|
||||||
|
|
||||||
|
<xeblog-conv name="Numa" mood="delet">A gonad is just a gonoid in the
|
||||||
|
category of endgofunctors. What's there to be confused
|
||||||
|
about?</xeblog-conv>
|
||||||
|
|
||||||
|
<xeblog-conv name="Cadey" mood="facepalm">\*sigh\*</xeblog-conv>
|
||||||
|
|
||||||
|
## `Queue[T]`
|
||||||
|
|
||||||
|
To start things out, let's show off a problem in computer science that
|
||||||
|
is normally difficult. Let's make a MPMS (multiple producer, multiple
|
||||||
|
subscriber) queue.
|
||||||
|
|
||||||
|
First we are going to need a struct to wrap everything around. It will
|
||||||
|
look like this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Queue[T any] struct {
|
||||||
|
data chan T
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates a type named `Queue` that takes a type argument `T`. This
|
||||||
|
`T` can be absolutely anything, but the only requirement is that the
|
||||||
|
data is a Go type.
|
||||||
|
|
||||||
|
You can create a little constructor for `Queue` instances with a
|
||||||
|
function like this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func NewQueue[T any](size int) Queue[T] {
|
||||||
|
return Queue[T]{
|
||||||
|
data: make(chan T, size),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now let's make some methods on the `Queue` struct that will let us
|
||||||
|
push to the queue and pop from the queue. They could look like this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (q Queue[T]) Push(val T) {
|
||||||
|
q.data <- val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q Queue[T]) Pop() T {
|
||||||
|
return <-q.data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
These methods will let you put data at the end of the queue and then
|
||||||
|
pull it out from the beginning. You can use them like this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
q := NewQueue[string](5)
|
||||||
|
q.Push("hi there")
|
||||||
|
str := q.Pop()
|
||||||
|
if str != "hi there" {
|
||||||
|
panic("string is wrong")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is good, but the main problem comes from trying to pop from an
|
||||||
|
empty queue. It'll stay there forever doing nothing. We can use the
|
||||||
|
`select` statement to allow us to write a nonblocking version of the
|
||||||
|
`Pop` function:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (q Queue[T]) TryPop() (T, bool) {
|
||||||
|
select {
|
||||||
|
case val := <-q.data:
|
||||||
|
return val, true
|
||||||
|
default:
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
However when we try to compile this, we get an error:
|
||||||
|
|
||||||
|
```
|
||||||
|
cannot use nil as T value in return statement
|
||||||
|
```
|
||||||
|
|
||||||
|
In that code, `T` can be _anything_, including values that may not be
|
||||||
|
able to be `nil`. We can work around this by taking advantage of the
|
||||||
|
`var` statement, which makes a new variable and initializes it to the
|
||||||
|
zero value of that type:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Zero[T any]() T {
|
||||||
|
var zero T
|
||||||
|
return zero
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When we run the `Zero` function like
|
||||||
|
[this](https://go.dev/play/p/Z5tRs1-aKBU):
|
||||||
|
|
||||||
|
```go
|
||||||
|
log.Printf("%q", Zero[string]())
|
||||||
|
log.Printf("%v", Zero[int]())
|
||||||
|
```
|
||||||
|
|
||||||
|
We get output that looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
2009/11/10 23:00:00 ""
|
||||||
|
2009/11/10 23:00:00 0
|
||||||
|
```
|
||||||
|
|
||||||
|
So we can adapt the `default` branch of `TryPop` to this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (q Queue[T]) TryPop() (T, bool) {
|
||||||
|
select {
|
||||||
|
case val := <-q.data:
|
||||||
|
return val, true
|
||||||
|
default:
|
||||||
|
var zero T
|
||||||
|
return zero, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And finally write a test for good measure:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestQueue(t *testing.T) {
|
||||||
|
q := NewQueue[int](5)
|
||||||
|
for i := range make([]struct{}, 5) {
|
||||||
|
q.Push(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for range make([]struct{}, 5) {
|
||||||
|
t.Log(q.Pop())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `Option[T]`
|
||||||
|
|
||||||
|
In Go, people use pointer values for a number of reasons:
|
||||||
|
|
||||||
|
1. A pointer value may be `nil`, so this can signal that the value may
|
||||||
|
not exist.
|
||||||
|
1. A pointer value only stores the offset in memory, so passing around
|
||||||
|
the value causes Go to only copy the pointer instead of copying the
|
||||||
|
value being passed around.
|
||||||
|
1. A pointer value being passed to a function lets you mutate values
|
||||||
|
in the value being passed. Otherwise Go will copy the value and you
|
||||||
|
can mutate it all you want, but the changes you made will not
|
||||||
|
persist past that function call. You can sort of consider this to
|
||||||
|
be "immutable", but it's not as strict as something like passing
|
||||||
|
`&mut T` to functions in Rust.
|
||||||
|
|
||||||
|
This `Option[T]` type will help us model the first kind of constraint:
|
||||||
|
a value that may not exist. We can define it like this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Option[T any] struct {
|
||||||
|
val *T
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can define a couple methods to use this container:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var ErrOptionIsNone = errors.New("gonads: Option[T] has no value")
|
||||||
|
|
||||||
|
func (o Option[T]) Take() (T, error) {
|
||||||
|
if o.IsNone() {
|
||||||
|
var zero T
|
||||||
|
return zero, ErrOptionIsNone
|
||||||
|
}
|
||||||
|
|
||||||
|
return *o.val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Option[T]) Set(val T) {
|
||||||
|
o.val = &val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Option[T]) Clear() {
|
||||||
|
o.val = nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Some other functions that will be useful will be an `IsSome` function
|
||||||
|
to tell if the `Option` contains a value. We can use this to also
|
||||||
|
implement an `IsNone` function that will let you tell if that `Option`
|
||||||
|
_does not_ contain a value. They will look like this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (o Option[T]) IsSome() bool {
|
||||||
|
return o.val != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Option[T]) IsNone() bool {
|
||||||
|
return !o.IsSome()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We can say that if an Option does not have something in it, it has
|
||||||
|
nothing in it. This will let us use `IsSome` to implement `IsNone`.
|
||||||
|
|
||||||
|
Finally we can add all this up to a `Yank` function, which is similar
|
||||||
|
to
|
||||||
|
[`Option::unwrap()`](https://doc.rust-lang.org/rust-by-example/error/option_unwrap.html)
|
||||||
|
in Rust:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (o Option[T]) Yank() T {
|
||||||
|
if o.IsNone() {
|
||||||
|
panic("gonads: Yank on None Option")
|
||||||
|
}
|
||||||
|
|
||||||
|
return *o.val
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will all be verified in a Go test:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestOption(t *testing.T) {
|
||||||
|
o := NewOption[string]()
|
||||||
|
val, err := o.Take()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("[unexpected] wanted no value out of Option[T], got: %v", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Set("hello friendos")
|
||||||
|
_, err = o.Take()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("[unexpected] wanted no value out of Option[T], got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Clear()
|
||||||
|
if o.IsSome() {
|
||||||
|
t.Fatal("Option should have none, but has some")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<xeblog-conv name="Mara" mood="hmm">I think that
|
||||||
|
<code>Option[T]</code> will be the most useful outside of this post.
|
||||||
|
It will need some work and generalization, but this may be something
|
||||||
|
that the Go team will have to make instead of some random
|
||||||
|
person.</xeblog-conv>
|
||||||
|
|
||||||
|
## `Thunk[T]`
|
||||||
|
|
||||||
|
In computer science we usually deal with values and computations.
|
||||||
|
Usually we deal with one or the other. Sometimes computations can be
|
||||||
|
treated as values, but this is very rare. It's even more rare to take
|
||||||
|
a partially completed computation and use it as a value.
|
||||||
|
|
||||||
|
A thunk is a partially evaluated computation that is stored as a
|
||||||
|
value. For an idea of what I'm talking about, let's consider this
|
||||||
|
JavaScript function:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const add = (x, y) => x + y;
|
||||||
|
console.log(add(2, 2)); // 4
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates a function called `add` that takes two arguments and
|
||||||
|
returns one argument. This is great in many cases, but it makes it
|
||||||
|
difficult for us to bind only one argument to the function and leave
|
||||||
|
the other as a variable input. What if computing the left hand side of
|
||||||
|
`add` is expensive and only needed once?
|
||||||
|
|
||||||
|
Instead we can write `add` like this:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const add = (x) => (y) => x + y;
|
||||||
|
console.log(add(2)(2)); // 4
|
||||||
|
```
|
||||||
|
|
||||||
|
This also allows us to make partially evaluated forms of `add` like
|
||||||
|
`addTwo`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const addTwo = add(2);
|
||||||
|
console.log(addTwo(3)); // 5
|
||||||
|
```
|
||||||
|
|
||||||
|
This can also be used with functions that do not take arguments, so
|
||||||
|
you can pass around a value that isn't computed yet and then only
|
||||||
|
actually compute it when needed:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const hypotenuse = (x, y) => Math.sqrt(x * x + y * y);
|
||||||
|
const thunk = () => hypot(3, 4);
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then pass this thunk to functions _without having to evaluate
|
||||||
|
it_ until it is needed:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
dominateWorld(thunk); // thunk is passed as an unevaluated function
|
||||||
|
```
|
||||||
|
|
||||||
|
We can implement this in Go by using a type like the following:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Thunk[T any] struct {
|
||||||
|
doer func() T
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And then force the thunk to evaluate with a function such as `Force`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (t Thunk[T]) Force() T {
|
||||||
|
return t.doer()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This works, however we can also go one step further than we did with
|
||||||
|
the JavaScript example. We can take advantage of the `Thunk[T]`
|
||||||
|
container to cache the result of the `doer` function so that calling
|
||||||
|
it multiple times will only actually it once and return the same
|
||||||
|
result.
|
||||||
|
|
||||||
|
<xeblog-conv name="Mara" mood="hacker">Keep in mind that this will
|
||||||
|
only work for _pure functions_, or functions that don't modify the
|
||||||
|
outside world. This isn't just global variables either, but any
|
||||||
|
function that modifies any state anywhere, including network and
|
||||||
|
filesystem IO.</xeblog-conv>
|
||||||
|
|
||||||
|
This would make `Thunk[T]` be implemented like this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Thunk[T any] struct {
|
||||||
|
doer func() T // action being thunked
|
||||||
|
o *Option[T] // cache for complete thunk data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Thunk[T]) Force() T {
|
||||||
|
if t.o.IsSome() {
|
||||||
|
return t.o.Yank()
|
||||||
|
}
|
||||||
|
|
||||||
|
t.o.Set(t.doer())
|
||||||
|
return t.o.Yank()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewThunk[T any](doer func() T) *Thunk[T] {
|
||||||
|
return &Thunk[T]{
|
||||||
|
doer: doer,
|
||||||
|
o: NewOption[T](),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, for an overcomplicated example you can use this to implement the
|
||||||
|
Fibonacci function. We can start out by writing a naiive Fibonacci
|
||||||
|
function like this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Fib(n int) int {
|
||||||
|
if n <= 1 {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
return Fib(n-1) + Fib(n-2)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We can turn this into a Go test in order to see how long it takes for
|
||||||
|
it to work:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestRecurFib(t *testing.T) {
|
||||||
|
t.Log(Fib(40))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then when we run `go test`:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ go test -run RecurFib
|
||||||
|
=== RUN TestRecurFib
|
||||||
|
thunk_test.go:15: 102334155
|
||||||
|
--- PASS: TestRecurFib (0.36s)
|
||||||
|
```
|
||||||
|
|
||||||
|
However, we can make this a lot more complicated with the power of the
|
||||||
|
`Thunk[T]` type:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestThunkFib(t *testing.T) {
|
||||||
|
cache := make([]*Thunk[int], 41)
|
||||||
|
|
||||||
|
var fib func(int) int
|
||||||
|
fib = func(n int) int {
|
||||||
|
if cache[n].o.IsSome() {
|
||||||
|
return *cache[n].o.val
|
||||||
|
}
|
||||||
|
return fib(n-1) + fib(n-2)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range cache {
|
||||||
|
i := i
|
||||||
|
cache[i] = NewThunk(func() int { return fib(i) })
|
||||||
|
}
|
||||||
|
cache[0].o.Set(0)
|
||||||
|
cache[1].o.Set(1)
|
||||||
|
|
||||||
|
t.Log(cache[40].Force())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And then run the test:
|
||||||
|
|
||||||
|
```
|
||||||
|
=== RUN TestThunkFib
|
||||||
|
thunk_test.go:36: 102334155
|
||||||
|
--- PASS: TestThunkFib (0.60s)
|
||||||
|
```
|
||||||
|
|
||||||
|
<xeblog-conv name="Mara" mood="hmm">Why is this so much slower? This
|
||||||
|
should be caching the intermediate values. Maybe something like this
|
||||||
|
would be faster? This should complete near instantly,
|
||||||
|
right?</xeblog-conv>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestMemoizedFib(t *testing.T) {
|
||||||
|
mem := map[int]int{
|
||||||
|
0: 0,
|
||||||
|
1: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
var fib func(int) int
|
||||||
|
fib = func(n int) int {
|
||||||
|
if result, ok := mem[n]; ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
result := fib(n-1) + fib(n-2)
|
||||||
|
mem[n] = result
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log(fib(40))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ go test -run Memoized
|
||||||
|
=== RUN TestMemoizedFib
|
||||||
|
thunk_test.go:35: 102334155
|
||||||
|
--- PASS: TestMemoizedFib (0.00s)
|
||||||
|
```
|
||||||
|
|
||||||
|
<xeblog-conv name="Cadey" mood="enby">I'm not sure
|
||||||
|
either.</xeblog-conv>
|
||||||
|
|
||||||
|
If you change the `fib` function to this, it works, but it also steps
|
||||||
|
around the `Thunk[T]` type:
|
||||||
|
|
||||||
|
```go
|
||||||
|
fib = func(n int) int {
|
||||||
|
if cache[n].o.IsSome() {
|
||||||
|
return *cache[n].o.val
|
||||||
|
}
|
||||||
|
|
||||||
|
result := fib(n-1) + fib(n-2)
|
||||||
|
cache[n].o.Set(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This completes instantly:
|
||||||
|
|
||||||
|
```
|
||||||
|
=== RUN TestThunkFib
|
||||||
|
thunk_test.go:59: 102334155
|
||||||
|
--- PASS: TestThunkFib (0.00s)
|
||||||
|
```
|
||||||
|
|
||||||
|
To be clear, this isn't the fault of Go generics. I'm almost certain
|
||||||
|
that my terrible code is causing this to be much slower.
|
||||||
|
|
||||||
|
<xeblog-conv name="Numa" mood="delet">This is the power of gonads:
|
||||||
|
making easy code complicated, harder to reason about and slower than
|
||||||
|
the naiive approach! Why see this as terrible code when it creates an
|
||||||
|
amazing opportunity for cloud providers to suggest that people use
|
||||||
|
gonads' `Thunk[T]` so that they use more CPU and then have to pay cloud
|
||||||
|
providers more money for CPU! Think about the children!</xeblog-conv>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<xeblog-conv name="Cadey" mood="coffee">EDIT(2022 M04 25 05:56): amscanne on
|
||||||
|
Hacker News pointed out that my code was in fact wrong. My `fib` function should
|
||||||
|
have been a lot simpler.</xeblog-conv>
|
||||||
|
|
||||||
|
```go
|
||||||
|
fib = func(n int) int {
|
||||||
|
return cache[n-1].Force() + cache[n-2].Force()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<xeblog-conv name="Cadey" mood="facepalm">Applying this also makes the code run
|
||||||
|
instantly as I'd expect. I knew _something_ was _very wrong_, but I never
|
||||||
|
expected something this stupid. Thanks amscanne!</xeblog-conv>
|
||||||
|
|
||||||
|
<xeblog-conv name="Numa" mood="happy">Hey, it makes for good surrealism. If that
|
||||||
|
isn't a success, what is?</xeblog-conv>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
I'm glad that Go has added generics to the language. It's certainly
|
||||||
|
going to make a lot of things a lot easier and more expressive. I'm
|
||||||
|
worried that the process of learning how to use generics in Go is
|
||||||
|
going to create a lot of churn and toil as people get up to speed on
|
||||||
|
when and where they should be used. These should be used in specific
|
||||||
|
cases, not as a bread and butter tool.
|
||||||
|
|
||||||
|
I hope this was an interesting look into how you can use generics in
|
||||||
|
Go, but again please don't use these examples in production.
|
|
@ -75,6 +75,9 @@ nix = {
|
||||||
|
|
||||||
Then rebuild your system and you can continue along with the article.
|
Then rebuild your system and you can continue along with the article.
|
||||||
|
|
||||||
|
<xeblog-conv name="Mara" mood="hacker">EDIT: You can use WSL for this. See
|
||||||
|
[here](/blog/nix-flakes-4-wsl-2022-05-01) for more information.</xeblog-conv>
|
||||||
|
|
||||||
If you are not on NixOS, you will need to either edit `~/.config/nix/nix.conf`
|
If you are not on NixOS, you will need to either edit `~/.config/nix/nix.conf`
|
||||||
or `/etc/nix/nix.conf` and add the following line to it:
|
or `/etc/nix/nix.conf` and add the following line to it:
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,334 @@
|
||||||
|
---
|
||||||
|
title: "Nix Flakes on WSL"
|
||||||
|
date: 2022-05-04
|
||||||
|
series: nix-flakes
|
||||||
|
tags:
|
||||||
|
- nixos
|
||||||
|
- wsl
|
||||||
|
vod:
|
||||||
|
youtube: https://youtu.be/VzQ_NwFJObc
|
||||||
|
twitch: https://www.twitch.tv/videos/1464781566
|
||||||
|
---
|
||||||
|
|
||||||
|
About five years ago, Microsoft released the Windows Subsystem for Linux
|
||||||
|
([WSL](https://docs.microsoft.com/en-us/windows/wsl/)). This allows you to run
|
||||||
|
Linux programs on a Windows machine. When they released WSL version 2 in 2019,
|
||||||
|
this added support for things like Docker and systemd. As a result, this is
|
||||||
|
enough to run NixOS on Windows.
|
||||||
|
|
||||||
|
<xeblog-conv name="Mara" mood="hacker">This will give you an environment to run
|
||||||
|
Nix and Nix Flakes commands with. You can use this to follow along with this
|
||||||
|
series without having to install NixOS on a VM or cloud server. This is going to
|
||||||
|
retread a bunch of ground from the first article. If you have been following
|
||||||
|
along through this entire series, once you get to the point where you convert
|
||||||
|
the install to flakes there isn't much more new material here.
|
||||||
|
</xeblog-conv>
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Head to the NixOS-WSL [releases
|
||||||
|
page](https://github.com/nix-community/NixOS-WSL/releases/) and download the
|
||||||
|
`nixos-wsl-installer-fixed.tar.gz` file to your Downloads folder.
|
||||||
|
|
||||||
|
Then open Powershell and make a folder called `WSL`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
New-Item -Path .\WSL -ItemType Directory
|
||||||
|
```
|
||||||
|
|
||||||
|
<xeblog-conv name="Mara" mood="hacker">It's worth noting that Powershell does
|
||||||
|
have a bunch of aliases for common coreutils commands to the appropriate
|
||||||
|
Powershell CMDlets. However these aliases are <b>NOT</b> flag-compatible and use
|
||||||
|
the Powershell semantics instead of the semantics of the command it is aliasing.
|
||||||
|
This will bite you when you use commands like <code>wget</code> out of instinct
|
||||||
|
to download things. In order to avoid your muscle memory betraying you, the
|
||||||
|
Powershell CMDlets are shown here in their full overly verbose glory.
|
||||||
|
</xeblog-conv>
|
||||||
|
|
||||||
|
Then enter the directory with `Set-Location`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Location -Path .\WSL
|
||||||
|
```
|
||||||
|
|
||||||
|
<xeblog-conv name="Mara" mood="hacker">This directory is where the NixOS root
|
||||||
|
filesystem will live. If you want to put this somewhere else, feel free to.
|
||||||
|
Somewhere in `%APPDATA%` will work, just as long as it's on an NTFS volume
|
||||||
|
somewhere.
|
||||||
|
</xeblog-conv>
|
||||||
|
|
||||||
|
Make a folder for the NixOS filesystem:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
New-Item -Path .\NixOS -ItemType Directory
|
||||||
|
```
|
||||||
|
|
||||||
|
Then install the NixOS root image with the `wsl` command:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
wsl --import NixOS .\NixOS\ ..\Downloads\nixos-wsl-installer-fixed.tar.gz --version 2
|
||||||
|
```
|
||||||
|
|
||||||
|
And start NixOS once to have it install itself:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
wsl -d NixOS
|
||||||
|
```
|
||||||
|
|
||||||
|
Once that finishes, press control-D (or use the `exit` command) to exit out of
|
||||||
|
NixOS and restart the WSL virtual machine:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
exit
|
||||||
|
wsl --shutdown
|
||||||
|
wsl -d NixOS
|
||||||
|
```
|
||||||
|
|
||||||
|
And then you have yourself a working NixOS environment! It's very barebones, but
|
||||||
|
we can use it to test the `nix run` command against our gohello command:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ nix run github:Xe/gohello
|
||||||
|
Hello reader!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local Services
|
||||||
|
|
||||||
|
We can also use this NixOS environment to run a local nginx server. Open
|
||||||
|
`/etc/nixos/configuration.nix`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{ lib, pkgs, config, modulesPath, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
nixos-wsl = import ./nixos-wsl;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
"${modulesPath}/profiles/minimal.nix"
|
||||||
|
|
||||||
|
nixos-wsl.nixosModules.wsl
|
||||||
|
];
|
||||||
|
|
||||||
|
wsl = {
|
||||||
|
enable = true;
|
||||||
|
automountPath = "/mnt";
|
||||||
|
defaultUser = "nixos";
|
||||||
|
startMenuLaunchers = true;
|
||||||
|
|
||||||
|
# Enable integration with Docker Desktop (needs to be installed)
|
||||||
|
# docker.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Enable nix flakes
|
||||||
|
nix.package = pkgs.nixFlakes;
|
||||||
|
nix.extraOptions = ''
|
||||||
|
experimental-features = nix-command flakes
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Right after the `wsl` block, add this nginx configuration to the file:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.nginx.enable = true;
|
||||||
|
services.nginx.virtualHosts."test.local.cetacean.club" = {
|
||||||
|
root = "/srv/http/test.local.cetacean.club";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create an nginx configuration that points the domain
|
||||||
|
`test.local.cetacean.club` to the contents of the folder `/srv/http/test.local.cetacean.club`.
|
||||||
|
|
||||||
|
<xeblog-conv name="Mara" mood="hacker">The <code>/srv</code> folder is set aside
|
||||||
|
for site-specific data, which is code for "do whatever you want with this
|
||||||
|
folder". In many cases people make a separate <code>/srv/http</code> folder and
|
||||||
|
put each static subdomain in its own folder under that, however I am also told
|
||||||
|
that it is idiomatic to put stuff in <code>/var/www</code>. Pick your poison.
|
||||||
|
</xeblog-conv>
|
||||||
|
|
||||||
|
Then you can test the web server with the `curl` command:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ curl http://test.local.cetacean.club
|
||||||
|
<html>
|
||||||
|
<head><title>404 Not Found</title></head>
|
||||||
|
<body>
|
||||||
|
<center><h1>404 Not Found</h1></center>
|
||||||
|
<hr><center>nginx</center>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
This is good! Nginx is running and since we haven't created the folder with our
|
||||||
|
website content yet, this 404 means that it can't find it! Let's create the
|
||||||
|
folder so that nginx has permission to it and we can modify things in it:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo mkdir -p /srv/http/test.local.cetacean.club
|
||||||
|
sudo chown nixos:nginx /srv/http/test.local.cetacean.club
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally we can make an amazing website. Open
|
||||||
|
`/srv/http/test.local.cetacean.club/index.html` in nano:
|
||||||
|
|
||||||
|
```
|
||||||
|
nano /srv/http/test.local.cetacean.club/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
And paste in this HTML:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<title>amazing website xD</title>
|
||||||
|
<h1>look at my AMAZING WEBSITE</h1>
|
||||||
|
It's so cool *twerks*
|
||||||
|
```
|
||||||
|
|
||||||
|
<xeblog-conv name="Mara" mood="hacker">This doesn't have to just be artisanal
|
||||||
|
handcrafted HTML in bespoke folders either. You can set the <code>root</code> of
|
||||||
|
a nginx virtual host to point to a Nix package as well. This will allow you to
|
||||||
|
automatically generate your website somehow and deploy it with the rest of the
|
||||||
|
system. Including being able to roll back changes.</xeblog-conv>
|
||||||
|
|
||||||
|
And then you can see it show up with `curl`:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ curl http://test.local.cetacean.club
|
||||||
|
<title>amazing website xD</title>
|
||||||
|
<h1>look at my AMAZING WEBSITE</h1>
|
||||||
|
It's so cool *twerks*
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also check this out in a browser by clicking
|
||||||
|
[here](http://test.local.cetacean.club):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Installing `gohello`
|
||||||
|
|
||||||
|
To install the `gohello` service, first we will need to convert this machine to
|
||||||
|
use NixOS flakes. We can do that really quick and easy by adding this file to
|
||||||
|
`/etc/nixos/flake.nix`:
|
||||||
|
|
||||||
|
<xeblog-conv name="Mara" mood="happy">Do this as root!</xeblog-conv>
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, ... }: {
|
||||||
|
nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [
|
||||||
|
./configuration.nix
|
||||||
|
|
||||||
|
# add things here
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run `nix flake check` to make sure everything is okay:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo nix flake check /etc/nixos
|
||||||
|
```
|
||||||
|
|
||||||
|
And finally activate the new configuration with flakes:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo nixos-rebuild switch
|
||||||
|
```
|
||||||
|
|
||||||
|
<xeblog-conv name="Mara" mood="hmm">Why don't you have the <code>--flake</code>
|
||||||
|
flag here? Based on what I read in the documentation, I thought you had to have
|
||||||
|
it there.</xeblog-conv>
|
||||||
|
|
||||||
|
<xeblog-conv name="Cadey" mood="enby"><code>nixos-rebuild</code> will
|
||||||
|
auomatically detect flakes in <code>/etc/nixos</code>. The only major thing it
|
||||||
|
cares about is the hostname matching. If you want to customize the hostname of
|
||||||
|
the WSL VM, change the <code>nixos</code> in
|
||||||
|
<code>nixosConfigurations.nixos</code> above and set
|
||||||
|
<code>networking.hostName</code> to the value you want to use. To use flakes
|
||||||
|
explicitly, pass <code>--flake /etc/nixos#hostname</code> to your
|
||||||
|
<code>nixos-rebuild</code> call.
|
||||||
|
</xeblog-conv>
|
||||||
|
|
||||||
|
After it thinks for a bit, you should notice that nothing happened. This is
|
||||||
|
good, we have just converted the system over to using Nix flakes instead of the
|
||||||
|
classic `nix-channel` rebuild method.
|
||||||
|
|
||||||
|
To get `gohello` in the system, first we need to add `git` to the commands
|
||||||
|
available on the system in `configuration.nix`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
environment.systemPackages = with pkgs; [ git ];
|
||||||
|
```
|
||||||
|
|
||||||
|
Then we can add `gohello` to our system flake:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||||
|
# XXX(Xe): this URL may change for you, such as github:Xe/gohello-http
|
||||||
|
gohello.url = "git+https://tulpa.dev/cadey/gohello-http?ref=main";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, gohello, ... }: {
|
||||||
|
nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [
|
||||||
|
./configuration.nix
|
||||||
|
|
||||||
|
# add things here
|
||||||
|
gohello.nixosModule
|
||||||
|
({ pkgs, ... }: {
|
||||||
|
xeserv.services.gohello.enable = true;
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And rebuild the system with `gohello` enabled:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo nixos-rebuild switch
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, poke it with `curl`:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ curl http://gohello.local.cetacean.club
|
||||||
|
hello world :)
|
||||||
|
```
|
||||||
|
|
||||||
|
To update it, update the flake inputs in `/etc/nixos` and run `nixos-rebuild`:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo nix flake update /etc/nixos
|
||||||
|
sudo nixos-rebuild switch
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
And from here you can do whatever you want with NixOS. You can use
|
||||||
|
[containers](https://nixos.org/manual/nixos/stable/#ch-containers), set up
|
||||||
|
arbitrary services, or plan for world domination as normal.
|
||||||
|
|
||||||
|
<xeblog-conv name="Numa" mood="delet">I thought it was "to save the world from
|
||||||
|
devastation", not "to plan for world domination". Who needs a monopoly on
|
||||||
|
violence for world domination when you have Nix expressions?</xeblog-conv>
|
||||||
|
|
||||||
|
<xeblog-conv name="Cadey" mood="coffee">Siiiiiiiiiiiiiiiiiigh.</xeblog-conv>
|
||||||
|
|
||||||
|
I will use this setup in future posts to make this more accessible and easy to
|
||||||
|
hack at without having to have a dedicated NixOS machine laying around.
|
|
@ -0,0 +1,474 @@
|
||||||
|
---
|
||||||
|
title: "robocadey: Shitposting as a Service"
|
||||||
|
date: 2022-04-30
|
||||||
|
tags:
|
||||||
|
- gpt2
|
||||||
|
- machinelearning
|
||||||
|
- python
|
||||||
|
- golang
|
||||||
|
- art
|
||||||
|
vod:
|
||||||
|
twitch: https://www.twitch.tv/videos/1471211336
|
||||||
|
youtube: https://youtu.be/UAd-mWMG198
|
||||||
|
---
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
|
||||||
|
[Hey, you need to enable JavaScript for most of the embedded posts in this
|
||||||
|
article to work. Sorry about this, we are working on a better solution, but this
|
||||||
|
is what we have right now.](conversation://Mara/hacker)
|
||||||
|
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
What is art? Art is when you challenge the assumptions that people make about a
|
||||||
|
medium and use that conflict to help them change what they think about that
|
||||||
|
medium. Let's take "Comedian" by Maurizio Cattelan for example:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
By my arbitrary definition above, this is art. This takes assumptions that you
|
||||||
|
have about paintings (you know, that they use paint on the canvas) and discards
|
||||||
|
them. This lets you change what you think art is. Art is not about the medium or
|
||||||
|
the things in it. Art is the expression of these things in new and exiting ways.
|
||||||
|
|
||||||
|
<xeblog-conv name="Cadey" mood="coffee">Originally I was going to use some
|
||||||
|
Banksky art here, but for understandable reasons it's quite difficult to get
|
||||||
|
images of Banksky art.</xeblog-conv>
|
||||||
|
|
||||||
|
One of my favorite kinds of art is the "uncanny valley" of realism. Let's take
|
||||||
|
Death Stranding as an example of this. Death Stranding is a video game that was
|
||||||
|
released in 2019 for the PlayStation 4 and is one of my favorite games of all
|
||||||
|
time. The game has a very hyper-realistic art style that is firmly in the
|
||||||
|
centre of the uncanny valley:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This game mixes very realistic scenery with a story about dead bodies turning
|
||||||
|
into antimatter and you being a UPS delivery person that saves America. This is
|
||||||
|
art to me. This transformed what a video game could be, even if the entire game
|
||||||
|
boils down to Kojima themed fetch quests. Oh and trying not to die even though
|
||||||
|
you can't die but when you die it's really bad.
|
||||||
|
|
||||||
|
I want to create this kind of art, and I think I have found a good medium to do
|
||||||
|
this with. I write a lot on this little independent site called Twitter. This is
|
||||||
|
one of the main things that I write on, and through the process of the last 8
|
||||||
|
years or so, I've written a shockingly large amount of things. I post a lot of
|
||||||
|
weird things there as well as a lot of boring/normal things.
|
||||||
|
|
||||||
|
However a lot of my posts boil down to creating a "stream of consciousness", or
|
||||||
|
using it as a way to help deal with intrusive thoughts. There's a certain art to
|
||||||
|
this, as it is a candid exchange between the author and the reader. The reader
|
||||||
|
doesn't get all the context (heck, I doubt that I have all the context lol), but
|
||||||
|
from there they get to put the pieces together.
|
||||||
|
|
||||||
|
So, when thinking about trying to get into the uncanny valley with this kind of
|
||||||
|
art medium, my mind goes back to the old days on IRC channels. Many IRC channels
|
||||||
|
run bots to help them run the channel or purely for amusement. One of my
|
||||||
|
favorite kinds of bots is a [Markov
|
||||||
|
chain](https://en.wikipedia.org/wiki/Markov_chain) bot. These kinds of bots
|
||||||
|
learn patterns in text and then try to repeat them at random. With enough
|
||||||
|
training data, it can be fairly convincing at first glance. However, you need _a
|
||||||
|
lot_ of training data to get there. More training data than I have ever tweeted.
|
||||||
|
|
||||||
|
This ends up creating a situation where the markov bot is right in the uncanny
|
||||||
|
valley of realism. At first glance it is something that isn't not plausibly
|
||||||
|
human. It looks like a bot, but it also looks like a human, but it also looks
|
||||||
|
like a bot. It appears to be in the middle. I like this from an artistic
|
||||||
|
standpoint because this challenges your assumptions that bots need to be
|
||||||
|
obviously bots and humans need to be obviously human.
|
||||||
|
|
||||||
|
In the past I have ran a service I call `cadeybot`. It took all of my Discord
|
||||||
|
messages, fed them into a Markov chain, and then attempted to create new
|
||||||
|
messages as a result. This worked pretty well, but we ran into an issue where it
|
||||||
|
would basically regurgitate its training data. So when people thought it was
|
||||||
|
being novel about roasting people, someone would search the chat and find out
|
||||||
|
that I said those exact words 2 years ago.
|
||||||
|
|
||||||
|
This isn't really exciting from an artistic point of view. You could get the
|
||||||
|
same result from randomly replying with old chat messages without any additional
|
||||||
|
data in the mix.
|
||||||
|
|
||||||
|
I haven't run `cadeybot` in some time because of this. It gets really boring
|
||||||
|
really fast.
|
||||||
|
|
||||||
|
However, I was looking at some DALL-E generated images and then inspiration
|
||||||
|
struck:
|
||||||
|
|
||||||
|
<xeblog-conv name="Mara" mood="hmm">What if I fed all those tweets into
|
||||||
|
[GPT-2](https://en.wikipedia.org/wiki/GPT-2)?</xeblog-conv>
|
||||||
|
|
||||||
|
So I did that. I made [@robocadey@botsin.space](https://botsin.space/@robocadey)
|
||||||
|
as a fediverse bot that generates new content based on everything I've ever
|
||||||
|
tweeted.
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108219835651549836/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="500"
|
||||||
|
height="245" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
## Data
|
||||||
|
|
||||||
|
The first step of this is getting all of my tweet data out of Twitter. This
|
||||||
|
was a lot easier than I thought. All I had to do was submit a GDPR data request,
|
||||||
|
wait a few days for the cloud to think and then I got a 3 gigabyte zip file full
|
||||||
|
of everything I've ever tweeted. Cool!
|
||||||
|
|
||||||
|
Looking through the dump, I found a 45 megabyte file called `tweets.js`. This
|
||||||
|
looked like it could be important! So I grabbed it and looked at the first few
|
||||||
|
lines:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
$ head tweet.js
|
||||||
|
window.YTD.tweet.part0 = [
|
||||||
|
{
|
||||||
|
"tweet" : {
|
||||||
|
"retweeted" : false,
|
||||||
|
"source" : "<a href=\"http://www.bitlbee.org/\" rel=\"nofollow\">BitlBee</a>",
|
||||||
|
"entities" : {
|
||||||
|
"hashtags" : [ ],
|
||||||
|
"symbols" : [ ],
|
||||||
|
"user_mentions" : [
|
||||||
|
{
|
||||||
|
```
|
||||||
|
|
||||||
|
So it looks like most of this is really just a giant block of data that's
|
||||||
|
stuffed into JavaScript so that the embedded HTML can show off everything you've
|
||||||
|
ever tweeted. Neat, but I only need the tweet contents. We can strip off the
|
||||||
|
preamble with `sed`, and then grab the first entry out of `tweets.js` with a
|
||||||
|
command like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
$ cat tweet.js | sed 's/window.YTD.tweet.part0 = //' | jq .[0]
|
||||||
|
{
|
||||||
|
"tweet": {
|
||||||
|
"retweeted": false,
|
||||||
|
"source": "<a href=\"http://www.bitlbee.org/\" rel=\"nofollow\">BitlBee</a>",
|
||||||
|
"entities": {
|
||||||
|
"hashtags": [],
|
||||||
|
"symbols": [],
|
||||||
|
"user_mentions": [
|
||||||
|
{
|
||||||
|
"name": "@Lyude@queer.party🌹",
|
||||||
|
"screen_name": "_Lyude",
|
||||||
|
"indices": [
|
||||||
|
"0",
|
||||||
|
"7"
|
||||||
|
],
|
||||||
|
"id_str": "1568160860",
|
||||||
|
"id": "1568160860"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"urls": []
|
||||||
|
},
|
||||||
|
"display_text_range": [
|
||||||
|
"0",
|
||||||
|
"83"
|
||||||
|
],
|
||||||
|
"favorite_count": "0",
|
||||||
|
"in_reply_to_status_id_str": "481634023295709185",
|
||||||
|
"id_str": "481634194729488386",
|
||||||
|
"in_reply_to_user_id": "1568160860",
|
||||||
|
"truncated": false,
|
||||||
|
"retweet_count": "0",
|
||||||
|
"id": "481634194729488386",
|
||||||
|
"in_reply_to_status_id": "481634023295709185",
|
||||||
|
"created_at": "Wed Jun 25 03:05:15 +0000 2014",
|
||||||
|
"favorited": false,
|
||||||
|
"full_text": "@_Lyude but how many licks does it take to get to the centre of a tootsie roll pop?",
|
||||||
|
"lang": "en",
|
||||||
|
"in_reply_to_screen_name": "_Lyude",
|
||||||
|
"in_reply_to_user_id_str": "1568160860"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It looks like most of what I want is in `.tweet.full_text`, so let's make a
|
||||||
|
giant text file with everything in it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sed 's/window.YTD.tweet.part0 = //' < tweets.js \
|
||||||
|
| jq '.[] | [ select(.tweet.retweeted == false) ] | .[].tweet.full_text' \
|
||||||
|
| sed -r 's/\s*\.?@[A-Za-z0-9_]+\s*//g' \
|
||||||
|
| grep -v 'RT:' \
|
||||||
|
| jq --slurp . \
|
||||||
|
| jq -r .[] \
|
||||||
|
| sed -e 's!http[s]\?://\S*!!g' \
|
||||||
|
| sed '/^$/d' \
|
||||||
|
> tweets.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
This does a few things:
|
||||||
|
|
||||||
|
1. Removes that twitter preamble so jq is happy
|
||||||
|
2. Removes all at-mentions from the training data (so the bot doesn't go on a
|
||||||
|
mentioning massacre)
|
||||||
|
3. Removes the "retweet" prefixed tweets from the dataset
|
||||||
|
4. Removes all urls
|
||||||
|
5. Removes all blank lines
|
||||||
|
|
||||||
|
This should hopefully cut out all the irrelevant extra crap and let the machine
|
||||||
|
learning focus on my text, which is what I actually care about.
|
||||||
|
|
||||||
|
## Getting It Up
|
||||||
|
|
||||||
|
As a prototype, I fed this all into Markov chains. This is boring, but I was
|
||||||
|
able to graft together a few projects to get that prototype up quickly. After
|
||||||
|
some testing, I ended up with things like this:
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108201675365283068/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="500"
|
||||||
|
height="225" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
This was probably the best thing to come out of the Markov chain testing phase,
|
||||||
|
the rest of it was regurgitating old tweets.
|
||||||
|
|
||||||
|
While I was doing this, I got GPT-2 training thanks to [this iPython
|
||||||
|
notebook](https://colab.research.google.com/github/sarthakmalik/GPT2.Training.Google.Colaboratory/blob/master/Train_a_GPT_2_Text_Generating_Model_w_GPU.ipynb).
|
||||||
|
I uploaded my 1.5 megabyte tweets.txt file and let the big pile of linear
|
||||||
|
algebra mix around for a bit.
|
||||||
|
|
||||||
|
Once it was done, I got a one gigabyte tarball that I extracted into a new
|
||||||
|
folder imaginatively named `gpt2`. Now I had the model, all I needed to do was
|
||||||
|
run it. So I wrote some Python:
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import gpt_2_simple as gpt2
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
sockpath = "/xe/gpt2/checkpoint/server.sock"
|
||||||
|
|
||||||
|
sess = gpt2.start_tf_sess()
|
||||||
|
gpt2.load_gpt2(sess, run_name='run1')
|
||||||
|
|
||||||
|
if os.path.exists(sockpath):
|
||||||
|
os.remove(sockpath)
|
||||||
|
|
||||||
|
sock = socket.socket(socket.AF_UNIX)
|
||||||
|
sock.bind(sockpath)
|
||||||
|
|
||||||
|
print("Listening on", sockpath)
|
||||||
|
sock.listen(1)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
connection, client_address = sock.accept()
|
||||||
|
try:
|
||||||
|
print("generating shitpost")
|
||||||
|
result = gpt2.generate(sess,
|
||||||
|
length=512,
|
||||||
|
temperature=0.8,
|
||||||
|
nsamples=1,
|
||||||
|
batch_size=1,
|
||||||
|
return_as_list=True,
|
||||||
|
top_p=0.9,
|
||||||
|
)[0].split("\n")[1:][:-1]
|
||||||
|
print("shitpost generated")
|
||||||
|
connection.send(json.dumps(result).encode())
|
||||||
|
finally:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
server.close()
|
||||||
|
os.remove("/xe/gpt2/checkpoint/server.sock")
|
||||||
|
```
|
||||||
|
|
||||||
|
And I used a Dockerfile to set up its environment:
|
||||||
|
|
||||||
|
```Dockerfile
|
||||||
|
FROM python:3
|
||||||
|
RUN pip3 install gpt-2-simple
|
||||||
|
WORKDIR /xe/gpt2
|
||||||
|
COPY . .
|
||||||
|
CMD python3 main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Then I bind-mounted the premade model into the container and asked it to think
|
||||||
|
up something for me. I got back a list of replies and then I knew it was good to
|
||||||
|
go:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"oh dear. I don't know if you're the best mannered technologist you've come to expect from such a unique perspective. On the technical side of things, you're a world-class advocate for open source who recently lost an argument over the state of the open source world to bitter enemies like Python.",
|
||||||
|
"I also like your approach to DNS! One step at a time. More info here: ",
|
||||||
|
"tl;dr: it's a bunch of random IP addresses and the outcome is a JSON file that you fill out in as you go.",
|
||||||
|
"datasoftware.reddit.com/r/programmingcirclejerk-memes",
|
||||||
|
"datasoftware.reddit.com/r/programmingcirclejerk-memes",
|
||||||
|
"datasoftware.reddit.com/r/programmingcirclejerk-memes",
|
||||||
|
"datasoftware.reddit.com/r/programmingcirclejerk-memes",
|
||||||
|
"Oh dear, can we third-person?",
|
||||||
|
"A group of us is a CVE-1918 impact statement",
|
||||||
|
"Is that breaking news?",
|
||||||
|
"Lol datasom shitposting omg ",
|
||||||
|
"I'm gonna be on the list for #Giving is easy, don't look so far ahead ",
|
||||||
|
"Oh dear. Welcome to ThePandora: ",
|
||||||
|
"I use a lot of shift lol",
|
||||||
|
"I thought you were an orca",
|
||||||
|
"Foone, my old computer crashed. What happened to your hard drive? ",
|
||||||
|
"Yeah I know some of those things should be automated, but this is about experimentation and experimentation is what makes me happy",
|
||||||
|
"Am I? ",
|
||||||
|
"Experiment is my favorite part of the article",
|
||||||
|
"Yes I can, scroll past the how to read words videos",
|
||||||
|
"I was able to see into space but I cannot seen into your eyes",
|
||||||
|
"This is with a virtual keyboard/MAC address field",
|
||||||
|
"Yes but with the keymap \"~M\"",
|
||||||
|
"Yes this is a structural change, I am trying to tease things out a bit. I am trying to make it slightly different sounding with the key mapping. I am trying to make it different sounding sounding.",
|
||||||
|
"The main thing I am trying to do is make it easy to type backwards. This is going to take experimentation. I am trying to make it slightly different sounding.",
|
||||||
|
"Is this vehicle of mercy?",
|
||||||
|
"God i forgot "
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
However, this involved using Docker. Docker is decent, but if I have the ability
|
||||||
|
not to, I don't want to use Docker. A friend of mine named `ckie` saw that I was
|
||||||
|
using Docker for this and decided to package the `gpt_2_simple` library [into
|
||||||
|
nixpkgs](https://github.com/NixOS/nixpkgs/pull/170713). They also made it easy
|
||||||
|
for me to pull it into robocadey's environment and then I ripped out Docker,
|
||||||
|
never to return.
|
||||||
|
|
||||||
|
Now the bot could fly. Here was the first thing it posted after it got online
|
||||||
|
with GPT-2 in a proper way:
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108209326706890695/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="500"
|
||||||
|
height="175" height=allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
I can't make this up.
|
||||||
|
|
||||||
|
## Art Gallery
|
||||||
|
|
||||||
|
Here are some of my favorite posts it's made. Most of them could pass off as my
|
||||||
|
tweets.
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108209924883002812/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="400"
|
||||||
|
height="190" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108212424672000652/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="400"
|
||||||
|
height="190" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108215827551779879/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="400"
|
||||||
|
height="210" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108218889999336372/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="400"
|
||||||
|
height="210" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108218894030986305/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="800"
|
||||||
|
height="250" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
Some of them get somber and are unintentionally a reflection on the state of the
|
||||||
|
world we find ourselves in.
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108219835651549836/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="400"
|
||||||
|
height="280" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108218522810351900/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="400"
|
||||||
|
height="280" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108217161432474717/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="400"
|
||||||
|
height="345" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108216170547691864/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="400"
|
||||||
|
height="280" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
Others are silly.
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108217116321450713/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="400"
|
||||||
|
height="200" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108218107689729996/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="400"
|
||||||
|
height="200" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108215257978801615/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="400"
|
||||||
|
height="180" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
I say things like this:
|
||||||
|
|
||||||
|
<iframe src="https://pony.social/@cadey/108218301565484230/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="400"
|
||||||
|
allowfullscreen="allowfullscreen"></iframe><script
|
||||||
|
src="https://pony.social/embed.js" async="async"></script>
|
||||||
|
|
||||||
|
and it fires back with:
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108218304118515023/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="400"
|
||||||
|
height="180" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
This is art. It looks like a robot pretending to be a human and just barely
|
||||||
|
passing at it. This helps you transform your expectations about what human and
|
||||||
|
bot tweets really are.
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108213387014890181/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="400"
|
||||||
|
height="200" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
If you want to influence `robocadey` into giving you an artistic experience,
|
||||||
|
mention it on the fediverse by adding `@robocadey@botsin.space` to your posts.
|
||||||
|
It will think a bit and then reply with a brand new post for you.
|
||||||
|
|
||||||
|
## Setting It Up
|
||||||
|
|
||||||
|
You probably don't want to do this, but if you're convinced you do then here's
|
||||||
|
some things that may help you.
|
||||||
|
|
||||||
|
1. Use the systemd units in `/run` of [github:Xe/x](https://github.com/Xe/x).
|
||||||
|
2. Put your model into a squashfs volume that you mount to the
|
||||||
|
`/var/lib/private/xeserv.robocadey-gpt2/checkpoint` folder.
|
||||||
|
3. Don't expect any warranty, reliability promises or assistance setting this
|
||||||
|
up. I made this for myself, not for others. Its source code is made available
|
||||||
|
to make the code part of that art, but the code is not the art that it makes.
|
||||||
|
|
||||||
|
Good luck.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
I guess what I think about art is that it's not just the medium. It's not just
|
||||||
|
the expression. It's the combination of it all. The expression, the medium, the
|
||||||
|
circumstances, all of that leads into what art really is. I could say that art
|
||||||
|
is the intangible expressions, emotions, and whatever that you experience when
|
||||||
|
looking at things; but that sounds really really pretentious, so let's just say
|
||||||
|
that art doesn't exist. Well it does, but only in the mind of the viewer.
|
||||||
|
|
||||||
|
There's not some objective scale that can say that something is or is not an
|
||||||
|
art. Art is imagined and we are conditioned to believe that things are or are
|
||||||
|
not art based on our upbringing.
|
||||||
|
|
||||||
|
I feel that as a shitposter my goal is to challenge people's "objective sense"
|
||||||
|
of what "can" and "can't" be art by sitting right in the middle of the two and
|
||||||
|
laughing. Projects like `robocadey` are how I make art. It's like what 200 lines
|
||||||
|
of code at most. You could probably recreate most of it based on the contents of
|
||||||
|
this post alone. I wonder if part of the art here comes from the fact that most
|
||||||
|
of this is so iterative yet so novel. Through the iteration process I end up
|
||||||
|
creating novelty.
|
||||||
|
|
||||||
|
You could also say that art is the antidote to the kind of suffering that comes
|
||||||
|
from the fundamental dissatisfactions that people have with everyday life. By
|
||||||
|
that defintion, I think that `robocadey` counts as art.
|
||||||
|
|
||||||
|
Either way, it's fun to do these things. I hope that this art can help inspire
|
||||||
|
you to think differently about the world. Even though it's through a chatbot
|
||||||
|
that says things like this:
|
||||||
|
|
||||||
|
<iframe src="https://botsin.space/@robocadey/108215945151030016/embed"
|
||||||
|
class="mastodon-embed" style="max-width: 100%; border: 0" width="400"
|
||||||
|
height="200" allowfullscreen="allowfullscreen"></iframe>
|
||||||
|
|
||||||
|
What is this if not art?
|
|
@ -0,0 +1,58 @@
|
||||||
|
---
|
||||||
|
title: "Twitter, Mastodon and The Parable of rasengan"
|
||||||
|
date: 2022-04-25
|
||||||
|
tags:
|
||||||
|
- twitter
|
||||||
|
- reaction
|
||||||
|
---
|
||||||
|
|
||||||
|
So a lot of things happened today. The threat that Elon Musk made to
|
||||||
|
buy Twitter seems to have been true. As I write this, my current
|
||||||
|
understanding is that the Twitter board of directors is in the process
|
||||||
|
of accepting the offer that Elon Musk to buy the company.
|
||||||
|
|
||||||
|
<xeblog-conv name="Cadey" mood="coffee">This was not on my bingo card
|
||||||
|
for 2022. I'm starting to think that I got a dud bingo card. I was
|
||||||
|
hoping that "Alien Invasion" would win out but it looks like that
|
||||||
|
won't be the case yet. Damn.</xeblog-conv>
|
||||||
|
|
||||||
|
I genuinely have no idea how I should be reacting to this news. I
|
||||||
|
spend a lot of time on Twitter. It's a lot of how I talk with people,
|
||||||
|
network in the tech community and generally shitpost. I do so much
|
||||||
|
idle shitposting on Twitter that it would probably count for a lot of
|
||||||
|
the non-work written word I produce on a weekly basis.
|
||||||
|
|
||||||
|
I'm really not sure what I feel about this, but the feelings that I am
|
||||||
|
getting remind me of [what happened to
|
||||||
|
freenode](/blog/series/freenode). A while ago someone I follow on
|
||||||
|
Twitter made a tweet that said something like:
|
||||||
|
|
||||||
|
> Elon Musk is to Twitter as rasengan is to freenode
|
||||||
|
|
||||||
|
<xeblog-conv name="Mara" mood="hacker">For context: rasengan is the
|
||||||
|
person that single-handedly destroyed freenode by apparently buying a
|
||||||
|
worthless holding company and then inciting a lot of
|
||||||
|
drama. For more information, check out the book Closed
|
||||||
|
Projects.</xeblog-conv>
|
||||||
|
|
||||||
|
I have made my book [Closed
|
||||||
|
Projects](/blog/closed-projects-2022-03-24) free for the next week.
|
||||||
|
This contains my moods, reactions and emotions as I was processing
|
||||||
|
everything falling apart around me. I feel this may help you
|
||||||
|
understand the emotions that you get watching this shitshow unfold
|
||||||
|
with Twitter.
|
||||||
|
|
||||||
|
For a direct link to the sale page, click
|
||||||
|
[here](https://itch.io/s/69916/elon-bought-twitter-sale). I will
|
||||||
|
donate proceeds from this sale to charity. If you choose to send me a
|
||||||
|
couple bucks for my book, I will donate them to the [Orca
|
||||||
|
Conservancy](https://www.orcaconservancy.org/).
|
||||||
|
|
||||||
|
I am also on Mastodon at
|
||||||
|
[@cadey@pony.social](https://pony.social/@cadey). Should something
|
||||||
|
happen to Twitter such that I can't participate there anymore, I will
|
||||||
|
be on Mastodon. If Twitter really starts falling apart, I will
|
||||||
|
probably be a lot more active on Mastodon. And probably writing a lot
|
||||||
|
more.
|
||||||
|
|
||||||
|
Let's hope things turn out well.
|
12
flake.lock
12
flake.lock
|
@ -35,11 +35,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1650647760,
|
"lastModified": 1651114127,
|
||||||
"narHash": "sha256-Ng8CGYLSTxeI+oEux0x+tSRA6K7ydoyfJNQf56ld+Uo=",
|
"narHash": "sha256-/lLC0wkMZkAdA5e1W76SnJzbhfOGDvync3VRHJMtAKk=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "b80f570a92d04e8ace67ff09c34aa48708a5c88c",
|
"rev": "6766fb6503ae1ebebc2a9704c162b2aef351f921",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -49,11 +49,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1650161686,
|
"lastModified": 1651007983,
|
||||||
"narHash": "sha256-70ZWAlOQ9nAZ08OU6WY7n4Ij2kOO199dLfNlvO/+pf8=",
|
"narHash": "sha256-GNay7yDPtLcRcKCNHldug85AhAvBpTtPEJWSSDYBw8U=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "1ffba9f2f683063c2b14c9f4d12c55ad5f4ed887",
|
"rev": "e10da1c7f542515b609f8dfbcf788f3d85b14936",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
@ -15,6 +15,6 @@ tracing = "0.1"
|
||||||
tracing-futures = "0.2"
|
tracing-futures = "0.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
eyre = "0.6.6"
|
eyre = "0.6.8"
|
||||||
kankyo = "0.3"
|
kankyo = "0.3"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
Loading…
Reference in New Issue