カメヲラボ

主にプログラミングとお勉強全般について書いてます

プログラミング言語★裏選挙―愛の風車【解説】

本稿はCodeIQで2014年10月2日~2014年10月23日に出題されたプログラミング言語★裏選挙―愛の風車」 という問題の結果・解説です。

問題文はこちら

プログラミング言語★裏選挙―愛の風車【解説】

f:id:Ozy:20180428145747j:plain

順位表

最終的な順位は、次のようになりました。

順位 有効得票数 言語
1位 17 JavaScript
2位 16 PHP
3位 14 Perl
4位 12 Python
5位 9 Scala
6位 7 Bash, C++11
8位 6 AWK
9位 4 Go, VB.NET
11位 3 D
12位 2 Clojure, Erlang, Octave, R, Scheme, SQL, Whitespace
19位 1 A, Brainfuck, Common Lisp, Egison, F#, Fortran, Groovy, Lua, Objective-C, Pascal, なでしこ

僅差でJavaScriptが1位となりました! PHPは惜しかったですね。 Perlは最近挑戦者数が減ってきているように感じていましたが、まだまだ人気ですね。 Pythonは本選に残る人気でしたから、今回はあえて使用しなかった方が多かったのかもしれませんが、なんとか4位に滑り込みました。 5位以下では、Scalaが健闘しました。 総選挙のときも、関数型言語ではHaskellに次ぐ人気で、今後が楽しみですね。 BashAWKが結構上位に来たのも興味深いです。 C++11は意外に伸びませんでした。 問題の内容が簡単でしたから、わざわざ使おうという気になりにくかったのかもしれませんね。 総選挙のときと違いideone縛りを無くしたこともあってか、言語の種類は29種と非常に多かったです。

解説と優秀・おもしろ解答

素敵な風車の描き方(1)

風車なんだから、くるくる回転してナンボでしょ!ということで、羽の左上だけ生成し、それを回転させることで風車全体を作るようにするという方法が考えられます。

【Muさんのコード(Octave)】

for i = 5:7
  a = char(tril(ones(i))*('*'-' ')+' ');
  b = horzcat(a, rot90(a,-1));
  disp( vertcat(b,rot90(b,2)));
endfor

Octaveという言語は行列の操作が簡単にできるのが特徴で、まずa = …の部分で三角行列を作ることによって羽の左上を生成しています。 次の行で、最初に作った羽を右に90度回転させ、最初に作った羽と連結しています。 rot90関数の第2引数は回転させる回数で、-1ということは右に1回回転させるという意味になります。 これで風車の上半分ができました。これをさらに180度回転させると下半分もできるので、簡単に風車が完成しました。美しいですね。

素敵な風車の描き方(2)

羽を1枚だけ生成し、それを回転させるためには配列を用意しなければなりません。 配列を使わずに、2重ループでまとめて描いてしまうという方法もあります。 羽が4枚ありますから、xy平面上を4分割し、各領域で’*’を表示するか’ ‘を表示するかの条件分岐をすると、8つも分岐しなければなりません。 それはそれで何をやっているのかわかりやすいという利点もありますが、if文をたくさん並べることなく、簡潔でカッコよく書きたい!と思うのも人情です。 というわけで、「コイツはカッコいいぜ!!」と私が感じたコードを紹介しておきます。 【ぶんさんのコード(VB.NET)】

Imports System

Public Class Test

    Public Shared Sub Main() 

        Dim n,x,y As Integer

        For n = 5 To 7

            For x= -2*n+1 To 2*n-1 Step 2

                For y= -2*n+1 To 2*n-1 Step 2

                    System.Console.Write(IIF((x/y<=y/x),"*"," "))

                Next y

                System.Console.Writeline("")

            Next x

        Next n

        End

    End Sub

End Class

【haruyaさんのコード(Python)】

import sys
for a in [9, 11, 13]:
    for y in range(a, -a - 2, -2):
        for x in range(-a, a + 2, 2):
            sys.stdout.write(' ' if ((x + y * 1j) ** 4).imag > 0 else '*')
        print

