Friday, January 27, 2023

Remission

I'm in a good mood.  I'm celebrating.  It has been three years since I've had a depressed episode and that is amazing.

Ever since high school I've been dealing with Bipolar II disorder.  Every few months I'd have an episode of major depression that would last weeks.  The condition is drug-resistant.  Mood stabilizers such as lithium do nothing, and the various anti-depressants I've tried offered only limited, temporary relief.

But over the years new medications have come to market and new off-label treatments with other medications have been developed.  Three years ago my neuropsychopharmacologist and I discovered a combination of six meds that seems to have put the condition into remission.

A blog doesn't seem an appropriate forum for discussing my health.  I'm making an exception because I want to offer encouragement to any readers dealing with depression or Bipolar disorder.  I've been there, it sucks.  But keep on going, a solution is not impossible.  And not being depressed is fantastic.

Friday, January 14, 2022

Time for the obligatory Sudoku solver. Everybody else has one. They have fancy features like constraint solvers and backtrackers, but I'm contrary. Let's just use brute force.

Given a sudoku puzzle, we'll make one step towards solution by picking an empty cell and trying all possible values in it:

(defun children (sudoku)
  (multiple-value-bind (row column) (first-empty-cell sudoku)
    (and row column
         (map 'list (lambda (digit)
                      (place-digit sudoku row column digit))
              (possible-digits sudoku row column)))))
