577 lines
62 KiB
XML
577 lines
62 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||
|
||
<title>Colin McMillen's Blog</title>
|
||
<link href="https://www.mcmillen.dev"/>
|
||
<link rel="self" href="https://www.mcmillen.dev/feed.atom"/>
|
||
<updated>2023-11-08T18:56:47-04:00</updated>
|
||
<author>
|
||
<name>Colin McMillen</name>
|
||
</author>
|
||
<id>https://www.mcmillen.dev/</id>
|
||
|
||
|
||
<entry>
|
||
<title>Creating robot behaviors with Python generators</title>
|
||
<id>https://www.mcmillen.dev/blog/20070502-robot-behaviors-python.html</id>
|
||
<link rel="alternate" href="https://www.mcmillen.dev/blog/20070502-robot-behaviors-python.html"/>
|
||
<content type="html">
|
||
<![CDATA[
|
||
<h1 id="creating-robot-behaviors-with-python-generators">Creating robot behaviors with Python generators</h1>
|
||
<p><em>Posted 2007-05-02.</em></p>
|
||
<p><a href="https://en.wikipedia.org/wiki/Generator_(computer_programming)">Generators</a> are a <a href="https://docs.python.org/2.7/reference/simple_stmts.html#the-yield-statement">powerful feature</a> of the Python programming language. In a nutshell, generators let you write a function that behaves like an iterator. The standard approach to programming robot behaviors is based on state machines. However, robotics code is <em>full</em> of special cases, so a complex behavior will typically end up with a lot of bookkeeping cruft. Generators let us simplify the bookkeeping and express the desired behavior in a straightforward manner.</p>
|
||
<p>(Idea originally due to <a href="http://www.cs.cmu.edu/~jbruce/">Jim Bruce</a>.)</p>
|
||
<p>I’ve worked for several years on <a href="https://robocup.org">RoboCup</a>, the international robot soccer competition. <a href="http://www.cs.cmu.edu/~robosoccer/legged/">Our software</a> is written in a mixture of C++ (for low-level localization and vision algorithms) and Python (for high-level behaviors). Let’s say we want to write a simple goalkeeper for a robot soccer team. Our keeper will be pretty simple; here’s a list of the requirements:</p>
|
||
<ol>
|
||
<li>If the ball is far away, stand in place.</li>
|
||
<li>If the ball is near by, dive to block it. Dive to the left if the ball is to the left; dive to the right if the ball is to the right.</li>
|
||
<li>If we choose a “dive” action, then “stand” on the next frame, nothing will happen. (Well, maybe the robot will twitch briefly....) So when we choose to dive, we need to commit to sending the same dive command for some time (let’s say one second).</li>
|
||
</ol>
|
||
<p>The usual approach to robot behavior design relies on hierarchical state machines. Specifically, we might be in a “standing” state while the ball is far away; when the ball becomes close, we enter a “diving” state that persists for one second. Because of requirement 3, this solution will have a few warts: we need to keep track of how much time we’ve spent in the dive state. Every time we add a special case like this, we need to keep some extra state information around. Since robotics code is full of special cases, we tend to end up with a lot of bookkeeping cruft. In contrast, generators will let us clearly express the desired behavior.</p>
|
||
<p>On to the state-machine approach. First, we’ll have a class called Features that abstracts the robot’s raw sensor data. For this example, we only care whether the ball is near/far and left/right, so Features will just contain two boolean variables:</p>
|
||
<div class="codehilite"><pre><span></span><span class="k">class</span> <span class="nc">Features</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
|
||
<span class="n">ballFar</span> <span class="o">=</span> <span class="bp">True</span>
|
||
<span class="n">ballOnLeft</span> <span class="o">=</span> <span class="bp">True</span>
|
||
</pre></div>
|
||
|
||
|
||
<p>Next, we make the goalkeeper. The keeper’s behavior is specified by the <code>next()</code> function, which is called thirty times per second by the robot’s main event loop (every time the on-board camera produces a new image). The <code>next()</code> function returns one of three actions: <code>"stand"</code>, <code>"diveLeft"</code>, or <code>"diveRight"</code>, based on the current values of the Features object. For now, let’s pretend that requirement 3 doesn’t exist.</p>
|
||
<div class="codehilite"><pre><span></span><span class="k">class</span> <span class="nc">Goalkeeper</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
|
||
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">features</span><span class="p">):</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">features</span> <span class="o">=</span> <span class="n">features</span>
|
||
|
||
<span class="k">def</span> <span class="nf">next</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||
<span class="n">features</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">features</span>
|
||
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballFar</span><span class="p">:</span>
|
||
<span class="k">return</span> <span class="s1">'stand'</span>
|
||
<span class="k">else</span><span class="p">:</span>
|
||
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballOnLeft</span><span class="p">:</span>
|
||
<span class="k">return</span> <span class="s1">'diveLeft'</span>
|
||
<span class="k">else</span><span class="p">:</span>
|
||
<span class="k">return</span> <span class="s1">'diveRight'</span>
|
||
</pre></div>
|
||
|
||
|
||
<p>That was simple enough. The constructor takes in the <code>Features</code> object; the <code>next()</code> method checks the current <code>Features</code> values and returns the correct action. Now, how about satisfying requirement 3? When we choose to dive, we need to keep track of two things: how long we need to stay in the <code>"dive"</code> state and which direction we dove. We’ll do this by adding a couple of instance variables (<code>self.diveFramesRemaining</code> and <code>self.lastDiveCommand</code>) to the Goalkeeper class. These variables are set when we initiate the dive. At the top of the <code>next()</code> function, we check if <code>self.diveFramesRemaining</code> is positive; if so, we can immediately return <code>self.lastDiveCommand</code> without consulting the <code>Features</code>. Here’s the code:</p>
|
||
<div class="codehilite"><pre><span></span><span class="k">class</span> <span class="nc">Goalkeeper</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
|
||
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">features</span><span class="p">):</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">features</span> <span class="o">=</span> <span class="n">features</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">diveFramesRemaining</span> <span class="o">=</span> <span class="mi">0</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">lastDiveCommand</span> <span class="o">=</span> <span class="bp">None</span>
|
||
|
||
<span class="k">def</span> <span class="nf">next</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||
<span class="n">features</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">features</span>
|
||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">diveFramesRemaining</span> <span class="o">></span> <span class="mi">0</span><span class="p">:</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">diveFramesRemaining</span> <span class="o">-=</span> <span class="mi">1</span>
|
||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">lastDiveCommand</span>
|
||
<span class="k">else</span><span class="p">:</span>
|
||
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballFar</span><span class="p">:</span>
|
||
<span class="k">return</span> <span class="s1">'stand'</span>
|
||
<span class="k">else</span><span class="p">:</span>
|
||
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballOnLeft</span><span class="p">:</span>
|
||
<span class="n">command</span> <span class="o">=</span> <span class="s1">'diveLeft'</span>
|
||
<span class="k">else</span><span class="p">:</span>
|
||
<span class="n">command</span> <span class="o">=</span> <span class="s1">'diveRight'</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">lastDiveCommand</span> <span class="o">=</span> <span class="n">command</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">diveFramesRemaining</span> <span class="o">=</span> <span class="mi">29</span>
|
||
<span class="k">return</span> <span class="n">command</span>
|
||
</pre></div>
|
||
|
||
|
||
<p>This satisfies all the requirements, but it’s ugly. We’ve added a couple of bookkeeping variables to the Goalkeeper class. Code to properly maintain these variables is sprinkled all over the <code>next()</code> function. Even worse, the structure of the code no longer accurately represents the programmer’s intent: the top-level if-statement depends on the state of the robot rather than the state of the world. The intent of the original <code>next()</code> function is much easier to discern. (In real code, we could use a state-machine class to tidy things up a bit, but the end result would still be ugly when compared to our original <code>next()</code> function.)</p>
|
||
<p>With generators, we can preserve the form of the original <code>next()</code> function and keep the bookkeeping only where it’s needed. If you’re not familiar with generators, you can think of them as a special kind of function. The <code>yield</code> keyword is essentially equivalent to <code>return</code>, but the next time the generator is called, <em>execution continues from the point of the last <code>yield</code></em>, preserving the state of all local variables. With <code>yield</code>, we can use a <code>for</code> loop to “return” the same dive command the next 30 times the function is called! Lines 11-16 of the below code show the magic:</p>
|
||
<div class="codehilite"><pre><span></span><span class="k">class</span> <span class="nc">GoalkeeperWithGenerator</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
|
||
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">features</span><span class="p">):</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">features</span> <span class="o">=</span> <span class="n">features</span>
|
||
|
||
<span class="k">def</span> <span class="nf">behavior</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||
<span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
|
||
<span class="n">features</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">features</span>
|
||
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballFar</span><span class="p">:</span>
|
||
<span class="k">yield</span> <span class="s1">'stand'</span>
|
||
<span class="k">else</span><span class="p">:</span>
|
||
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballOnLeft</span><span class="p">:</span>
|
||
<span class="n">command</span> <span class="o">=</span> <span class="s1">'diveLeft'</span>
|
||
<span class="k">else</span><span class="p">:</span>
|
||
<span class="n">command</span> <span class="o">=</span> <span class="s1">'diveRight'</span>
|
||
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">xrange</span><span class="p">(</span><span class="mi">30</span><span class="p">):</span>
|
||
<span class="k">yield</span> <span class="n">command</span>
|
||
</pre></div>
|
||
|
||
|
||
<p>Here’s a simple driver script that shows how to use our goalkeepers:</p>
|
||
<div class="codehilite"><pre><span></span><span class="kn">import</span> <span class="nn">random</span>
|
||
|
||
<span class="n">f</span> <span class="o">=</span> <span class="n">Features</span><span class="p">()</span>
|
||
<span class="n">g1</span> <span class="o">=</span> <span class="n">Goalkeeper</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
|
||
<span class="n">g2</span> <span class="o">=</span> <span class="n">GoalkeeperWithGenerator</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="o">.</span><span class="n">behavior</span><span class="p">()</span>
|
||
|
||
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">xrange</span><span class="p">(</span><span class="mi">10000</span><span class="p">):</span>
|
||
<span class="n">f</span><span class="o">.</span><span class="n">ballFar</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">random</span><span class="p">()</span> <span class="o">></span> <span class="mf">0.1</span>
|
||
<span class="n">f</span><span class="o">.</span><span class="n">ballOnLeft</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">random</span><span class="p">()</span> <span class="o"><</span> <span class="mf">0.5</span>
|
||
<span class="n">g1action</span> <span class="o">=</span> <span class="n">g1</span><span class="o">.</span><span class="n">next</span><span class="p">()</span>
|
||
<span class="n">g2action</span> <span class="o">=</span> <span class="n">g2</span><span class="o">.</span><span class="n">next</span><span class="p">()</span>
|
||
<span class="k">print</span> <span class="s2">"</span><span class="si">%s</span><span class="se">\t</span><span class="si">%s</span><span class="se">\t</span><span class="si">%s</span><span class="se">\t</span><span class="si">%s</span><span class="s2">"</span> <span class="o">%</span> <span class="p">(</span>
|
||
<span class="n">f</span><span class="o">.</span><span class="n">ballFar</span><span class="p">,</span> <span class="n">f</span><span class="o">.</span><span class="n">ballOnLeft</span><span class="p">,</span> <span class="n">g1action</span><span class="p">,</span> <span class="n">g2action</span><span class="p">)</span>
|
||
<span class="k">assert</span><span class="p">(</span><span class="n">g1action</span> <span class="o">==</span> <span class="n">g2action</span><span class="p">)</span>
|
||
</pre></div>
|
||
|
||
|
||
<p>… and we’re done! I hope you’ll agree that the generator-based keeper is much easier to understand and maintain than the state-machine-based keeper. You can grab the full source code below and take a look for yourself.</p>
|
||
<div class="codehilite"><pre><span></span><span class="ch">#!/usr/bin/env python</span>
|
||
|
||
<span class="k">class</span> <span class="nc">Features</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
|
||
<span class="n">ballFar</span> <span class="o">=</span> <span class="bp">True</span>
|
||
<span class="n">ballOnLeft</span> <span class="o">=</span> <span class="bp">True</span>
|
||
|
||
|
||
<span class="k">class</span> <span class="nc">Goalkeeper</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
|
||
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">features</span><span class="p">):</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">features</span> <span class="o">=</span> <span class="n">features</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">diveFramesRemaining</span> <span class="o">=</span> <span class="mi">0</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">lastDiveCommand</span> <span class="o">=</span> <span class="bp">None</span>
|
||
|
||
<span class="k">def</span> <span class="nf">next</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||
<span class="n">features</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">features</span>
|
||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">diveFramesRemaining</span><span class="p">:</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">diveFramesRemaining</span> <span class="o">-=</span> <span class="mi">1</span>
|
||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">lastDiveCommand</span>
|
||
<span class="k">else</span><span class="p">:</span>
|
||
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballFar</span><span class="p">:</span>
|
||
<span class="k">return</span> <span class="s1">'stand'</span>
|
||
<span class="k">else</span><span class="p">:</span>
|
||
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballOnLeft</span><span class="p">:</span>
|
||
<span class="n">command</span> <span class="o">=</span> <span class="s1">'diveLeft'</span>
|
||
<span class="k">else</span><span class="p">:</span>
|
||
<span class="n">command</span> <span class="o">=</span> <span class="s1">'diveRight'</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">lastDiveCommand</span> <span class="o">=</span> <span class="n">command</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">diveFramesRemaining</span> <span class="o">=</span> <span class="mi">29</span>
|
||
<span class="k">return</span> <span class="n">command</span>
|
||
|
||
|
||
<span class="k">class</span> <span class="nc">GoalkeeperWithGenerator</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
|
||
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">features</span><span class="p">):</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">features</span> <span class="o">=</span> <span class="n">features</span>
|
||
|
||
<span class="k">def</span> <span class="nf">behavior</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||
<span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
|
||
<span class="n">features</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">features</span>
|
||
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballFar</span><span class="p">:</span>
|
||
<span class="k">yield</span> <span class="s1">'stand'</span>
|
||
<span class="k">else</span><span class="p">:</span>
|
||
<span class="k">if</span> <span class="n">features</span><span class="o">.</span><span class="n">ballOnLeft</span><span class="p">:</span>
|
||
<span class="n">command</span> <span class="o">=</span> <span class="s1">'diveLeft'</span>
|
||
<span class="k">else</span><span class="p">:</span>
|
||
<span class="n">command</span> <span class="o">=</span> <span class="s1">'diveRight'</span>
|
||
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">xrange</span><span class="p">(</span><span class="mi">30</span><span class="p">):</span>
|
||
<span class="k">yield</span> <span class="n">command</span>
|
||
|
||
|
||
<span class="kn">import</span> <span class="nn">random</span>
|
||
<span class="n">f</span> <span class="o">=</span> <span class="n">Features</span><span class="p">()</span>
|
||
<span class="n">g1</span> <span class="o">=</span> <span class="n">Goalkeeper</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
|
||
<span class="n">g2</span> <span class="o">=</span> <span class="n">GoalkeeperWithGenerator</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="o">.</span><span class="n">behavior</span><span class="p">()</span>
|
||
|
||
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">xrange</span><span class="p">(</span><span class="mi">10000</span><span class="p">):</span>
|
||
<span class="n">f</span><span class="o">.</span><span class="n">ballFar</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">random</span><span class="p">()</span> <span class="o">></span> <span class="mf">0.1</span>
|
||
<span class="n">f</span><span class="o">.</span><span class="n">ballOnLeft</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">random</span><span class="p">()</span> <span class="o"><</span> <span class="mf">0.5</span>
|
||
<span class="n">g1action</span> <span class="o">=</span> <span class="n">g1</span><span class="o">.</span><span class="n">next</span><span class="p">()</span>
|
||
<span class="n">g2action</span> <span class="o">=</span> <span class="n">g2</span><span class="o">.</span><span class="n">next</span><span class="p">()</span>
|
||
<span class="k">print</span> <span class="s2">"</span><span class="si">%s</span><span class="se">\t</span><span class="si">%s</span><span class="se">\t</span><span class="si">%s</span><span class="se">\t</span><span class="si">%s</span><span class="s2">"</span> <span class="o">%</span> <span class="p">(</span>
|
||
<span class="n">f</span><span class="o">.</span><span class="n">ballFar</span><span class="p">,</span> <span class="n">f</span><span class="o">.</span><span class="n">ballOnLeft</span><span class="p">,</span> <span class="n">g1action</span><span class="p">,</span> <span class="n">g2action</span><span class="p">)</span>
|
||
<span class="k">assert</span><span class="p">(</span><span class="n">g1action</span> <span class="o">==</span> <span class="n">g2action</span><span class="p">)</span>
|
||
</pre></div>
|
||
]]>
|
||
</content>
|
||
<updated>2007-05-02T12:00:00-04:00</updated>
|
||
</entry>
|
||
|
||
<entry>
|
||
<title>Emacs Tips</title>
|
||
<id>https://www.mcmillen.dev/blog/20070522-emacs-tips.html</id>
|
||
<link rel="alternate" href="https://www.mcmillen.dev/blog/20070522-emacs-tips.html"/>
|
||
<content type="html">
|
||
<![CDATA[
|
||
<h1 id="emacs-tips">Emacs Tips</h1>
|
||
<p><em>Posted 2007-05-22, updated 2021-07-01.</em></p>
|
||
<p>These are some emacs keybindings (and other functions) that I once found useful. I’ve mostly used Sublime Text for the last few years, however.</p>
|
||
<h2 id="editing">Editing</h2>
|
||
<p><code>C-[SPC]</code>: set mark<br>
|
||
<code>C-x C-x</code>: exchange point and mark<br>
|
||
<code>C-w</code>: kill (AKA “cut”)<br>
|
||
<code>M-w</code>: kill-ring-save (AKA “copy”)<br>
|
||
<code>C-y</code>: yank (AKA “paste”)<br>
|
||
<code>M-h</code>: Put region around current paragraph (mark-paragraph).<br>
|
||
<code>C-x h</code>: Put region around the entire buffer (mark-whole-buffer).<br>
|
||
<code>C-u C-[SPC]</code>: Move in mark ring<br>
|
||
<code>M-d</code>: Kill word<br>
|
||
<code>M-[DEL]</code>: Kill word backwards<br>
|
||
<code>C-M-k</code>: Kill the following balanced expression (kill-sexp)</p>
|
||
<h2 id="registers">Registers</h2>
|
||
<p><code>C-x r r</code>: Save position of point in register r (point-to-register).<br>
|
||
<code>C-x r j r</code>: Jump to the position saved in register r (jump-to-register).<br>
|
||
<code>C-x r s r</code>: Copy region into register r (copy-to-register).<br>
|
||
<code>C-x r i r</code>: Insert text from register r (insert-register).</p>
|
||
<h2 id="bookmarks">Bookmarks</h2>
|
||
<p><code>C-x r m [RET]</code>: Set the bookmark for the visited file, at point.<br>
|
||
<code>C-x r m bookmark [RET]</code>: Set the bookmark named bookmark at point (bookmark-set).<br>
|
||
<code>C-x r b bookmark [RET]</code>: Jump to the bookmark named bookmark (bookmark-jump).<br>
|
||
<code>C-x r l</code>: List all bookmarks (list-bookmarks).<br>
|
||
<code>M-x bookmark-save</code>: Save all the current bookmark values in the default bookmark file.</p>
|
||
<h2 id="miscellaneous">Miscellaneous</h2>
|
||
<p><code>M-`</code> shows the menu.<br>
|
||
<code>M-x highlight-changes-mode</code> toggles showing the changes you’ve made to the file since the last save.</p>
|
||
]]>
|
||
</content>
|
||
<updated>2007-05-22T12:00:00-04:00</updated>
|
||
</entry>
|
||
|
||
<entry>
|
||
<title>Gnokii Tips</title>
|
||
<id>https://www.mcmillen.dev/blog/20070522-gnokii-tips.html</id>
|
||
<link rel="alternate" href="https://www.mcmillen.dev/blog/20070522-gnokii-tips.html"/>
|
||
<content type="html">
|
||
<![CDATA[
|
||
<h1 id="gnokii-tips">Gnokii Tips</h1>
|
||
<p><em>Posted 2007-05-22, updated 2021-07-01.</em></p>
|
||
<p>I own a Nokia 6102i phone (provided by Cingular). <a href="http://gnokii.org">gnokii</a> is a Linux program that lets me interface with the phone. Here are some recipes:</p>
|
||
<h2 id="file-io">File I/O</h2>
|
||
<p><code>gnokii --getfilelist "A:\\predefgallery\\predeftones\\predefringtones\\*"</code></p>
|
||
<p><code>gnokii --putfile WiiSports.mp3 "A:\\predefgallery\\predeftones\\predefringtones\\WiiSports.mp3"</code></p>
|
||
<h2 id="ring-tones">Ring Tones</h2>
|
||
<p>Voice mail picks up in 20 seconds, so a ring tone should be about 20 seconds long.</p>
|
||
<p>The easiest way to chop an MP3 in Linux is with <code>dd</code>; the drawback is that you need to specify length in KB, not time. To chop an MP3 to be 200 KB long, do:</p>
|
||
<p><code>dd if=Mii\ Channel.mp3 of=MiiChan2.mp3 bs=1k count=200</code></p>
|
||
<h2 id="phonebook">Phonebook</h2>
|
||
<p>To make a Phonebook.ldif file from the phone (suitable for import into Thunderbird):</p>
|
||
<p><code>gnokii --getphonebook ME 1 end --ldif > Phonebook.ldif</code></p>
|
||
<p>To add the entries in Phonebook.ldif to the phone:</p>
|
||
<p><code>cat Phonebook.ldif | gnokii --writephonebook -m ME --find-free --ldif</code></p>
|
||
<p>You can specify <code>--overwrite</code> instead of <code>--find-free</code> if you want to overwrite all the entries, but this will lose some data (e.g. speed dial, preferred numbers).</p>
|
||
<h2 id="multimedia">Multimedia</h2>
|
||
<p>You can get photos like this:<br>
|
||
<code>gnokii --getfile "A:\\predefgallery\\predefphotos\\Image000.jpg"</code><br>
|
||
They are 640x480 JPG files. (You can also configure the camera so that it takes pictures at 80x96.)</p>
|
||
<p>You can also store files:<br>
|
||
<code>gnokii --putfile silly.jpg "A:\\predefgallery\\predefphotos\\silly.jpg"</code><br>
|
||
These show up on the phone in <code>My Stuff/Images</code>. The files don’t need to be any specific size; they are autoscaled. GIFs probably also work.</p>
|
||
<p>Videos live here:<br>
|
||
<code>gnokii --getfile "A:\\predefgallery\\predefvideos\\Video000.3gp"</code><br>
|
||
VLC seems to be able to play <code>.3gp</code> files, but the audio doesn’t work.</p>
|
||
<p>Audio recordings live here:<br>
|
||
<code>gnokii --getfile "A:\\predefgallery\\predefrecordings\\Audio000.amr"</code></p>
|
||
<p>Unfortunately, nothing I knew of in 2007 (when this page was first written) would play <code>.amr</code> files, but these days (2021) perhaps <code>ffmpeg input.amr output.mp3</code> would work. You might have to use the <code>-ar</code> flag to specify the audio rate. I haven’t actually tried this though!</p>
|
||
]]>
|
||
</content>
|
||
<updated>2007-05-22T12:00:00-04:00</updated>
|
||
</entry>
|
||
|
||
<entry>
|
||
<title>LaTeX Tips</title>
|
||
<id>https://www.mcmillen.dev/blog/20070522-latex-tips.html</id>
|
||
<link rel="alternate" href="https://www.mcmillen.dev/blog/20070522-latex-tips.html"/>
|
||
<content type="html">
|
||
<![CDATA[
|
||
<h1 id="latex-tips">LaTeX Tips</h1>
|
||
<p><em>Posted 2007-05-22; updated 2021-07-01.</em></p>
|
||
<p>Note that these instructions are over a decade old. Some documentation may be out of date. :)</p>
|
||
<h2 id="embedding-fonts-in-pdfs">Embedding fonts in PDFs</h2>
|
||
<p>To check whether fonts are embedded, use <code>pdffonts</code>, which is included with <code>xpdf</code>. <code>pdffonts</code> gives output that looks like this:</p>
|
||
<div class="codehilite"><pre><span></span>$ pdffonts paper.pdf
|
||
name type emb sub uni object ID
|
||
------------------------------------ ------------ --- --- --- ---------
|
||
FHQIOS+NimbusRomNo9L-Medi Type 1 yes yes no 6 0
|
||
NEESMN+NimbusRomNo9L-Regu Type 1 yes yes no 9 0
|
||
PJQNOS+CMSY10 Type 1 yes yes no 12 0
|
||
</pre></div>
|
||
|
||
|
||
<p>You want <code>emb</code> to be <code>yes</code> for all fonts (and possibly <code>sub</code> as well; also, all fonts should be Type 1, not Type 3). By default in Ubuntu, pdflatex should embed all fonts. Just in case, you can check <code>/etc/texmf/updmap.d/00updmap.cfg</code>, which should have a line like this:</p>
|
||
<p><code>pdftexDownloadBase14 true</code></p>
|
||
<p>If it’s set to <code>false</code>, change it to <code>true</code>, then run <code>update-updmap</code> as root. Remake the PDF; if it still has non-embedded fonts, your figures are probably to blame. Check your PDF figures and make sure their fonts are embedded (using the <code>pdffonts</code> command). For anything that doesn’t have embedded fonts, you can try the following magical invocation:</p>
|
||
<div class="codehilite"><pre><span></span>gs -dSAFER -dNOPLATFONTS -dNOPAUSE -dBATCH -sDEVICE=pdfwrite \
|
||
-sPAPERSIZE=letter -dCompatibilityLevel=1.4 -dPDFSETTINGS=/printer \
|
||
-dCompatibilityLevel=1.4 -dMaxSubsetPct=100 -dSubsetFonts=true \
|
||
-dEmbedAllFonts=true -sOutputFile=figures/Mprime-new.pdf -f figures/Mprime.pdf
|
||
</pre></div>
|
||
|
||
|
||
<p>This creates a file <code>figures/Mprime-new.pdf</code> that is hopefully identical to the input file <code>figures/Mprime.pdf</code>, except that the fonts are embedded. Run <code>pdffonts</code> on it to check.</p>
|
||
<p>Once all your figures are in PDF format, remake the paper again. Hopefully, all your fonts are now embedded — check again with <code>pdffonts</code>.</p>
|
||
]]>
|
||
</content>
|
||
<updated>2007-05-22T12:00:00-04:00</updated>
|
||
</entry>
|
||
|
||
<entry>
|
||
<title>Vim Tips</title>
|
||
<id>https://www.mcmillen.dev/blog/20070807-vim-tips.html</id>
|
||
<link rel="alternate" href="https://www.mcmillen.dev/blog/20070807-vim-tips.html"/>
|
||
<content type="html">
|
||
<![CDATA[
|
||
<h1 id="vim-tips">Vim Tips</h1>
|
||
<p><em>Posted 2007-08-07.</em></p>
|
||
<p>Here’s some links about learning/mastering vim.</p>
|
||
<h2 id="why-use-vim">Why use vim?</h2>
|
||
<ul>
|
||
<li><a href="http://blog.ngedit.com/2005/06/03/the-vi-input-model/">The vi input model - laptop keyboards suck</a></li>
|
||
<li><a href="http://www.viemu.com/a-why-vi-vim.html">Why, oh WHY, do those nutheads use vi?</a></li>
|
||
</ul>
|
||
<h2 id="tutorials">Tutorials</h2>
|
||
<ul>
|
||
<li><a href="http://www.viemu.com/a_vi_vim_graphical_cheat_sheet_tutorial.html">Graphical vim cheat sheet and tutorial</a> — <a href="http://www.glump.net/dokuwiki/howto/vim_graphical_cheat_sheet">PDF version</a></li>
|
||
</ul>
|
||
]]>
|
||
</content>
|
||
<updated>2007-08-07T12:00:00-04:00</updated>
|
||
</entry>
|
||
|
||
<entry>
|
||
<title>93% of Paint Splatters are Valid Perl Programs</title>
|
||
<id>https://www.mcmillen.dev/sigbovik/</id>
|
||
<link rel="alternate" href="https://www.mcmillen.dev/sigbovik/"/>
|
||
<content type="html">
|
||
<![CDATA[
|
||
<h1 id="93-of-paint-splatters-are-valid-perl-programs">93% of Paint Splatters are Valid Perl Programs</h1>
|
||
<p><em>Posted 2019-04-01.</em></p>
|
||
<p>TLDR: <a href="2019.pdf">read the paper</a> and <a href="splatters.html">view the gallery of pretty Perl programs</a>.</p>
|
||
<p>In this paper, we aim to answer a long-standing open problem in the programming languages community: <em>is it possible to smear paint on the wall without creating valid Perl?</em></p>
|
||
<p>We answer this question in the affirmative: it <strong>is possible</strong> to smear paint on the wall without creating a valid Perl program. We employ an empirical approach, using optical character recognition (OCR) software, which finds that merely 93% of paint splatters parse as valid Perl. We analyze the properties of paint-splatter Perl programs, and present seven examples of paint splatters which are not valid Perl programs.</p>
|
||
<p><a href="https://twitter.com/jaffathecake/status/1095706032448393217"><img alt="Screenshot of a Twitter conversation. Adrienne Porter Felt says: "I don't want to teach my kid to code. I want him to splash in muddy puddles and smear paint on the walls and read novels under the covers way too late at night. I grew up too soon and wish I'd had more time to be a kid. Why do schools teach vocational skills so young these days?" Jake Archibald replies: "but is it possible to smear paint on the wall without creating valid Perl?"" src="/media/20190401-sigbovik-tweet.png"></a></p>
|
||
<p>Accepted for publication at SIGBOVIK 2019, held April 1st 2019 in Pittsburgh. Winner of a Unwitting Participation Ribbon, “an unwelcome brand we’ve affixed to each paper determined after careful scrutiny to have included a genuine artifact, thereby furthering the admirable causes of open science and fruitful procrastination.”</p>
|
||
<p>Read it on <a href="https://docs.google.com/document/d/1ZGGNMfmfpWB-DzWS3Jr-YLcRNRjhp3FKS6v0KELxXK8/preview">Google Docs</a> or download a <a href="2019.pdf">PDF</a>. Or grab the <a href="http://sigbovik.org/2019/proceedings.pdf">entire SIGBOVIK 2019 proceedings</a>; I’m on page 174.</p>
|
||
<h2 id="supplementary-materials">Supplementary Materials</h2>
|
||
<p>Here’s <a href="splatters.html">all the paint splatters</a> on a single page, along with the valid Perl source code corresponding to each. “Not valid” is written in red for those images which did not parse as valid Perl programs. If different OCR settings recognized multiple valid Perl programs, I chose the one that seemed the most “interesting”, according to my own aesthetic sense.</p>
|
||
<p>Here’s a <a href="splatters.tar.gz">tarball of 100 paint-splatter images</a> that were used as the main dataset for this paper.</p>
|
||
<p><strong>(source code not available yet because i am bad at GitHub)</strong></p>
|
||
<h2 id="errata">Errata</h2>
|
||
<p>There are a few paint splatter Perl programs that I didn’t recognize as “interesting” until after the SIGBOVIK submission deadline. For example, this splatter is recognized by OCR as the string <code>lerzfijglpFiji-j</code>, which evaluates to the number <code>0</code> in Perl:</p>
|
||
<p><img alt="paint splatter" src="splatters/6b78f8696b05f9322b2dda21b6932776.jpg"></p>
|
||
<p>The image below is recognized as the string <code>-*?</code>, which also evaluates to the number <code>0</code> in Perl:</p>
|
||
<p><img alt="paint splatter" src="splatters/e47b8463b359906947c66ec4c852a2a3.jpg"></p>
|
||
<p>Another surprising program is shown below; OCR recognizes this image as the string <code>;i;c;;#\\?z{;?;;fn':.;</code>, which evaluates to the string <code>c</code> in Perl:</p>
|
||
<p><img alt="paint splatter" src="splatters/803dd5a54c42ed93462c78ad7da357b0.jpg"></p>
|
||
<p>Finally, this image is recognized as the string <code>;E,'__'</code>, which evaluates to the string <code>E__</code> in Perl:</p>
|
||
<p><img alt="paint splatter" src="splatters/dc86c1c3553705b7b2f973d5be9e0389.jpg"></p>
|
||
]]>
|
||
</content>
|
||
<updated>2019-04-01T12:00:00-04:00</updated>
|
||
</entry>
|
||
|
||
<entry>
|
||
<title>My first paper in 10 years?!</title>
|
||
<id>https://www.mcmillen.dev/blog/20190403-update.html</id>
|
||
<link rel="alternate" href="https://www.mcmillen.dev/blog/20190403-update.html"/>
|
||
<content type="html">
|
||
<![CDATA[
|
||
<h1 id="my-first-paper-in-10-years">My first paper in 10 years?!</h1>
|
||
<p><em>Posted 2019-04-03.</em></p>
|
||
<p>It’s been nearly two months since my last day at Google, so I guess I should finally make use of this newsletter :)</p>
|
||
<p>I wrote <a href="https://docs.google.com/document/d/1ZGGNMfmfpWB-DzWS3Jr-YLcRNRjhp3FKS6v0KELxXK8/preview">a paper</a> which was published on April 1st as part of SIGBOVIK 2019: “93% of Paint Splatters are Valid Perl Programs”. In this paper, I answer a long-standing open problem in the programming languages community: <em>is it possible to smear paint on the wall without creating valid Perl?</em></p>
|
||
<p>(Long-standing since February 13, 2019, when a <a href="https://twitter.com/jaffathecake/status/1095706032448393217">Twitter conversation</a> between Adrienne Porter Felt & Jake Archibald posed the question.)</p>
|
||
<p>To answer this question, I downloaded 100 images of paint splatters from Pinterest, ran the open-source Tesseract OCR engine to turn each into a text string, and then sent that text to the Perl interpreter to see whether that text successfully parsed as Perl. It turns out that 93 of the 100 paint splatters do parse as valid Perl, but since 7% do not, I conclude that it <strong>is possible</strong> to smear paint on a wall without creating valid Perl.</p>
|
||
<p>You might suspect there is some chicanery going on with this result. You’d be correct, but… honestly there’s not <em>that</em> much chicanery going on. You’ll have to read the paper for details… and for my attempts at academic humor. :)</p>
|
||
<p>There’s also some <a href="/sigbovik">supporting material</a> on this website, including a <a href="/sigbovik/splatters.html">gallery of all 100 images</a> and their associated valid Perl code. Here’s a screenshot of some of them. (Did you know that the string <code>lerzfijglpFiji-j</code> evaluates to the number <code>0</code> in Perl?)</p>
|
||
<p><img alt="screenshot of 17 paint splatters, and the Perl programs they represent" src="/media/20190403-update-splatters.png"></p>
|
||
<p>As it turns out, the publication date of my paper was exactly 10-years-minus-a-day since my Ph.D. thesis defense. I’d planned on travelling back to Carnegie Mellon to give this talk live at SIGBOVIK 2019, but unfortunately came down with a nasty cold-and-cough so I had to cancel my trip. :( Perhaps I can give a belated talk at next year’s conference.</p>
|
||
<p>For more light-hearted and vaguely CS-shaped research papers, check out the rest of the <a href="http://sigbovik.org/2019/proceedings.pdf">SIGBOVIK 2019 proceedings</a>. I particularly enjoyed “Elo World, a framework for benchmarking weak chess engines” by tom7 (“The computer players include some traditional chess engines, but also many algorithms chosen for their simplicity, as well as some designed to be competitively bad”.)</p>
|
||
<p>Some other random things that I’ve been up to in the last month-and-a-half:</p>
|
||
<ul>
|
||
<li>
|
||
<p><a href="https://twitter.com/mcmillen/status/1095795492196364297">ohnosay</a>, which is like “cowsay” but for comics in the style of webcomicname. [GitHub] This was a good excuse to get a Linux development environment set up on a persistent Google Cloud instance & to learn how to GitHub. Since then, I also realized that the World Outside Google uses Python 3, so I’ve started learning that :)</p>
|
||
<p><img alt='a three panel comic displayed on a linux terminal: "i will write a silly program" "hm, what did i do with my ssh credentials?" "oh no"' src="/media/20190403-update-ohno.png"></p>
|
||
</li>
|
||
<li>
|
||
<p>Gardening! Last August I randomly planted some peppermint in a railing container on my balcony, and it went gangbusters. This spring I’ve actually planned out a whole porch-garden (like <a href="https://twitter.com/sevandyk/status/1109121188079427585">Stardew Valley but real life</a>). Last year’s mint has started growing again, and I’ve added spearmint and mojito mint. I’ve also got two types of peas, two mixes of salad greens, and spinach planted. Later I’ll be planting carrots, basil, and rosemary. The peas just started sprouting a couple days ago, which is exciting!</p>
|
||
<p><img alt='a container showing an assortment of "asian salad" greens' src="/media/20190403-update-garden.jpg"></p>
|
||
</li>
|
||
<li>
|
||
<p>Gloomhaven! This is a cooperative legacy-style board game — a fun dungeon-crawler that doesn’t need a DM, so everyone gets to play. Our group is still only a few scenarios in, but we’re enjoying it so far. SO MANY HEX TILES. I’m also getting ready to paint our party’s miniatures, which is another (potential) new hobby of mine; more to come in a future newsletter, I suspect :)</p>
|
||
</li>
|
||
<li>
|
||
<p>Video games: just started Sekiro: Shadows Die Twice on PS4. Recently completed (and really enjoyed) New Super Mario Bros. U Deluxe for Nintendo Switch (though Nintendo seems to be trying to give Google a run for their money on ridiculous product names). I’ve also been playing Total War: Warhammer 2 regularly, and Splatoon 2 from time to time. I tried getting into XCOM 2 & enjoyed it, but I’m not sure I’m interested enough to finish the campaign. I keep going back to Total War when I want something in the tactical / strategy genre.</p>
|
||
</li>
|
||
<li>
|
||
<p>Guitar: starting to learn fingerstyle, with the goal of eventually becoming good enough to play <a href="https://www.songsterr.com/a/wsa/chrono-cross-dream-of-the-shore-boardering-another-world-tab-s5033t2">Dream of the Shore Bordering Another World</a> from Chrono Cross.</p>
|
||
</li>
|
||
<li>
|
||
<p>Computer stuff: upgraded my PC’s video card (it was many years old) and upgraded to an all-SSD setup. It turns out that 2TB SSDs aren’t that expensive any more.</p>
|
||
</li>
|
||
<li>
|
||
<p>Getting healthcare without an employer is a disaster — even in Massachusetts, which reportedly has one of the best systems in the US. Still working on straightening out my paperwork. Apparently they refuse to believe in my proof of health-insurance termination, even though it’s lettermarked by Google and everything.</p>
|
||
</li>
|
||
</ul>
|
||
<p>Thanks for reading! Hopefully the next update will come sooner than 2 months and thus be a bit shorter than this one ended up being :)</p>
|
||
<p>~ Colin</p>
|
||
]]>
|
||
</content>
|
||
<updated>2019-04-03T12:00:00-04:00</updated>
|
||
</entry>
|
||
|
||
<entry>
|
||
<title>A new year & a sneaky new project</title>
|
||
<id>https://www.mcmillen.dev/blog/20200209-sneak.html</id>
|
||
<link rel="alternate" href="https://www.mcmillen.dev/blog/20200209-sneak.html"/>
|
||
<content type="html">
|
||
<![CDATA[
|
||
<h1 id="a-new-year-a-sneaky-new-project">A new year & a sneaky new project</h1>
|
||
<p><em>Posted 2020-02-09.</em></p>
|
||
<p>I can’t believe it’s here so quickly, but: today marks a year since my last day at Google. That seemed like a good occasion to dust off this newsletter & let you know what I’ve been up to: making a videogame!</p>
|
||
<p>I’m working on a stealth-based 2D platformer where you don’t have to kill anyone unless you want to. It’ll be possible to get through every level by sneaking and misdirection, but it’ll require you to be careful and tactical to do so… and of course if that doesn’t work out, you can always draw your swords and go in fighting! So far I’ve given it “Sneak” as a codename, but that’s definitely a placeholder until I can flesh out more of the world.</p>
|
||
<p>So far Sneak runs on PC & Xbox, but I hope to add Switch and PS4 support within the next couple months. I’m using a C# framework called MonoGame, which provides low-level graphics & audio support across all these platforms. In order to write games for Switch or PS4, you need to apply to Nintendo & Sony to get access to their platform-specific SDKs. So my first real milestone will be coming up with a compelling Game Design Doc & gameplay videos so that they can (hopefully) be convinced that I’m worth taking seriously. Wish me luck!</p>
|
||
<p>Sony won’t even talk to anyone unless they’re a Real Business (& Nintendo kinda wants you to be too), so as of… yesterday, I’m officially the founder of SemiColin Games LLC (and, for now at least, the only member…)</p>
|
||
<p>If you want to follow along, I have an extremely-placeholder website up at <a href="https://semicolin.games">semicolin.games</a> where you can sign up for Yet Another Newsletter if you like, and a Twitter account <a href="https://twitter.com/SemiColinGames">@SemiColinGames</a> that would appreciate a follow. I’ll probably set up a devblog with an RSS feed too eventually, but that’s not quite ready yet. When it is, I’ll send a quick update here.</p>
|
||
<p>I only got started in December & a lot of my work so far has been on building infrastructure (and learning how to start a business), so I don’t have any Extremely Compelling Gameplay Videos yet. Here’s a short animated GIF for now. The <a href="https://twitter.com/mcmillen/status/1205164954728509440">bloopers on Twitter</a> might be more fun though. :)</p>
|
||
<p><img alt="Animation of a pixel-art character swinging a sword" src="/media/20200209-sneak.gif"><br>
|
||
(Art definitely not final!)</p>
|
||
<p>Thanks for following along with me on this adventure! Hopefully my next update will come more quickly, and be less wordy! I’ve wanted to make videogames since I was Literally A Kid, so I’m quite excited to finally be doing that full-time, and to hopefully share something good with all of you. When I’m at a stage where I want alpha testers, I’ll definitely be asking here first.</p>
|
||
<p>Thanks for your support!<br>
|
||
~ Colin (& <a href="https://semicolin.games">SemiColin Games</a>)</p>
|
||
]]>
|
||
</content>
|
||
<updated>2020-02-09T12:00:00-04:00</updated>
|
||
</entry>
|
||
|
||
<entry>
|
||
<title>Downvotes & Dislikes Considered Harmful</title>
|
||
<id>https://www.mcmillen.dev/blog/20210721-downvotes-considered-harmful.html</id>
|
||
<link rel="alternate" href="https://www.mcmillen.dev/blog/20210721-downvotes-considered-harmful.html"/>
|
||
<content type="html">
|
||
<![CDATA[
|
||
<h1 id="downvotes-dislikes-considered-harmful">Downvotes & Dislikes Considered Harmful</h1>
|
||
<p><em>Posted 2021-07-21.</em></p>
|
||
<p>If you’re letting users rank content, you probably <strong>don’t need and don’t want downvotes</strong>. Here’s why.</p>
|
||
<p>(This post inspired by news that Twitter is considering <a href="https://twitter.com/Sadcrib/status/1417913362999136257">adding “Dislikes” to Tweets</a>.)</p>
|
||
<h2 id="background">Background</h2>
|
||
<p>In my past life at Google, I was responsible for co-creating <a href="https://books.google.com/books?id=fEJ0AwAAQBAJ&newbks=1&newbks_redir=0&lpg=PP83&dq=memegen%20eric%20schmidt&pg=PP83#v=onepage&q=memegen%20eric%20schmidt&f=false">Memegen</a>, a large & influential Google-internal social network. Memegen lets Google employees create internal-only memes and allows users to upvote & downvote the memes of others. Memegen’s home page is the Popular page, which shows the most-upvoted memes of the past day.</p>
|
||
<p>Adding downvotes to Memegen was my single greatest mistake.</p>
|
||
<h2 id="the-problems-of-downvotes">The problems of downvotes</h2>
|
||
<p>Any voting system where <em>most</em> posts mostly receive upvotes, but also allows downvotes, has a huge problem:</p>
|
||
<blockquote>
|
||
<p>No matter how you do the math, <strong>downvotes count more</strong> than upvotes do.</p>
|
||
</blockquote>
|
||
<p>Mathematically, it will always be comparatively easy for a vocal minority to bury any specific items that they don’t want surfaced on the top-N posts page. This is true even if you’re using a sophisticated ranking algorithm like <a href="https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval#Wilson_score_interval">Wilson score intervals</a> to rank posts (as Reddit & many other sites do).</p>
|
||
<p>Downvotes aim to solve the problem of filtering out low-quality <strong>content</strong>, but are too easily coopted by trolls to let them filter out <strong>people</strong> — often for bad reasons that have more to do with the identity of who’s posting rather than the content of their posts.</p>
|
||
<p>From the standpoint of attracting users, downvotes create another huge problem: someone whose first submission to a site gets downvoted to oblivion will feel bad about it and probably not come back to submit better stuff in the future.</p>
|
||
<h2 id="what-does-a-downvote-actually-mean">What does a downvote actually <em>mean?</em></h2>
|
||
<p>The other problem with downvotes is that it’s unclear to everyone what they mean. Does a downvote mean that this particular post is:</p>
|
||
<ol>
|
||
<li>offensive or illegal and needs to be removed ASAP?</li>
|
||
<li>a duplicate?</li>
|
||
<li>just something you personally don’t like?</li>
|
||
<li>off-topic for the forum?</li>
|
||
</ol>
|
||
<p>As the creator of a social product, you need <strong>give people different buttons</strong> for these.</p>
|
||
<p>Offensive or illegal posts (#1) shouldn’t be handled by an algorithmic rating system. You need actual human moderators for that — and enough of them that they can review those reports in a timely manner. (I hope you’re willing to train & pay them well!)</p>
|
||
<p>For duplicate posts (#2) it’s nicer & more informative if your software simply says “hey, this submission is a duplicate of this other thing, why don’t you all check out that post instead?”</p>
|
||
<p>#3 is solved by default — people can simply not vote for content they don’t like.</p>
|
||
<p>#4 is pretty much the same as #3 (but maybe a moderator should intervene if a user has a history of posting too many off-topic things, or if it’s obviously spam).</p>
|
||
<h2 id="how-to-actually-rank-posts">How to actually rank posts</h2>
|
||
<p>Once you’ve dispensed with the idea of downvotes, the main things a user cares about are: “what are the best things that have been posted today?” (or in the last hour / week / etc) or “what are the best things since I last visited?”</p>
|
||
<p>On paper, the math is super simple: just count the number of upvotes for each item that was submitted in the relevant time period, and show the top N!</p>
|
||
<p>It turns out that’s it’s actually a bit trickier to implement than something like a Wilson score interval, so here’s some tips on how to do that.</p>
|
||
<p>We need to store each vote and when it was cast, and then when it’s time to compute the “most popular in the last day” page, you first select all the votes cast within the last day, and then count how many were for each post, and rank those.</p>
|
||
<p>Doing this every time the user hits the homepage is clearly a terrible idea, so set up a cronjob to do it every 5 or 15 minutes or something. It’s okay if the info is slightly out of date! Most users won’t care or notice if it takes a few minutes for things to move around.</p>
|
||
<p>How exactly to optimize this depends on the scale of your site, your storage architecture, a ton of other stuff, but for Memegen, every post had properties like <code>score_hour</code>, <code>score_day</code>, <code>score_month</code>, <code>score_alltime</code>. A mapreduce was responsible for updating these values every few minutes.</p>
|
||
<p>Obviously you don’t need to touch or compute anything for any post that got no votes since the last time you ran the updater. In the steady state, <em>most</em> of the posts in your system won’t need any update.</p>
|
||
<h2 id="conclusion">Conclusion</h2>
|
||
<p>Downvotes are a blunt instrument for users to say “I don’t like this content”.</p>
|
||
<p>It’s easy for small groups of trolls to misuse downvotes as a vehicle for harassing & silencing groups of (often marginalized) people.</p>
|
||
<p>Downvotes reduce engagement by scaring off first-time posters.</p>
|
||
<p>Instead of adding downvotes to your site, build <em>specific</em> tools that handle specific kinds of unwanted posts.</p>
|
||
<p>(This post is a distillation & refinement of some thoughts originally posted in <a href="https://twitter.com/mcmillen/status/1310998579184574465?s=20">a Twitter thread</a> in September 2020.)</p>
|
||
<h2 id="comments">Comments?</h2>
|
||
<p>Feel free to reply to <a href="https://twitter.com/mcmillen/status/1418010991913230336">my post on Twitter</a> about this article. Thanks!</p>
|
||
]]>
|
||
</content>
|
||
<updated>2021-07-21T12:00:00-04:00</updated>
|
||
</entry>
|
||
|
||
<entry>
|
||
<title>Goodbye Twitter, Hello Blog!</title>
|
||
<id>https://www.mcmillen.dev/blog/20221107-goodbye-twitter.html</id>
|
||
<link rel="alternate" href="https://www.mcmillen.dev/blog/20221107-goodbye-twitter.html"/>
|
||
<content type="html">
|
||
<![CDATA[
|
||
<h1 id="goodbye-twitter-hello-blog">Goodbye Twitter, Hello Blog!</h1>
|
||
<p><em>Posted 2022-11-07.</em></p>
|
||
<p>It’s quickly become clear that the acquisition of Twitter by Elon Musk means the end of the site as I’ve known it since 2008. Instead I’m going to collect all the stuff I <em>would</em> have tweeted about & post it to this blog roughly once a week. If you have an RSS reader you can <a href="https://mcmillen.dev/feed.atom">follow along there</a>. I’ve also created a <a href="https://www.reddit.com/r/mcmillen">subreddit</a> where I’ll link to each post, so that folks can follow via Reddit if they prefer, and also comment on what I’ve written. I’d also appreciate comments over <a href="mailto:email@example.com">email</a>!</p>
|
||
<p>My goal is to post <strong>fewer things, more thoughtfully, to a smaller audience</strong> for a while, and see how that goes.</p>
|
||
<h2 id="deleting-your-twitter-responsibly">Deleting your Twitter responsibly</h2>
|
||
<p>I’d figured Elon Musk’s “free speech idealism” was code for a slow regression back to a Nazi-sympathetic swamp of toxicity, but I hadn’t quite expected him to <em>immediately</em> light user trust on fire. Musk seems to be determined to squeeze every drop of revenue from the site in order to make up for his foolhardy acquisition, so I think it’s only a matter of time before he starts selling user data to third-party companies.</p>
|
||
<p>So instead of just deleting my Twitter account wholesale, I’ve <a href="https://help.twitter.com/en/managing-your-account/how-to-download-your-twitter-archive">downloaded my Twitter archive</a> and set up <a href="https://tweetdeleter.com/">TweetDeleter</a> to delete almost all my data, including Tweets, Retweets, and Likes. Right now I have it set up to automatically delete anything older than 2 days old. I expect that after a month passes I’ll be posting to Twitter a lot less (if at all) and will just manually remove old content then.</p>
|
||
<p>I’m intending to keep my Twitter account registered and to occasionally log in, rather than deleting it entirely. That way I can maintain a pointer to this blog in my bio, and not have to worry about someone impersonating me if Twitter someday decides to reap & reissue old usernames.</p>
|
||
<p>For a more nuanced approach to deleting your Twitter content than “burning all of it as quickly as possible”, consider <a href="https://twitter.com/ryanqnorth/status/1418543853167071232">this 2021 thread from Ryan North</a>.</p>
|
||
<h2 id="hows-your-blog-work">How’s your blog work?</h2>
|
||
<p>This site is authored with Markdown. I have a short Python script that converts the entire thing to static HTML, so I can still author posts in my favorite text editor (<a href="https://www.sublimetext.com/3">Sublime Text 3</a>) or even draft things on-the-go with a Markdown app like <a href="https://joplinapp.org/">Joplin</a>. This means I’m not reliant on anyone else’s fancy GUI editor, and also means it’s pretty future-proof in case I ever <em>do</em> want to move my content to another site for some reason.</p>
|
||
<h2 id="why-not-substack-medium-etc">Why not Substack / Medium (etc)?</h2>
|
||
<p>Many people have moved to tools like Substack & Medium for writing longer-form articles. I’ve definitely considered those sites as well, but it’s hard to be sure that they won’t <em>also</em> go “boom” due to reasons entirely outside my control sometime in the next few years. There’s already something like a 15-year-long gap in my online presence due to Google+ and Twitter melting down in various ways. I’d feel like a bit of a sucker to be relying on anyone other than myself to host my content, at this point.</p>
|
||
<h2 id="why-not-mastodon">Why not Mastodon?</h2>
|
||
<p><a href="https://joinmastodon.org/">Mastodon</a> seems like the most obvious choice as a Twitter replacement, but I’m not actually sure that I <em>want</em> a Twitter replacement. As I said up top, I’m gonna try posting fewer things, more thoughtfully. A new app that lets me rapidly tap out hot takes isn’t exactly a good way of supporting that.</p>
|
||
<p>I might eventually make a Mastodon account to <em>follow</em> other folks who’ve departed Twitter, so I’m not counting out the idea entirely. :)</p>
|
||
<h2 id="comments">Comments?</h2>
|
||
<p>Chime in on <a href="https://www.reddit.com/r/mcmillen/comments/yoyvc8/goodbye_twitter_hello_blog/">this article’s Reddit thread</a> or send me an <a href="mailto:email@example.com">email</a>.</p>
|
||
]]>
|
||
</content>
|
||
<updated>2022-11-07T12:00:00-04:00</updated>
|
||
</entry>
|
||
|
||
<entry>
|
||
<title>Japan 2023 Photos</title>
|
||
<id>https://www.mcmillen.dev/blog/20231108-japan-photos.html</id>
|
||
<link rel="alternate" href="https://www.mcmillen.dev/blog/20231108-japan-photos.html"/>
|
||
<content type="html">
|
||
<![CDATA[
|
||
<h1 id="japan-2023-photos">Japan 2023 Photos</h1>
|
||
<p><em>Posted 2023-11-08.</em></p>
|
||
<p>I took a trip to Japan in September & October 2023 along with 7 friends. The trip was over a month long and I walked over a half-million steps.</p>
|
||
<p>Here’s some links to all the photos I took on that trip! More details (and probably a longer blog post) to come later…</p>
|
||
<h2 id="tokyo">Tokyo</h2>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-09-23/n-NDxZGT">September 23</a>: arrival in Narita & Okubo.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-09-24/n-Fz2w3H">September 24</a>: Shinjuku, Yoyogi Park, Meiji Shrine.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-09-25/n-kVdSc4/">September 25</a>: Ginza, Shinjuku.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-09-26/n-MXJPXj/">September 26</a>: Imperial Palace & East Gardens, Shinjuku at night.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-09-27/n-NGZzWF/">September 27</a>: Hakone day trip.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-09-28/n-cwv75Q/">September 28</a>: Shinjuku & Kichijoji.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-09-29/n-PLtH8N/">September 29</a>: Gotanda, Hokkaido Food Fest, Setagaya, and Sangenjaya.</p>
|
||
<h2 id="nikko">Nikko</h2>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-09-30/n-SGLjvj/">September 30</a>: Nikko National Park, Toshogu.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-01/n-8WPcq2">October 1</a>: Senjogahara.</p>
|
||
<h2 id="tokyo_1">Tokyo</h2>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-02/n-G4gsQk">October 2</a>: Hamarikyu Gardens.</p>
|
||
<h2 id="kyoto">Kyoto</h2>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-03/n-2cJMwB/">October 3</a>: Gion.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-04/n-Qw4NsK/">October 4</a> Golden Temple, Katsurazaka.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-05/n-svvcmr/">October 5</a>: Fushimi Inari, Kyoto Station.</p>
|
||
<h2 id="tokyo_2">Tokyo</h2>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-06/n-mvJ2sT/">October 6</a>: Mt. Takao.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-07/n-LR267c/">October 7</a>: Sushi & Akihabara.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-08/n-j7pBmH/">October 8</a>: Akihabara.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-09/n-k24VQR/">October 9</a>: Eorzea Cafe.</p>
|
||
<h2 id="hakodate">Hakodate</h2>
|
||
<p>October 10th doesn’t exist yet :O (travel day, I only have cell phone photos)</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-11/n-zC6Vpb/">October 11</a>: Hakodate morning market, harborside birdwatching, Mt. Hakodate.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-12/n-qkMHg8/">October 12</a>: Hakodate seaside, Upopoy Ainu village (Shiraoi), Sapporo.</p>
|
||
<h2 id="sapporo">Sapporo</h2>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-13/n-FwgzP7/">October 13</a>: Odori Park.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-14/n-5b95M7/">October 14</a>: Lake Shikotsu National Park.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-15/n-BzM5XX/">October 15</a>: Hokkaido Botanical Garden, Susukino.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-16/n-v5vT5P/">October 16</a>: Maruyama Park & Mt. Maruyama, Susukino.</p>
|
||
<h2 id="morioka">Morioka</h2>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-17/n-GqcS27/">October 17</a>: Morioka Ekimae at night.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-18/n-wzT5DB/">October 18</a>: Morioka-jo Castle Site Park, Mt. Iwate.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-19/n-M6hNrH/">October 19</a>: Kitakami River walk, sake distillery tour.</p>
|
||
<h2 id="tokyo_3">Tokyo</h2>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-20/n-rZBRWs/">October 20</a>: Nippori, Ueno Park, Kiyosumi Shirakawa Teien, Okubo.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-21/n-r7npdT/">October 21</a>: Ginza, Monster Hunter bar.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-22/n-hHRmGs/">October 22</a>: Kichijoji & Shimokitazawa.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-23/n-HhQfDJ/">October 23</a>: Yokohama — Ramen Museum & seaside park.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-24/n-twJPn9/">October 24</a>: Sensoji temple, Eorzea Cafe round two, Shinjuku Gyoen, & Tokyo Metropolitan Government Building.</p>
|
||
<p><a href="https://pix.mcmillen.dev/Japan-2023/2023-10-25/n-7pgPSD/">October 25</a>: Nippori & Arakawa Nature Park, trip home :(</p>
|
||
]]>
|
||
</content>
|
||
<updated>2023-11-08T12:00:00-04:00</updated>
|
||
</entry>
|
||
|
||
</feed>
|