両者の共通点として、羽のサイズNの風車を書く際、単純に0..2N-1の範囲でループせずに、-2N+1..2N+1の範囲で2ステップしているコードになっています。 これは、xy平面を4象限に分割しつつ、座標の絶対値を対称にするためです。 座標が0になってしまう箇所を避けることもでき、余計な分岐を無くすことができます。 わかりやすいように、N=5の場合を図にしてみましょう。 ぶんさんのコードで、(x/y<=y/x)となっている部分を調べるために、各座標に対応する(x/y – y/x)を計算してみます。

f:id:Ozy:20180428150252p:plain

上記のように羽の部分(‘*’を表示する部分)の値は0以下になり、1つの不等式できちんと判別できています。 haruyaさんのコードでは、複素数平面を利用していて、複素数(x + yi)を4乗すると、虚部はxy(y2 – x2)という式になります。 これは、先程の(x/y – y/x)という式に(xy)2を掛けたものと一致するので、判定方法としては同じです(ただし、両者のコードはx, yの向きが異なっていますので注意してください)。 風車の左上と右下・右上と左下では羽の向きを判定する式が共通化でき、2領域の区別はx, y座標を乗除したときの正負で判定できるというところがポイントです。

おもしろ解答

個性的な解答が多数ありましたが、すべてをご紹介することはできませんので、130のコードのうち特に印象に残ったものをご紹介いたします。

【hcmiyaさんのコード(Bash)】

for i in {5..7}
do 
    hane1="$(head -c$i /dev/zero |tr '\0' ' ' |sed ':j;H;s/ /*/;t j;x'|tail -n+3)"
    hane2="$hane1
$(rev <<<"$hane1")"
    rev <<<"$hane2" |tac |paste -d '' <(cat <<<"$hane2") -
done

sedで羽を1枚作り、tacやrevで回転させるタイプのコードで、様々なコマンドを上手に使いこなしている感と、それによって非常に簡潔なコードに仕上がっている点に惹かれました。