to solve the entire puzzle, we start with a population containing the single problem sudoku. Each iteration we replace the population of sudokus with their children.
(defun solve (sudoku)
  (labels ((lp (population)
            (let ((next-generation (mapcan #'children population)))
              (if (null next-generation)
                  population
                  (lp next-generation)))))
    (lp (list sudoku))))

Nothing clever here, just going through the combinations. This turns out to be enough, though. An extreme puzzle can take a few seconds and allocate a few hundred megabytes of storage, but a typical puzzle is solved quickly.

If you want to be a bit more clever, you can choose a better cell than the first empty one. A better cell would be one with fewer possible values to put in it. Although it's more expensive to compute the possible values for all 81 cells to find the fewest, it greatly reduces the population growth and ultimately speeds things up. But I don't need a sudoku solver at all, let alone an efficient one.

Monday, April 5, 2021

A TicTacToe solution

An anonymous reader offers this solution:

class TicTacToe {
    constructor() {
        this.turns = []
        this.board = new Array(9).fill(0)
        this.winner
        this.wins = {
            0: { 1: { 2: true }, 3: { 6: true }, 4: { 8: true } },
            1: { 4: { 7: true } },
            2: { 5: { 8: true }, 4: { 6: true } },
            3: { 4: { 5: true } },
            6: { 7: { 8: true } },
        }
        this.cellMap = {
            0: '_',
            1: 'X',
            2: 'O',
        }
    }

    print() {
        console.log('v'.repeat(7), 'turn', this.turns.length)

        this.board
            .map(c => this.cellMap[c])
            .join('')
            .match(/.{3}/g)
            .forEach(r => console.log(r))
    }

    takeTurn(t) {
        if (this.winner) {
            throw new Error('game is over')
        } else if (![1, 2].includes(t.state)) {
            throw new Error('invalid state')
        } else if (t.state === this.turns.slice(-1)[0]?.state) {
            throw new Error('not your turn')
        } else if (t.x < 0 || t.x > 2 || t.y < 0 || t.y > 2) {
            throw new Error('out of bounds')
        } else if (this.turns.find(turn => turn.x === t.x && turn.y === t.y)) {
            throw new Error('cell occupied')
        }

        this.board[t.x * 3 + t.y] = t.state
        const marks = this.board.reduce(
            (a, v, i) => (v === t.state ? [...a, i] : a),
            [],
        )
        const combos = combine(marks, 3)
        for (let i = 0; i < combos.length; i++) {
            const [a, b, c] = combos[i]
            if (this.wins[a] && this.wins[a][b] && this.wins[a][b][c]) {
                this.winner = this.cellMap[t.state]
                break
            }
        }

        this.turns.push(t)
        this.print()
        if (this.winner) return console.log('WINNER', this.winner)
    }
}

function combine(n, k) {
    let set
    if (typeof n == 'object') {
        set = n
        n = n.length
    }
    const result = []
    const combos = []
    const recurse = start => {
        if (combos.length + (n - start + 1) < k) {
            return
        }
        recurse(start + 1)
        combos.push(start)
        if (combos.length === k) {
            result.push(
                set
                    ? combos
                        .slice()
                        .map(c => set[c - 1])
                        .sort()
                    : combos.slice(),
            )
        } else if (combos.length + (n - start + 2) >= k) {
            recurse(start + 1)
        }
        combos.pop()
    }
    recurse(1, combos)
    return result
}

const t = new TicTacToe()
t.takeTurn({ state: 1, x: 0, y: 0 })
t.takeTurn({ state: 2, x: 1, y: 0 })
t.takeTurn({ state: 1, x: 2, y: 2 })
t.takeTurn({ state: 2, x: 1, y: 2 })
t.takeTurn({ state: 1, x: 0, y: 2 })
t.takeTurn({ state: 2, x: 1, y: 1 })
I've reproduced it here because Google strips the leading whitespace out of the comments section. I didn't have the original indentation, so I just asked Emacs to indent it.


It being an unsolicited submission, I'll have the gall to make an unsolicited critique.

From the standpoint of the original point of the question, the code demonstrates that the author can write coherent, runnable programs. Many of the candidates I interviewed could not get this far. That said, there is room for improvement.

My biggest concern is with the complexity of the code. Let me focus on the combine procedure for the moment. It took me some time to figure out what on earth it was trying to accomplish. It is given a list of the positions for a particular player during the game. So if the game looked like this:

 X | O | X
---+---+---
   | X | X
---+---+---
 O | O |   
the list of positions for X would be 0, 2, 4, 5. combine is also given a number k. It is to return a list of all the k-tuples that can be made from the list of positions. If k were 3 in this example, it would return (0, 2, 4), (0, 2, 5), (0, 4, 5), (2, 4, 5).

Take a moment to test your recursion skills by coding this up.


We can observe this recursion: the solution consists of the first element paired with all (k-1)-tuples of the remaining elements

(map 'list (lambda (s) (cons (car elements) s)) (combine (cdr elements) (- k 1)))
plus all the solutions that don't involve the first element
(combine (cdr elements) k)
There are two base cases: if k is zero, there is a single solution: the zero-tuple '(). If there are no elements, we cannot choose k of them and there are no solutions. Putting this together we get this:
(defun combine (elements k)
   (cond ((zerop k) (list '()))    ; a list of one solution, the zero-tuple
         ((null elements) (list))  ; a list of no solutions
         (t (append (map 'list (lambda (s)
                                 (cons (car elements) s))
                         (combine (cdr elements) (- k 1)))
                    (combine (cdr elements) k)))))
I'll leave it to the reader to translate this back into Javascript.

The author of the original code seems to have gotten lost in the mechanics of constructing solutions. The variable combos is pushed and popped as the recursive calls are made. This means we are keeping track of the recursion both through the recursive flow control and through mutation and backtracking. I find mutation and backtracking harder to understand than recursion because you have to keep track of when exactly things are being mutated.

As I mentioned in the previous post, I was less concerned with the details of how the program worked than I was with whether the candidate could take a “word problem” and turn it into something that works. This approach — generating triplets of positions and seeing if they are winning — works. However, I find overall that it is a bit too complex and the procedure combine is confusing.

Saturday, March 13, 2021

A Google Interview Question

When I was working at Google I had to interview a lot of candidates. Most often the interview was general in nature. I wasn't evaluating a candidate for a particular position, I was determining whether a candidate was all around "Google material".

Although Google is known for its challenging interview questions, I found the simple questions gave me the most information about a candidate. One of my favorite interview questions was this: “Given the state of a tic-tac-toe game (naughts and crosses), determine if someone has won the game. Code this up however you'd like.”

This isn't a very hard problem. (Pause a moment and think how you'd do it, or even take a moment to write it up.) The straightforward solution is to check the rows, the columns, and the two diagonals. I was less interested in whether the candidate would come up with this solution or something more clever than I was in seeing exactly how the candidate worked his way from an informal word problem towards a formal solution in code. I wanted to see how the candidate organized his thoughts. I wanted to see how proficient the candidate was in programming when he was allowed to use the language he was most comfortable with.

I expected most candidates to whip off a solution in a few minutes and then we'd move on to the next interview question. To my surprise, fully half the candidates were unable to finish this simple task. A couple seemed unable to use a whiteboard to sketch out a solution. A large number dove right in and started coding up the obvious nested loop only to get confused at handling the flow control. (There is a hidden trap in this problem: if you choose the nested loop solution, you want to abort the inner loop early when you determine a solution is not possible on the current row or column you are checking, but you want to abort the outer loop early when you determine a solution is possible.) It was often that a candidate did not at first realize that a cell in the tic-tac-toe board has three possible states, not two. One candidate used 4x4 arrays to represent the board so he could use 1-based indexing. Not a single candidate tried to abstract over the low-level array manipulation.

Interviews are highly stressful for candidates, so I didn't judge them too harshly. If it looked like they were on the right track and they successfully noticed and repaired a bug or two in their solution I'd call it a win. If they got hopelessly lost, I'm afraid I couldn't recommend them. It was sad because many seemed very nice people and they were trying their best. Either they weren't cut out to be programmers or their education was not serving them well at all.

I do have to mention one candidate who knocked this question out of the park. He observed that there really aren't that many tic-tac-toe game positions, so you can just hash the game position and look up the winner in a table. I didn't even ask him to code this up.

Tuesday, March 2, 2021

Roll your own conditional

Sometimes you'd like to write your own conditionals. Perhaps you'd like to wrap something around one or both arms of the branch. Perhaps the predicate is tristate and you need three branches. Perhaps the predicate wants to share information with one or both arms. You want to abstract away how the predicate makes it decision from how the caller wants to handle the possible outcomes. The trick is to use continuation passing style:

sign = lambda n, if_positive, if_zero, if_negative: \
           if_positive() if n > 0 else \
           if_negative() if n < 0 else \
           if_zero()

# use it like this:
for i in range (-1, 1):
    print (i)
    print (sign (i,
            lambda: \
              "positive",
            lambda: (lambda first, second: second)(
              print ("Hooray!"),
              "zero"),
            lambda: \
              "negative"))
By making the branches be thunks, we delay evaluation. Once the conditional decides which branch to take, it forces the thunk and returns its value.

Monday, March 1, 2021

Some of Python's language constructs, like variable binding, exception handling, sequencing, and iteration, are provided only through language statements. You cannot use statements in an expression context, so it would seem we are up against a limitation. But Python has lambda expressions. Lambda expressions and function calls are all you need to implement variable binding, sequencing and recursion as ordinary Python expressions.

For example, suppose you wish to have a 'let' expression where you bind helper variables 'a' and 'b' to subexpressions. Simply use lambda and immediately call it:

    return (lambda a, b: h(a, b))(f(x), g(y))
Not pretty, but it gets the job done.

If you want to sequence two expressions, write them like this:
    return (lambda ignore: x + 7)(print (x))
The print expression runs first, its result is ignored and then x is added to 7 and returned. Again, this could use a lot of syntactic sugar, but it gets the job done. Alternatively, we could use left to right evaluation of arguments to do our sequencing:
    return (lambda first, second: second)(print(x), x + 7)
In order to implement iteration, we need to bind a name recursively. We'll use the Y operator.
Y = lambda f: (lambda d: d(d)) (lambda x: f (lambda: x (x)))

print (Y (lambda loop:  \
              lambda x: \
                  None if x == 0 else (lambda first, second: second)(
                                           print (x),
                                           loop()(x - 1)))
       (5))

5
4
3
2
1
0
None

Handling exceptions is easy if you use a helper function and some thunks:

def on_error (compute_value, handle_error):
    try:
        return compute_value()
    except:
        return handle_error()


def safe_divide (dividend, divisor):
    return on_error (
           lambda: dividend / divisor,
           lambda: (lambda first, second: second)(
                       print ('divide by zero, answer 1'),
                       1))

print (safe_divide (12, 3))
4
print (safe_divide (12, 0))
divide by zero, answer 1
1

Sunday, February 28, 2021

I saw this gem at www.askpython.com:
Someone asked me if we can have a lambda function without any argument?
Yes, we can define a lambda function without any argument. But, it will be useless because there will be nothing to operate on.
Using lambda function without any argument is plain abuse of this feature.

Obviously the author never heard of a thunk. 

Tuesday, February 2, 2021

Descriptive statistics

Suppose I ran a benchmark. Being thorough, I ran it several hundred times, but that just produces a big pile of numbers and we need a way to summarize the results concisely.

The naive way would be to report the average run time and perhaps the variance or standard deviation. This would allow us to assert that the benchmark takes, say, 10.5 ± 1.2 seconds. A reader would feel confident that the bulk of the benchmark runs would take somewhere between 9.3 seconds and 11.7 seconds, and that a lot more runs would take near 10.5 seconds than the minimum of 9.3 seconds or the maximum of 11.7 seconds. The mean (average) and the standard deviation are the characteristic statistics of a normal (Gaussian) distribution. Rather than give the results of hundreds of runs, I summarize by saying that the distribution is Gaussian and I give the characteristic statistics of the best fitting Gaussian distribution.

But what if the distribution isn't Gaussian? By reporting the statistics of the best fitting Gaussian at best I've reported something irrelevant, at worst I've mislead the reader about the nature of the distribution.

It turns out that program run times do not typically follow a Gaussian distribution. A number of different factors influence the run time of a program and these are combined multiplicatively. The resulting distribution will be heavily skewed towards the shorter times, but have a long tail of longer run times. It will look a bit like this:

But a curious thing is going on here. If we plot the exact same curve but use a logarithmic scale for the X axis, we get this:

Our old friend the Gaussian distribution appears. Run times of computer programs typically follow a Gaussian distribution if you plot them on a logarithmic scale. This is called a log-normal distribution and it is one of the most frequently encountered distributions (after the Gaussian).

When reporting a log-normal distribution, it makes sense to report the mean and standard deviation of the Gaussian that appears after you transform to a logarithmic scale. If you transform and take the mean, you end up with the geometric mean. If you transform and take the standard deviation, you end up with something called the geometric standard deviation. This latter is just a little tricky. Instead of a margin of error that you add or subtract from the mean, you have a factor by which you multiply or divide the geometric mean. For example, I might assert a particular benchmark takes 10.5 ×/÷ 1.2, and you can interpret this as meaning that the majority of runs took between (/ 10.5 1.2) = 8.75 seconds and (* 10.5 1.2) = 12.6 seconds.

When you see a paper reporting the mean and standard deviation of some benchmark runs, you should be instantly suspicious. Benchmark runs don't usually follow a Gaussian distribution and it is likely that the author is unaware of this fact and is mindlessly applying irrelevant statistics. But if you see a paper where the geometric mean and geometric standard deviation are reported, you can have much more confidence that the author thought about what he or she was trying to say.

I find it particularly annoying when I see a benchmark harness that will run a benchmark several times and report the mean and standard deviation of the runs. The author of the harness is trying to be helpful by reporting some statistics, but the harness is likely reporting the wrong ones.

Monday, February 1, 2021

Oddity noticed while running a benchmark

I've heard it said that using a trampoline to achieve tail recursion incurs a cost of around a factor of two. I was curious whether that was accurate and where that cost comes from, so I decided to measure it with a micro-benchmark. I chose the Tak benchmark as a starting point because it doesn't do much but function calls and it is fairly well known. The basic Tak benchmark is this:

public static int Tak (int x, int y, int z) {
    return (! (y < x))
        ? z
        : Tak (Tak (x - 1, y, z),
               Tak (y - 1, z, x),
               Tak (z - 1, x, y));
}

Tak (32, 16, 8) => 9
When called with arguments 32, 16, and 8, the benchmark makes an additional 50,510,520 calls to itself, of which 1/4 of these (12,627,630) are self tail calls. One run of the benchmark consists of calling Tak (32, 16, 8) ten times. On my laptop this takes around 900 ms.

I ran the benchmark 250 times and recorded how long it took. Here are the times plotted against the run number:

For the first 50 runs of the benchmark the average time seems to slowly increase from around 830 ms to around 930 ms, then it goes to about 900 ms for the next 50 runs, then it settles down to around maybe 890 ms after that. Mostly it stays in a fairly narrow band around the average time, but now and then the time will jump to over 1000 ms for a couple of runs.

I don't know why the average run time drifts over time like this, but the effect is visible on several different benchmarks. Since it takes the better part of a minute to perform the first 50 runs, the period over which the runtime increases is relatively long — on the order of 30 - 40 seconds. My guess is maybe as the benchmark runs the processor heats up and begins to slow down slightly in order to control the heat.

Friday, April 17, 2020

This is lame

<rant>
I'm reading in a file called “telemetry.json”. The contents resemble this:
start_time:2020-04-17:08-32-45
end_time:2020-04-17:09-18-07
  • Despite the file name, this isn't JSON format.
  • The timestamps are in some weird format. Who puts dashes between the time entries?
  • It's anybody's guess whether this is local time or GMT.
So instead of just writing objectMapper.readValue(), I had to write a page and a half of code that reads the file a line at a time, splits the line into a key and value string, runs the value string through a custom date parser, and constructs a telemetry POJO to hold the results. I'm guessing this is in local time. I just have to hope the machine that wrote the file is physically in the same time zone as the machine that ends up reading the file (that'll make a truly obscure bug).

There are libraries to write JSON files. Someone was too lazy or ignorant to use them. There is a standard date/time format, ISO 8601, and libraries to read and write timestamps with actual time zone information. Someone was too lazy to use them. Anyone want to take a bet that this is written with a Python script? So now I have to do the extra work to make up for someone else's laziness.

Amateurs.

I lose the bet. It's 13 different shell scripts.
</rant>

Tuesday, February 4, 2020

Apropos of nothing

I am visiting San Jose, California to meet with my remote boss and the team I am working on. I was sitting at the hotel bar last night when an older gentleman with a large bushy beard came in, walked up to the bar, plunked down a coupon, and ordered a scotch, Macallan 12-year old, neat. I've never seen someone get a scotch with a coupon before, so I remarked upon it. The gentleman sat down and we started chatting. The bartender needed approval from her boss, which she got, and then she poured half a shot before the bottle ran out. She asked if another half shot of Macallan 10-year old was an acceptable substitute and got a reluctant yes.

I eventually got around to asking what the gentleman did for a living (I understand that in some countries asking about someone's work is considered rude, but not here). To my surprise, it turned out he is working for the same company I am. He was visiting from North Carolina where he worked out of the Durham office. He was one of the early employees of the company and knows a lot about the architecture of the product and was out here to train new sales engineers about how the product worked. Although he was in sales, he was an engineer, so we had a lot in common. We talked late into the evening about work, college, cars, music, mathematics, and other various topics. I got a lot of good background information on the company and the product from him.

I suppose there's no real point to this story. It's just interesting how chance meetings sometimes turn out.

Saturday, January 25, 2020

User Interface

I don't care for UI code.  I don't care where the pixels end up or how round a button is.

Case in point, I just added a column to a table.   The problem:  the column isn't wide enough to hold the data it displays, so the data runs out of the column and into the next one, overwriting the text there.  Simple problem.  Simple solution?  No.  It was easy enough to add a style element to the column making it wider.  But now the column header didn't line up with the column.  I added the same style element to the header, but it was completely ignored.  In fact, nothing I could think of would make the column header simply be wider than it was.  There were 12 columns, and each header took 1/12th of the space.  That was written in stone.

Well, almost.  I made there be 13 columns, and set that one particular header to span 2 columns.  I had to do the same to the data and make it span 2 columns as well.  It does not look good, but that particular column is now wide enough to hold the data, and the header lines up above the data like it should.  Unfortunately, each of the other columns are just that much narrower to spoil the layout.  There are some columns that would naturally be nice and narrow and give me lots of  extra room to play with, but no, there are now 13 columns, each taking 1/13th of the space, except for one that's twice as wide.

This is why I don't like UI.  Why does the table data pay attention to the "width" directive and the table header ignore it completely?  I don't know.  Will anyone complain that my "solution" is ugly?  You bet.  I even think it is ugly.  I've already wasted enough thought and energy trying to make it prettier, but to no avail.

Tuesday, January 21, 2020

Thanks to my readers

I want to thank everyone who reads what I post. I write blog entries to try to gather my thoughts into coherent forms, and knowing that someone might actually take me seriously keeps me on my toes. I don't want to confuse or mislead anyone.

Even if no one takes me seriously, though, at least they get some cheap entertainment watching me try to muddle through my confusion.

I'm starting a new job today, so blog posts will likely be shorter and less frequent. With luck, however, I've gotten back into the habit of posting regularly.

Please keep the feedback coming. I read every comment, though I don't answer many (most don't require an answer).

Thanks again.

Sunday, January 19, 2020

Group actions

In a previous post I discussed how the elements of a group could be seen as “acting” on other elements directly as if they were functions. There's a more direct way of getting at this, but it is so simple, it might be a bit of a disappointment. But we'll take things a bit further.

First, let's consider the group with the element 0, 1, and 2 where the binary operation is addition modulo 3. It is closed, associative, 0 is the identity element, and 1 and 2 are inverses of each other. It is a group.

;; save the real multiply
> (define real-multiply *)
real-multiply

> (define (* a b) (modulo (+ a b) 3))
*

> (* 0 1)
1

> (* 1 1)
2

> (* 2 2)
1
But the elements of the group aren't functions, they are integers. They cannot directly act on other elements:
> (1 2)
Error -- application of non-procedure object 1

There's a simple trick to get directly at the “action” of an element on another element. Just “curry” the * operator:

> (define (get-action operator element)
    (define (action other-element) (operator element other-element))
    action)
get-action

> (define action-of-1 (get-action * 1))
action-of-1

> (action-of-1 2)
0

> (action-of-1 1)
2
See, that's almost disappointingly simple. Of course the other elements in the group will have their own actions obtained in the same way:
>(define action-of-0 (get-action * 0))
action-of-0

>(action-of-0 1)
1
No surprise that the action of the identity element leaves things unchanged.

Let's consider a different group. This group also has 3 element, '(a b c), '(b c a), and '(c a b). The operator is a funny sort of list rotation: if the first argument is '(a b c), it doesn't rotate the second argument at all. If the first argument is '(b c a), it rotates the second argument left by one position. If the first argument is '(c a b) it rotates the second argument by two positions:

> (define (funny-rotate first second)
     (cond ((equal? first '(a b c)) second)
           ((equal? first '(b c a)) (rotate-left second))
           ((equal? first '(c a b)) (rotate-left (rotate-left second)))))
funny-rotate

> (funny-rotate '(a b c) '(b c a))
(b c a)

> (funny-rotate '(b c a) '(c a b))
(a b c)
You've probably noticed by now that this second group is a disguised version of the first group with
0 <=> (a b c)
1 <=> (b c a)
2 <=> (c a b)
(modulo (+ a b) 3) <=> (funny-rotate first second)
This is an example of an isomorphism between two groups.

Suppose, for the sake of argument, that this second group is just like it currently is, but it had a few more elements that funny-rotate knew how to handle. But otherwise everything else is the same. Then there would be elements that didn't have a one-to-one mapping. This wouldn't be an isomorphism but a monomorphism. These are both kinds of homomorphisms between groups. (The wall of terminology appears.)

Let's back away from the wall of terminology for a moment and go back to the action of an element. Notice that the action of an element is a function. It maps elements to elements. The action is actually a bit more abstract than that. We can swap out the elements with the elements of an isomporphic group. So the action-of-1 which carries 0->1,1->2, and 2->0 can be considered the same action that carries '(a b c)->'(b c a), '(b c a)->'(c a b), and '(c a b)->'(a b c). Even though 1 doesn't even belong to the same group as '(a b c), we can consider action-of-1 and action-of-bca to be the same thing.

The action is even a bit more abstract. We can swap out the operator of the action, too. Here we have to be careful to make sure we don't ruin the group structure when we do that. To give an example, consider the conjugate of some action on some argument: (conjugate (f g)) = (h f) where we've found the h that makes (h f) equal to (f g). Recall that h is equivalent to fgf-1. We can, without ruining the group structure, give each element in the group the action of conjugating it's argument. (Just to throw out a little more terminology, when a group element acts on other elements in the same group, it's called an automorphism.)

We can, if we are careful not to ruin the group structure, swap out both the operator and the elements of the action of an element. But let's not do that. Let's return to simply getting the elements of the group to act on each other by currying the operator. This is a nice, easy to understand automorphism. In our examples, our groups have three elements. You can imagine them as sitting on the corners of a triangle. One element is the identity element, it leaves everything alone. One element bring 0->1, 1->2, and 2->0 or the equivalent, and the final element undoes it, bringing 0->2, 1->0, and 2->1. If they were sitting on the corners of a triangle, one element rotates clockwise, one elements rotates counterclockwise, one element does nothing. These automorphisms simply permute the elements of the group. And it has to be so. Because each element has an inverse, we have to be able to determine what the “input” element was associated with the “output” element, so each unique input must lead to a unique output, which makes it a permutation.

Ok, one more thing. Permutations are associative. If you have a set of permutations that are closed, includes the identity and all the inverses, then you have a group of permutations (called the symmetric group, even though there are many because it depends on how many items you are permuting. Better to parameterize by the number of items and call it symmetricn.) Cayley's theorem says that every group is a subgroup of the symmetricn group acting on the elements. That is, every group has some symmetries associated with it. The symmetries can help you gain insight into the group. This is pretty remarkable: we started with foldable sequences with identity elements and inverse elements, and we ended up with deep connection to symmetries.

I just wanted to get to as far as Cayley's theorem, so I'll end this now. I hope there wasn't too much terminology, but I'm afraid there was enough abstraction to make me dizzy.

Monday, November 25, 2019

Don't s— where you eat

It's a vulgar saying, but true. It is one of the primary reasons I don't blog much about work. But I don't work for Google anymore, and my consulting gig at Blue Origin is finished. So I think I'll be blogging a bit about working at Google and a bit about consulting. Of course I'm obligated to not deliver the “real dirt”, so I won't say anything you couldn't find out through public sources, and everything I say will reflect the past, not the present. Things may have changed.

I stayed at Google several years longer than I should have. It left me a bit ill-prepared for the real world. At Google, we used mostly custom, in-house software development tools. Google's build tool, Blaze, was released to the outside world as Bazel, and it does a lot of what Maven and Artifactory do for building and tracking build dependencies. There was a way to use git with the source control, but it was optional and I preferred to use the Perforce-like interface directly. We didn't use a tool like gitlab for code reviews, we had our own in-house tool. We used protocol-buffers for inter-process communication, not JSON generated by Swagger. Google's internal cloud was built long before Kubernetes. The tools provided were all decent tools, but they aren't the “industry standard”, and when I started consulting I was a bit at sea with a tool set that was similar, but not exactly the same thing. The fact is you have to be familiar with a lot more tools than you used to be: autoconf, make, and GCC don't cut it anymore.

The consulting gig was a trial by fire. I prefer programming the back-end and systems level to programming the front-end and user-interface, but out of necessity I got to write Angular, and typescript, use node.js, and write GraphQL queries to support the UI. I got to store things in MongoDB rather than in Bigtable. I even used IntelliJ instead of Emacs (somewhat. I still used Emacs for serious editing.) And although both jobs made use of Java, even it has changed a fair bit.

So I'm looking forward to the my newest job and I expect the learning curve to be not quite as steep. No doubt there will be some new tools but probably not all new tools.

First Post

My first posts on my unsyndicated blog. I want to keep this unsyndicated so I can be more spontaneous and less focused. My other blog, Abstract Heresies is rather Lisp, Scheme, and computer language focused. It's also replicated in several Lisp and Scheme newsgroups, which makes me feel uncomfortable posting non-Lisp or Scheme content. This blog will have computers, but also science, math, and any other random thing I feel like blogging about. Well, this is the first article, so now I have something to format.