カニ戯(ryさんのコード(Bash)】

for x in 5 6 7
do
    echo $x | \
    sed \
     -e ':0;x;s/^.*$/&\*/;x;y/987654321/876543210/;/0/!b0' \
     -e 's/.*//;g;s/^./@/;y/@*/* /;G;s/\n//;h;s/.*//;x;h' \
     -e ':1;/^\** *$/!{;s/\* /\*\*/' \
     -e 's/^\(.*\)\(\*\*\)\( *\)$/\1\* \3/;H;b1}' \
     -e 's/\(^\**\)\( *$\)/\2\1/;H;s/ \*\*\*/\*\* \*/;H' \
     -e ':2;/^\*/!{;s/ \*/  /2;s/ \*/\*\*/;H;b2};g'
done

こちらはsedだけですべての羽を作っているコードで、パッと見た感じだけでお腹いっぱいなのですが、コードを順に追っていくとsedプログラミングがよくわかります。 sedゴルフに興味があるけどよくわからないという方は、これで勉強してみてはいかがですか? パターンスペース・ホールドスペースという2つのバッファをうまく使うのがポイントです。

【みけCATさんのコード(A言語)】

AtSnc:c\ArSnc:+*n++n-1-%cn/cn\AfSnc:+*n/cn++n-1-%cn\AgSnc:?>%cn/cnS \\:S*\\:\
Ri3PAn+5iRy*2nPRx*2nPAv%ynPAu%xnOFgnF?<xnt:r:nF?<ynt:f:n+*nvuOMc10

なんじゃこら!というか、A言語って何ですか!という感じですが、 https://github.com/snukent/a から処理系が手に入るようです。 素敵な風車の描き方(2)で解説したアルゴリズムを使って書くと、

Rn3PAk+n5Ri*k2PAy+1*2+i-kPRj*k2PAx+1*2+j-kO?</xy/yxS \:S*\:OMc10

のような感じで、かなり短く書くことができます。興味のある方は試してみてください。

【KTazakikさんのコード(Brainfuck)】

[-]>[-]+++++++[-<++++++>]<>[-]>[-]++++++++[-<++++>]<>[-]++++++++++<<
 .>....<.....>>.<<
 ..>...<....>.>.<<
 ...>..<...>..>.<<
 ....>.<..>...>.<<
 .....><.>....>.<<
>....<.><.....>>.<<
>...<..>.<....>>.<<
>..<...>..<...>>.<<
>.<....>...<..>>.<<
><.....>....<.>>.<<
 .>.....<......>>.<<
 ..>....<.....>.>.<<
 ...>...<....>..>.<<
 ....>..<...>...>.<<
 .....>.<..>....>.<<
 ......><.>.....>.<<
>.....<.><......>>.<<
>....<..>.<.....>>.<<
>...<...>..<....>>.<<
>..<....>...<...>>.<<
>.<.....>....<..>>.<<
><......>.....<.>>.<<
 .>......<.......>>.<<
 ..>.....<......>.>.<<
 ...>....<.....>..>.<<
 ....>...<....>...>.<<
 .....>..<...>....>.<<
 ......>.<..>.....>.<<
 .......><.>......>.<<
>......<.><.......>>.<<
>.....<..>.<......>>.<<
>....<...>..<.....>>.<<
>...<....>...<....>>.<<
>..<.....>....<...>>.<<
>.<......>.....<..>>.<<
><.......>......<.>>.<<

ソースコード自体が風車っぽくてステキですね。 ‘\n’, ‘*’, ‘ ‘をセットしただけでほぼベタ書きじゃないかー!という突込みはナシで^^; 他にも面白いコード(WhitespaceとかSQLとか!!)をたくさんいただきましたが、これくらいにしておきます。

最後に

単純な問題ではありましたがどれも個性的で、読んでいて楽しいコードがたくさんありました。 同時に、ものすごく似た感じのコードもあり、それらを大量に見た感想を書いておきます。 まず、次のようなコードをご覧ください。

void kazaguruma( int n )
{
  for (int y = 0; y < 2 * n; ++y) {
    for (int x = 0; x <  2 * n; ++x) {
      if ( y < n ) {
        if ( x < n ) {
          if ( y >= x ) {
            printf( "*" );
          } else {
            printf( " " );
          }
        } else {
          if ( y + x < 2 * n ) {
            printf( "*" );
          } else {
            printf( " " );
          }
        }
      } else {
        if ( x < n ) {
          if ( y + x >= 2 * n - 1 ) {
            printf( "*" );
          } else {
            printf( " " );
          }
        } else {
          if ( y <= x ) {
            printf( "*" );
          } else {
            printf( " " );
          }
        }
      }
    }
    printf( "\n" );
  }
}

羽のサイズnを受け取り、左上・右上・左下・右下の順で分岐して羽を書くコードです。 好き嫌いで言えば、いかにも冗長な感じで、私はあまり好きなタイプのコードではありません。 ただ、同じようなコードをたくさん見ていると、そんなに悪いコードでもないように思えてきます。 このような形が書かれていれば、コードの中身を読まずともその内容がすぐにわかるからです。 平面的に文字を描画する場合、“左から右に向かって”、“上から下に向かって”、という順序は最も素直で、よほど捻くれたプログラマでない限りはこの順序で条件分岐も書くでしょう。

つまり、このようなコードはシルエットを一目見ただけで、どのようなコードかが理解できます。 私は子どもの頃から視力が弱いので、ぼんやりとモノを見る癖がついてしまっていますし、そういうコードの見方をしている人にとってはこれ以上わかりやすい記述はありません。 それでは自分が普段このようなコードを書くのかというと、書きません。 一応、頭の中では上記のようなイメージ(文字通り画像のような“絵”)がパッと浮かびます。 次に何をするのかというと、その絵を見ながら、文句を言うわけです。 「printfが9個もあるじゃないか!!!」 それで、次のようなコードに書き替えてみたりします。

void draw( bool a )
{
  printf( a ? "*" : " " );
}

void kazaguruma( int n )
{
  for (int y = 0; y < 2 * n; ++y) {
    for (int x = 0; x < 2 * n; ++x) {
      if ( y < n ) {
        if ( x < n ) {
          draw( y >= x );
        } else {
          draw( y + x < 2 * n );
        }
      } else {
        if ( x < n ) {
          draw( y + x >= 2 * n - 1 );
        } else {
          draw( y <= x );
        }
      }
    }
    printf("\n");
  }
}

改行用のprintfは仕方なしということで、条件式を引数に、drawという関数を新たに作りました。 8つのprintf関数が1つになったことと、ifブロックのネストが減ってだいぶスッキリしました。 まあ、これくらいのコード量ならよいよね?
しかし、drawという短い記述の関数を作ってしまったが故に、短くしたはずのkazaguruma関数が比較的冗長に見えてきてしまいました。 それなら、ネストの一番内側にある、左右の条件式の部分をもうちょっとまとめておこうか。

void draw( bool a )
{
  printf( a ? "*" : " " );
}

void drawLR( int n, int x, bool l, bool r )
{
  draw( x < n ? l : r );
}

void kazaguruma(int n)
{
  for (int y = 0; y < 2 * n; ++y) {
    for (int x = 0; x < 2 * n; ++x) {
      if ( y < n ) {
        drawLR( n, x, y >= x, y + x < 2 * n );
      } else {
        drawLR( n, x, y + x >= 2 * n - 1, y <= x );
      }
    }
    printf("\n");
  }
}

なんだかコードもスッキリしたし、関数化して細かく切り分けとかしてコンパクトに書ければ「ステキ!抱いて!!」ってなるんじゃね? じゃあ上下段も思い切って、切り出そうかな。

void draw( bool a )
{
  printf( a ? "*" : " " );
}

void drawLR( int n, int x, bool l, bool r )
{
  draw( x < n ? l : r );
}

void drawUDLR( int n, int y, int x, bool ul, bool ur, bool dl, bool dr )
{
  y < n ? drawLR( n, x, ul, ur ) : drawLR( n, x, dl, dr );
}

void kazaguruma(int n)
{
  for (int y=0; y<2*n; ++y) {
    for (int x=0; x<2*n; ++x) {
      drawUDLR( n, y, x, y >= x, y + x < 2 * n, y + x >= 2 * n - 1, y <= x );
    }
    printf("\n");
  }
}

…なんだか横に伸びてきちゃった。ちょっと空白減らして、nもグローバルでいいかな?

int n;

void draw(bool a)
{
  printf(a ? "*" : " ");
}

void drawLR(int x, bool l, bool r)
{
  draw(x<n ? l : r);
}

void drawUDLR(int y, int x, bool ul, bool ur, bool dl, bool dr)
{
  y<n ? drawLR(x, ul, ur) : drawLR(x, dl, dr);
}

void kazaguruma()
{
  for(int y=0;y<2*n;++y){
    for(int x=0;x<2*n;++x){
      drawUDLR(y, x, y>=x, y+x<2*n, y+x>=2*n-1, y<=x);
    }
    printf("\n");
  }
}

このような思考がエスカレートするのは、いわゆる(コード)ゴルフ脳という状態で、自分でブレーキを掛けなければなりません。 この辺りまでで我慢ができれば、正常なプログラマではないかと思います。 上記のような関数への切り出しも、比較的短めのコードなので見栄えがしますが、何十万行もあるコードでこれをやっていくと、ものすごく読みにくくなってしまいます。 全体を見渡して、「まあこれくらいが自然かな」という感覚をある程度持っておくと、他人にコードを提供したりする場合に、相手にとって読みやすいコードを書くのが上手になります。

ベテランのプログラマと、若いプログラマ、もちろん十把一絡げに考えることはできないのですが、ひとつの傾向として、ベテランプログラマで「この人は仕事できるな」って感じるの人は、シチュエーションに合わせたコーディングができるという特長があるように思います。 どのように書くかは、良く言えば美学、悪く言えば自己満足で、趣味の世界ならどこまでもこだわってよいと思います。 しかし、他人と何らかのかかわりを持って、仕事として書くなら、自分より上級のプログラマに迷惑をかけることなく、なおかつ自分よりスキルの劣るプログラマを包み込めるようなバランス感覚を身に付けると、仕事もしやすくなるのではないでしょうか。

…と、一応転職向けのサイトということで無理やり仕事の話に結び付けましたが、ちょっと無理やり過ぎましたね。 兎も角も、ある程度コードが書けるようになったら、他人のコードをたくさん読んで、コードから書き手のスキルレベルを感じ取る経験は、きっと役に立ちます。 特に若い人は、いろんな人が書いた、いろんな言語のコードに触れてみてください。