ふるつき

私は素直に思ったことを書いてるけど、上から目線だって言われる

懺悔

なんだか急に、このことを書かなければならないと思ったので記しておく。僕は今までこのことを語ってこなかったし、なかったことにしていた。だけどそれではいけないような気がして、せめて衆目にさらすことにした。彼女には申し訳ないと思う。


僕は小学五年生だった。小学五年生の僕は「頭はいいけれど、変なやつ」で通っていたと思う。少なくとも僕はそう自分のことを見ていた。

ある時期、一緒の班になった女の子とよく喋るようになった。彼女は少し背が低かった。目がくりくりと大きくて少しあひる口だった。ドナルドダックが好きで、筆箱のストラップはドナルドダックとデイジーダックだった。髪の毛がまっくろくて、ふわりとカーブを描いて卵のような輪郭を作っていたと思う。ギリシャの地名と同じ三文字の名前で、漢字がきれいに当たっていてその響きは好きだった。明るくて、色んな人と喋る子だった。勉強はあまりできなかった気がする。

三学期の中頃だったんだと思う。ある日の放課後の帰り際、その時隣の席だった女の子に手紙を渡された。脇道にそれるが、その女の子は「ほえ〜」とかそういうことを真顔で言ってのける「不思議ちゃん」だった。渡された手紙は彼女からだった。その場で読んで、もう一度目は、音読した。なんと書いてあったか、具体的なことは忘れてしまったが、「好きです」か「付き合ってください」「返事は三学期が終わるまでにお願いします」に相当する内容が、彼女らしい文字、文章で書かれていた。これも確かな文字列は思い出せないが「ドナルド好きより」で結んであった気がする。

音読なんてしてしまったものだから、その場にいた人はそのことを知った。何人いたか全然憶えてないが、女の子が二人いて「嬉しいやろ」と言われたのは憶えている。担任もいて、口に人差し指をあてて「そういうのを人前で言わない」と諭してきたのを憶えている。それは無視したが。

帰り道が同じのクラスメイトに、手紙を見せてくれと言われて見せた。そのあと、返してもらうのを忘れたのだったか、それとも「あげる」と言ったのだったか、その手紙はそれ以降見かけていない。

学年末の大掃除の日、椅子を運んでいるときに、彼女に叩かれ「返事、よろしくね」と言われた。僕は曖昧に頷いた。

僕はその日、そのまま帰った。彼女に好意を告げられたことは嬉しかったし、彼女のことは可愛いと認識していたけど、彼女が直接告げてこなかったこと、手紙を友達経由で渡してきたことを言い訳に、僕は彼女に「YES」も「NO」も言わなかった。なかったことにしたのだ。彼女と付き合って、他の可愛い子、気になる子と付き合うチャンスがなくなってしまうのを惜しいと感じていたような気もするし、付き合うってなにかわかってなかった、怖かったという思いがあった気もする。

中学校に上がって、彼女と同じクラスになったけど、彼女はこちらに話しかけてくることはなかったし、何にもないような顔をしていた。僕は最初に顔を合わせたときには何を言われるものかと心配していたが、その後はそのことは忘れてしまった。

僕は生徒会に入っていて、その時はたまたま、僕と、同じクラスの女の子と、女子の先輩が二人で作業をしていた。先輩のうち一人は生徒会長だったが、その人が僕達に恋話はないのかと話をふってきた。同じクラスの子はもちろん僕が何をしたか知っていて、そのことを先輩二人に話した。そのとき僕がどんな気持ちだったのか全然憶えていない。先輩らは「それはひどい」と評した。「何も言わずになかったことにしようだなんて」。全くそのとおりだと思った。僕は何か反論した気がする。あるいは「反省してますよ」と言ったのだったか。クラスメイトのその子も僕になにか鋭い言葉を言って、僕はしんどかったはずだった。その頃僕はその子のことを可愛いなと思っていて、その子が生徒会役員に立候補したから僕も立候補した、みたいなエピソードも持っていた。

これでこの話は終わりだ。その子がいまどこで何をしてるかなんて全然知らない。なんならこの話の中に出てきた人の誰一人の行方も知らない。僕は彼女に酷いことをしてしまったと思っているし、あのときに戻ったら必ず返事をしたいと思っている。もしあのとき「YES」なり「NO」なりを告げていれば、今とは何かが違ったかもしれないとも。

幸運だったのは、僕が高専に進学して、この話を知る殆どの人と縁が切れたこと。この話を知る人の誰も、このブログを見に来るような人はいないということだ。

シェル芸でうんこつるんする

いわゆるとこのズンドコきよしです。

シェル芸botに食わせたんだけどうまく行かなくて悔しかったのでこっちに書いて溜飲を下げることにします。

やること

「う」「ん」「こ」の三文字のうちどれか一文字をランダムに流し続け*1、たまたま「うんこ」の順番で流れてきたときに、「つるん」します。

どのようにそのシェル芸を組み立てるかという話をね、していきます。

「う」「ん」「こ」を流す。

まずは無限ストリームを作りたいですね。こういうときは yes です。一文字ずつ区切って使うことを考えると yes う ん こ と空白区切りで出力してやるのが無難です。

ここまでのスクリプト

> yes う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
う ん こ
...

一文字だけランダムに選ぶ

awk でやるのが良さそうだったんですが、 randの使いやすさから perl で行きます。

-nle オプションはシェル芸人御用達で、 -n 一行ずつ処理 。 -l 改行する。 -e コマンドラインスクリプトを指定。です

-a がつくと awk モードになって $F が使えます。 $F は入力をsplitしたやつで、標準入力が 「う ん こ」だったら ('う', 'ん', 'こ') になっていることが期待できます。 というわけで配列からランダムに一つ引っ張ってきますので perl -anle 'print $F[int(rand(3))]' になりそうです。ちなみにオプション引数の順番は気をつけたほうがいいらしい。

ここまで

> yes う ん こ | perl -anle 'print $F[int(rand(3))]'
ん
う
う
う
ん
う
こ
う
こ
ん
ん
こ
ん
...

うんこ判定

perlでいきます。 -n オプションがあると行毎に処理することになりますが、 BEGIN{} END{} があるとコンテキストを持てます。 というわけで起動時に配列を作っておき一文字ずつ先頭に追加。splice で消し飛ばしつつ直近3文字を保持します。

そして、 join で結合したものが こんう と一致したら直近3つが「うんこ」になっているので、 exit してperlを終了します。いい感じだ。もちろん入力は吐き直さないとだめなので print $_ はします。

ここまで

> yes う ん こ | perl -anle 'print $F[int(rand(3))]' | perl -nle 'BEGIN{@a=()} unshift(@a,$_); splice(@a,3); print $_; if (join("", @a) eq "こんう"){ exit();} ' 
ん
う
う
こ
う
ん
こ

「つるん」する

echo つるん

> yes う ん こ | perl -anle 'print $F[int(rand(3))]' | perl -nle 'BEGIN{@a=()} unshift(@a,$_); splice(@a,3); print $_; if (join("", @a) eq "こんう"){ exit();} ' ; echo つるん
う
う
ん
こ
つるん

見栄えがわるい

つるんだけ横になっていて気持ち悪いので、うんこまでの道のりもちゃんとヨコ読みにしてあげます tr -d '\n' で改行文字を消し飛ばせるのは常識ですね。

> yes う ん こ | perl -anle 'print $F[int(rand(3))]' | perl -nle 'BEGIN{@a=()} unshift(@a,$_); splice(@a,3); print $_; if (join("", @a) eq "こんう"){ exit();} ' | tr -d '\n'; echo 'つるん'
ここうこんこううこうここうんんうんうこうんうこうここううんこつるん

あー、スッキリ……!

perl に触るのなんて初めてなので苦労しました。

追記

うんこシェル芸で右に出るものはいないと噂の @grethlen さんがすごいきれいに出してた。シェル芸って感じですねすごい

*1:流すとかトイレですね。だれうま

シェル芸botの宣伝をさせてください

 この記事はShell Script Advent Calendar の19日目に向けて書かれた記事です。参加する気はなかったんですが、ブログでシェル芸botについて言及したことがなかったのと、ふとカレンダーを見たら空いてたのとで入れてもらうことにしました。よろしくお願いします。


シェル芸botについて

 シェル芸bot ( @minyoruminyon )をご存知でしょうか。シェル芸botは、「TLに現れた #シェル芸 または #危険シェル芸 とタグのついたツイートについて、そのツイートをシェルスクリプトとしての実行を試み、正常終了すれば結果を引用でツイートする」というbotになります。「シェル芸」とはなんぞやという方は こちら の定義や #シェル芸 - Twitter Search の例などをご参照下さい。

 シェル芸botは現在 386 フォロワーを獲得しており、生まれてから 2633 のシェル芸を実行してきました。このシェル芸botを作ったのが私ふるつきで、せっかくなのでセールストークをさせていただきます。

 シェル芸botが生まれたのは 2017年の 6月であると、 シェル芸史 にあります。以前から、 base64 encoded なTweetをする界隈に身をおいていたこともあり、ある程度の「シェル芸」というワードに親しんでいた私はしかし、TLに流れてくる不可思議な記号列=シェル芸をターミナルに貼り付けて実行するのが面倒&怖かった憶えがあります。そこで、ある種のサンドボックスとして、シェル芸botを作成することにしました。もちろん、シェル芸界隈のノリの良さのようなものを目にしており、これはイケるだろうと思っていなかったわけがありませんが。

 斯くしてシェル芸botはTL上に生を受け、瞬く間にシェル芸人たちの間に広がった、はずです。はじめは不慮の停止が数度あった気がしますが、現在は安定して稼働を続けています。シェル芸botはユーザフレンドリーなbotを目指しているので、シェル芸人の皆様の活動に合わせて日々利用できるコマンドが追加されています。

@paiza_run との関係

 TL上のプログラムを実行するとくれば、まず思い浮かぶのが paiza_run です。シェル芸botと動きは丸かぶりですが、シェル芸botはどちらかといえば、「ツイートしたシェル芸が思いがけず実行され、結果が見られる」という public な向きのbot です。

 paiza_run は凍結されてしまいましたが、以前にはコラボレーションしたこともありました。

togetter.com

シェル芸bot に触れてみよう

 大したハードルもないですが、シェル芸bot に初めて触る方向けのチュートリアルです。

  1. シェル芸botをフォローしましょう
  2. シェル芸botのフォローバックを待ちましょう(手動です)
  3. echo-sd シェル芸楽しい!! #シェル芸 とツイートします

 これであなたもシェル芸人です。巷では様々なシェル芸が飛び交っていますので眺めるも解読するも創り出すも良しです。お楽しみ下さい。  

シェル芸bot のこれから

 ある程度安定してきたシェル芸botですが、これからも「あんなコマンドがほしい」「こんなコマンドを作った」が尽きることはないと思うので、ガンガンアップデートをかましていきたいです(何かのタイミングで私の目に止まれば可及的に速やかに更新致します)。そしてできれば私もシェル芸人の仲間入りを果たしたい……!

 ところでシェル芸bot さんの顔グラがないのが味気ないので募集しています。よろしくお願いします。

ふるつきにいつでも質問できるサービスができました

この記事は、 kosen14s Advent Calendar 2017(本ページをリンク)のn日目の記事です。

昨日はちげくんが、 俺的スライドデザイン変遷2017 - 鍋はすき焼きがすき をやってくれました。ちげくんはn方向に手が伸びてるのにそれぞれがちゃんとハイレベルですごいですね。でも中学生なんだって。

今日は穴埋め的にここ数日で私が作っていた theoldmoon0602BOX ができたのでご報告を致します。

theoldmoon0602BOX はこんなサイトです

f:id:Furutsuki:20171205182134p:plain

殺風景なサイトです。見た目は Cakephp3 のものをそのまま使いました。だってかっこよかったので。というかこのデザインに合わせて作り始めてしまったのであとから変更とか考えてなかったですね。

で、これは何をするかと言って、「ふるつきに対して質問を投げかけるサイト」です。そしてそれに私が答える。

質問をする

f:id:Furutsuki:20171205182345p:plain

と、ふるつきにメールが届きます

f:id:Furutsuki:20171205182413p:plain

のでふるつきが答えて、

f:id:Furutsuki:20171205182434p:plain

質問した人にもメールが届きます

f:id:Furutsuki:20171205182456p:plain

やったね。

という感じのくだらないサイトです

Webアプリケーションみたいなのをフットワーク軽く作れるようになりたいな、とか。

くだらないサービス作って自己満足したいな、とか。

そういう気持ちが集まってできました。最初はLaravelで作りたかったのですが変に大きくて泣いちゃったので、Cakephp3に手を出してみたのですが、チュートリアルやったらだいたいわかって幸せになりました。ソースコードも一応公開していて、 https://github.com/theoldmoon0602/box です。

気が向いたら、適当に質問をしてくれると嬉しいです。


おまけ

theoldmoon0602BOX は さくらのVPSの上のUbuntu 16.04 上で動いています。 h2o + php-fpm + Cakephp3 という感じなので、軽くデプロイの感じを紹介しておきます。

h2o のビルド

https://github.com/h2o/h2o/ を clone してきて、 https://h2o.examp1e.net/install.html に従うとすんなりできました。依存で bison とか ruby とか、あと暗号周りを apt-get しました。 mruby 拡張使うかな、と思って ruby も入れたのですが、結局使っていません。

php-fpm の導入

最初は apt-get install php7.0-fpm していたのですが、あとで都合が悪くなって、 ppa から拾ってきた php7.1-fpm を使っています。開発環境は Ubuntu 17.10 だったので、 7.1 の環境で composerが lock lock しちゃったからのような気がします。

h2o の設定

/etc/h2o/ を作って /etc/h2o/h2o.conf.yml にはこんな感じで書いてます(ママ)

hosts:
  "theoldmoon0602.tk:80":
    listen:
      port: 80
    paths:
      "/":
        redirect: https://theoldmoon0602.tk/
  "theoldmoon0602.tk":
    listen: &default_ssl
      port: 443
      ssl:
        key-file: /etc/letsencrypt/live/theoldmoon0602.tk/privkey.pem
        certificate-file: /etc/letsencrypt/live/theoldmoon0602.tk/fullchain.pem
        min-version: TLSv1.2
        cipher-suite: ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:DH-DSS-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DH-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DH-RSA-AES256-SHA256:DH-DSS-AES256-SHA256:ECDH-RSA-AES256-GCM-SHA384:ECDH-ECDSA-AES256-GCM-SHA384:ECDH-RSA-AES256-SHA384:ECDH-ECDSA-AES256-SHA384:AES256-GCM-SHA384:AES256-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:DH-DSS-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:DH-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DH-RSA-AES128-SHA256:DH-DSS-AES128-SHA256:ECDH-RSA-AES128-GCM-SHA256:ECDH-ECDSA-AES128-GCM-SHA256:ECDH-RSA-AES128-SHA256:ECDH-ECDSA-AES128-SHA256:AES128-GCM-SHA256:AES128-SHA256

    compress: [ gzip, br ]
    file.index: [ index.html, index.php ]
    paths:
      /:
        file.dir: /var/www/html
        header.add: "MI-ESTAS: THEOLDMOON0602"
      /bin:
        file.dir: /var/www/html/bin
        file.dirlisting: ON
      /kujokaren:
        file.dir: /var/www/kujokaren/public
      /box:
        file.dir: /var/www/box/webroot/
        redirect:
          url: /box/index.php
          internal: YES
          status: 307

  "box.theoldmoon0602.tk:80":
    listen:
      port: 80
    paths:
      /:
        redirect: https://theoldmoon0602.tk/box/
  "box.theoldmoon0602.tk":
    listen: *default_ssl
    paths:
      /:
        redirect: https://theoldmoon0602.tk/box/

user: www-data

access-log: /var/log/h2o/access-log
error-log: /var/log/h2o/error-log
pid-file: /var/run/h2o/h2o.pid

file.custom-handler:
  extension: .php
  fastcgi.connect:
    port: /var/run/php/php7.1-fpm.sock
    type: unix

cipher-suite くそ長くてじゃま。ファイル分けたいですね。まあこんな感じです。ホントは theoldmoon0602BOX を box.theoldmoon0602.tk で動かしたかったのですが、なんか url が /index.php/user/user/login とか /index.php/user/css/bake.css になって普通に辛くて泣きました。

h2o 自体は、昔は systemd で管理していたのですがうまく動いてくれなくて泣いたので、 /etc/h2o/runh2o.sh みたいなファイルを作ってます。

#!/bin/bash

start() {
    h2o -c /etc/h2o/h2o.conf.yml -m daemon
}

stop() {
    echo "STOP"
    kill $(cat /var/run/h2o/h2o.pid)
}

case $1 in
    "stop" ) stop ;;
    "restart" ) stop && start ;;
    "start" ) start ;;
    * ) start ;;
esac

php-fpm の設定

ほとんど設定する必要はないんですが、 h2o に合わせて、 /etc/php/7.1/fpm/pool.d を少しだけいじって、 走るユーザを www-data にしたり、 sock ファイルを /var/run... に動かしたりしています。

theoldmoon0602BOX のデプロイ

cd /var/www; git clone https://github.com/theoldmoon0602/box して、 composer install の後、 config/app.php を少し書き換えました。 debugfalse にしたり、 App.base' を/box` にしたり、SQLの設定をしたり、メールの設定をしたりしました。

そいから、 bin/cake migrations migrate とかするとだいたい設定完了でだいたい動いている気がします。

読書感想文「俺たちは異世界に行ったらまず真っ先に物理法則を確認する 3」

この記事は kosen14s Advent Calendar の二日目の記事です。一日目から誰もいなくてどうなることかと思っていたけど yamasy が一日目にふさわしい感じの記事を書いてくれました。ありがとう。

その一日目の記事の補遺のような感じですが、 kosen14s slack のchannelsには book なんてのもあります。 book はかつて行われた kosen14s読書会で利用されたり、おすすめの本を紹介したり、あとはkosen14sで薄い本かなにか作ろうぜという話がありましたが、あれはどこに行ったんでしょうね。やりたくなったからそのうち再燃すると思います。

さて、去る11月の30日に、「俺達は異世界に行ったらまず真っ先に物理法則を確認する」の3巻が発売されました。2巻が出たのが5月末だったと思うので、なかなか順調に刊行されているという感じですね。同日、小山高専で、作者の藍月要さんの講演があったらしいのですが、めっちゃ遠かったのでいけませんでした。とっても悲しい。

それはそれとして、3巻が出たし1巻2巻も読み返したので、全国の高専生やkosen14sの面々に届ける読書感想文を書きたいと思います。なお、文中で「高専生」を用いるとき、その代表として私が選ばれていることがありますのでそいう感じで。

タイトルで語るに落ちていますが、この小説は「高専生が異世界に行ってしまったら」という夢の作品です。大山高専のロボコペチームは、迫る全国大会に向けて夜を徹して作業に打ち込み、寝落ちて気がついたら異世界にいます。もちろん彼らが何をするかと言って、「まず真っ先に物理法則を確認する」のですが。

彼らは紛うかたなき高専生です。それもすこぶる優秀な。一体どこの高専生が異世界に飛ばされて重力加速度を調べるっていうんだ。俺もそういう高専生になりたかったかもしれない。

話の主軸には、高専生なのにコミュ力お化けの雨ケ谷幹人、その妹でお化け魔力持ちの雨ケ谷咲、ロボコペチームのリーダで雨ケ谷兄妹と仲良しの中久喜照治、コミュ障を極めた美少女天才プログラマ&ハッカ三峯魅衣、異世界で出会った美少女特級冒険者ザザ・ビラレッリなどがいますが、(咲とザザを除いて)みんな高専生です。おかしいっていう自覚がありながらおかしいに振り切ってるとことか、無限に効率もとめちゃったり運動がからきしだったりとか、もうめっちゃ高専生です。他のチームメイトももちろん高専生で、うーん最高に馬鹿ですね。

最高にわかりやすい例が幹人の「それぞれ独立でしょ」っていう台詞かなと思います。例えば言動とかですが、そういうこと思ったりしますよね? あいつは気に食わないけど言ってることは正しい、みたいな。

そういう高専生が異世界に行ったら……。もちろんモノを作って、プログラムして無双するわけですが、それを追いかけるのが、すっごい楽しいですね。俺でもこうする、俺ここまでの地力ないや……って自分と重ねてしまえるので、楽しい。一方で、「けーっ、天才プログラマ様は未知の言語もリバースエンジニアリングできるんですね」みたいな、高専生お得意のめっちゃこき下ろすみたいなのもしますが。とにかく、奇特な「高専生」という生き物が異世界で蠢いてるのはそれだけでも結構なものですよ。

それを書いてる、作者の藍月要さんはすごいですね。高専って機械とか電気とか情報とか科学とか建築とかいろいろあるのに、そして学科間の交流ってそんなにないのに、結構幅広い知識を扱ってる。なによりも高専を卒業して小説家になるのがすごいし、高専を卒業してなお高専と関わっていくの本当にすごいですね。尊敬の一言しかありません。

しかも3巻までちゃんとでている……。3巻では大山高専の面々は自動生成ダンジョン、n層からなるダンジョンで24時間(こちらの時間で言えば21時間と57分ですが)毎に各階が再生成されるようなダンジョンの攻略をするんですが、高専生のあなたならどうやって取り組みますか? 私はどうだろうなぁ……諦めそうだなぁ……。攻略諦めたら落単ですね。

結局3巻の感想文にならなかったし、文章読み返しても繋がってないし、短いし、だから私は小説家にはなれないんですが、とにかく3巻もちゃんと高専生が高専生していてよかったです。しかし高専生なのに恋愛をしていて許されない。

個人的には今の所2巻が好きです。終盤めっちゃかっこいいしずびすびなくよね……。

OCaml入門 ひとりAdvent Calendar

この記事は、OCaml入門 ひとりAdventCalendar 1日目の記事ですが、そんなAdventCalendarはなくって、AdventCalendarの季節にかこつけてOCamlの勉強をしてみるだけです。こんな記事がn個もブログにはびこるのもどうかと思うけど公開しないのも勿体無いみたいな気持ちの折衷案としてこの記事に全部収める。

思い立って OCaml に触れてみることにしました。当方C言語からプログラミングの世界に入って未だに初心者をやっています。

この Advent Calendar は次のような環境でお送りします。

~> cat /etc/os-release
NAME="Ubuntu"
VERSION="17.10 (Artful Aardvark)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 17.10"
VERSION_ID="17.10"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=artful
UBUNTU_CODENAME=artful

今日の目標

今日は OCaml を取り巻くエコシステムを構築していきたいです。最後にHello worldを書いて締めます。ちなみにいまのところ7日目まで書きましたが、すでにエディタ周りの環境はVimになりました……。

opam のインストール

opamOCaml のパッケージマネージャであり、Environment Managerっぽいです。Ubuntu 17.10 では apt を使って入れられるので楽そうです。

~> sudo apt install opam -y

よくわからないけど、 opam init しておいたほうが良さそうな風潮があるので、 opam init します。

~> opam init

すると $HOME の下に .opam ディレクトリが作成されそうです。 Do you want OPAM to modify ~/.zshrc and ~/.ocamlinit? と訪ねてくるので快く y と答えておきます(n と答えても opam config setup -a で同様のことができそうというきもちになりました)。

eval `opam config env` してね。と言われるのでしておきます。

環境の準備

opam が依存しているので ocaml コンパイラは既にインストールされていそうですが、なんか分けといたほうがためになりそうなので分けます。これには opam switch を使いそうなんで opam switch list で利用可能なコンパイラの一覧を見て後、一番新しそうなのを入れます。

ここで普通にバージョン番号を指定するとそのバージョン番号の環境が作成されると思ってるんですが、 名前を指定して環境をつくることも当然できるっぽいです。

ここでは adc という名前で環境を作ることにしました。

~> opam switch install adc --alias-of 4.06.0 

まあこれはそれなりに時間がかかります。

終わると 例によって例のごとく eval `opam config env` してくれといわれるのですると、環境は adc になってます。

utop の導入

今でも ocaml と打てばREPLが起動しますが、もう少しリッチな utop を導入しておきます。 opam install utop でインストールできて慣習が生きてます。

~> opam install utop

emacsのインストール

血迷ってしまったので emacs の環境をつくっていきます。まずはインストールからやね。 apt install emacs -y です。

~> sudo apt install emacs -y

tuaregの導入

tuareg は多分、emacsocaml-mode をもたらしてくれる偉い子だとおもいます。私は「つあれぐ」って読んでますけど読み方わからん。インストールは opam を使ってやっていきます。 melpa と marmalade にもあるけど古いかもねって github が言ってます。

~> opam install tuareg

インストール後のメッセージに、「これこれこうしてくれ」という指示がありますが無視します。なぜなら、 opam user-setup install という必殺技があるからです。 これをやると、初回は必要なパッケージのインストールが行われ、後にエディタの設定ファイルをいじってくれます。

merlinの導入

merlin (私は親しみを込めて「めるりん」と呼ぶことにしました)は、 tuareg と一緒に使って、「補完」「型の表示」をしてくれそうな感じがします。

これも opam で入れましょう。

~> opam install merlin

ついでに opam user-setup install もしておきます。

company-mode の導入のための el-get の導入

company-modeemacs の補完ライブラリです。 auto-complete というのもあるみたいですが時勢がこっちに傾いているっぽさがある。

company-mode は ELPA で提供されていて、 emacs では M-xpackage-install <RET> company-mode <RET> とかでインストールできそうなのですが、ちょっとこれはあんまり好きくない感じなので、 ついでということでemacsのパッケージマネージャの一つ el-get を使ってみることにしました。 el-get を選んだ理由はなんとなくです。

というわけで、 el-get をインストールしていきます。

el-get のインストールをするために emacs の設定ファイルであるところの ~/.emacs を編集していきます。user-setup が作ってくれた ~/.emacs が転がっていたので、これをいじります。いろいろ既に書いてあって触りたくない感じなので、これの上の方に必要な設定を書いていきます。

とはいえ、 el-get の README から引っ張ってくるだけなのでかんたんです。

次の行を足しておきました。

(add-to-list 'load-path "~/.emacs.d/el-get/el-get")

(unless (require 'el-get nil 'noerror)
  (with-current-buffer
      (url-retrieve-synchronously
       "https://raw.githubusercontent.com/dimitri/el-get/master/el-get-install.el")
    (goto-char (point-max))
    (eval-print-last-sexp)))

(add-to-list 'el-get-recipe-path "~/.emacs.d/el-get-user/recipes")
(el-get 'sync)

この状態で、 C-x C-s で保存して、 M-x eval-buffer としてあげると、勝手に el-get をインストールしてくれます。そんなきがする。

これが終わったら↑の下にもう一行追加します。 company-mode のインストールです。

(el-get-bundle company-mode/company-mode)

やはりこれも保存して、 eval-buffer します。さらに、もう一文、company-modeを自動で有効化する設定を追加します。

(add-hook 'after-init-hook 'global-company-mode)

で、 実は company-mode を入れたのは merlin のためなので、 merlin と連携させます。 ここ にあったコードを ~/.emacs に足します。

; Make company aware of merlin
(with-eval-after-load 'company
 (add-to-list 'company-backends 'merlin-company-backend))
; Enable company on merlin managed buffers
; (add-hook 'merlin-mode-hook 'company-mode)
; Or enable it globally:
; (add-hook 'after-init-hook 'global-company-mode)

hook は既に行っているのでコメントアウトしておきました。

これで多分設定が完了しました。

Hello worldを書く

C-x f して適当な hello.ml ファイルを開きます。モード表示が (Tuareg utop Merlin company) になっているといい感じです。

はじめは Printf.printfHello Worldしていたのですが、 kosen14s の面々に、「標準は print_string じゃないかなあ」という指摘を受けたので、 print_string で書きます。

let () =
  print_string "Hello World\n"

print_string だと恩恵が感じられなくて寂しいですが、 Printf. まで打つと、補完の候補が表示されると思います。うれしいね。

実行する

実は main を実行するようなコマンドはないので、二行目を選択して、 C-x C-r などしましょう。これは utop-eval-region のショートカットで、 utop repl が開いて、選択した部分を実行して結果を返してくれます。

utop[0]>   print_string "Hello World\n";;
Hello World
- : unit = ()

Hello World ができました。

やる気次第で、 2日目につづきます。

OCaml入門 ひとりAdventCalendar 2日目

昨日が楽しかったので今日もやります。環境構築って一番楽しい時間だと思うのね。

今日の目標

FizzBuzz を書きたい

自然な感情が生まれました。

試しにFizzBuzzを書いてみると、こうなりそうです。

let fizzbuzz = function
  |x when x mod 15 == 0 -> "FizzBuzz"
  |x when x mod 3 == 0 -> "Fizz"
  |x when x mod 5 == 0 -> "Buzz"
  |x -> string_of_int x

let main =
  for i=1 to 100 do
    print_string ((fizzbuzz i) ^ "\n")
  done

雰囲気で読んでください。 ^ は文字列の結合を行う(たぶん)関数です。 Pervasives といういつでも読み込まれている標準のモジュールで定義されてたはず。ぺるばしぶす。

けどまあ、forを使ってるのがOCamlっぽくない気がするので、forみたいな感じでリストを返す関数 range を作ってみます。

let fizzbuzz = function
  |x when x mod 15 == 0 -> "FizzBuzz"
  |x when x mod 3 == 0 -> "Fizz"
  |x when x mod 5 == 0 -> "Buzz"
  |x -> string_of_int x

let rec range a b =
  if a > b then []
  else a :: range (a+1) b


let main =
  List.iter (fun x -> (print_string ((fizzbuzz x)^"\n"))) (range 1 100)

書きましたけど、 List.iter に渡してる関数が汚いですね……。

こういうときは関数合成を使ってあげるのがいい気がするんですが、OCamlって関数合成の演算子ないんですね。なんでだ。

ということで定義します。F# にならって、記号 >> を使います。

let (>>) f g x = g (f x)

中置記号は () でくくって let してあげて、これを使うとFizzBuzzがもうちょっときれいになるはず。

let fizzbuzz = function
  |x when x mod 15 == 0 -> "FizzBuzz"
  |x when x mod 3 == 0 -> "Fizz"
  |x when x mod 5 == 0 -> "Buzz"
  |x -> string_of_int x

let rec range a b =
  if a > b then []
  else a :: range (a+1) b

let (>>) f g x =
  g (f x)

let print_string_line s = print_string (s ^ "\n")

let main =
  List.iter (fizzbuzz >> print_string_line) (range 1 100)

print_string して改行もする print_string_line つくっちゃった。まあいいよね。 (fizzbuzz >> (fun x -> x ^ "\n") >> print_string) って書いても同じことになりそう。あとで見たら、 print_endline という関数がありましたね……。

ちなみに >> が三引数取りそうなのに二引数でもエラーになってないのは、カリー化というやつです。僕はなんとなく知ってた。

OCaml入門 ひとりAdventCalendar 3日目

飽きてきた。ちなみに 7日目までしかやってないので。

今日の目標

cat を書きたい。にゃーん。

出来心で man cat したら普通にオプションとかあってびっくりしてる、ます。

オプションの実装はちょっと面倒なのでやりません。というか cat の実装くらい知っててもいいかもなぁという気持ちが強くなってきたぞ。

適当に読んだ一行を吐く

こういうのは調べるだけでできます。

let main =
  print_endline @@ read_line()

emacs 上の utop で入力する方法がわからなかったので ocaml cat.ml とかします。 @@ は 開き括弧のようなものです。 read_line は引数を持たないので、呼び出すときには () をつけるらしいです。

いつまでも読んで吐く

let main =
  while true do
    print_endline @@ read_line()
  done

なんだか当然のように while があって、これで無限に読んでは吐けます。ただ、 EOF に達すると End_of_file 例外を吐きます。

ので、例外処理をする

let main =
  try
    while true do
      print_endline @@ read_line()
    done
  with End_of_file -> ()

try-with で例外を捕まえられそうです。捕まえたあとは何もしないので ()

ファイルを読む

次はファイルを読みたいので読みます。ファイルを開くのは open_in で、これは当然のように読み込み用途でファイルを開きます。

これを let で束縛して、 input_line やなんかの引数として使えそうです。例えばこう。

let main =
  let chan = open_in "cat.ml" in
  try
    while true do
      print_endline @@ (input_line chan)
    done
  with End_of_file -> ()

関数にまとめる

あんまりきれいじゃない気がするけど echo_all とか適当な名前をつけてまとめます。

let echo_all chan =
  try
    while true do
      print_endline @@ (input_line chan)
    done
  with End_of_file -> ()

let main =
  let chan = open_in "cat.ml" in
  echo_all chan

こうすると echo_allstdinを渡すのもできるようになってくれる。

引数をとってcatする

コマンドライン引数は Sys.argv で取れそう。これを for で回します。 iter できれいにかけそうなの思いつかなかったので。

let echo_all chan =
  try
    while true do
      print_endline @@ (input_line chan)
    done
  with End_of_file -> ()

let main =
  for i = 1 to Array.length Sys.argv - 1 do
    let chan = open_in Sys.argv.(i) in
    echo_all chan;
    close_in chan
  done

二文以上書くときは ; でつなげるとか普通は知らないですよね。怒られた。配列のランダムアクセスが .(n) とか知らないですよね。困った。

完成させる

はい。

let echo_all chan =
  try
    while true do
      print_endline @@ (input_line chan)
    done
  with End_of_file -> ()

let main =
  if Array.length Sys.argv == 1
  then
    echo_all stdin
  else
    for i = 1 to Array.length Sys.argv - 1 do
      if Sys.argv.(i) = "-" then
        echo_all stdin
      else
        let chan = open_in Sys.argv.(i) in
        echo_all chan;
        close_in chan
    done

こう、

let chan = match Sys.argv.(i) with
    |"-" -> stdin
    |f -> open_in f

みたいに書いて echo_all chan 一個にまとめたかったんですが、 match 式と let in をうまくまとめる方法がわからなかったのと stdinclose_in すると怒られるのとで諦めました。

実行可能な形式にする

ocamlcバイトコードへのコンパイラで、 ocamlopt はネイティブコードへのコンパイラっぽいです。

ocamlopt cat.ml -o cat とかすると、 cat が生成されて、いい感じに動いてくれそうですね。

OCaml入門 ひとりAdventCalendar 4日目

惰性がもーちょい続いてほしい。

今日はライフゲームを実装した

やりたくなったからやったけど、時間の都合でちまちま記事を書いてないので、成果物だけ貼ることになる。

type lifegame_cell = Live | Dead
type lifegame_type = lifegame_cell array array

let lifegame_construct size =
    Array.make_matrix size size Dead

let random_choice xs =
    let len = Array.length xs in
    xs.(Random.int len)

let lifegame_initialize size =
    let field = lifegame_construct size in
    begin
        for y=0 to size-1 do
            for x=0 to size-1 do
                field.(y).(x) <- random_choice [|Live; Dead|]
            done
        done;
        field
    end

let lifegame_cell_string = function
    | Dead -> "□"
    | Live -> "■"

let lifegame_at field x y =
    let len = Array.length field in
    field.((y+len) mod len).((x + len) mod len)


let lifegame_cell_update field x y =
    let envcount = ref 0 in
    begin
        for i = -1 to 1 do
            for j = -1 to 1 do
                if not (i == 0 && j == 0) then
                    if lifegame_at field (y+i) (x+j) == Live then
                        envcount := !envcount + 1
            done
        done;
        match (lifegame_at field x y, !envcount) with
        |(Dead, 3) | (Live, 2) | (Live, 3) -> Live
        |_ -> Dead
    end

let lifegame_update field =
    let size = Array.length field in
    let new_field = lifegame_construct size in
    begin
        for y=0 to size-1 do
            for x=0 to size-1 do
                new_field.(y).(x) <- lifegame_cell_update field x y
            done
        done;
        new_field
    end


let lifegame_print field =
    Array.iter (fun row ->
        Array.map lifegame_cell_string row
        |> Array.to_list
        |> String.concat ""
        |> print_endline) field

let main =
    begin
        Random.self_init();
        let turn = ref 1 in
        let field = ref (lifegame_initialize 10) in
        while true do
            print_string "\027c";
            lifegame_print !field;
            print_endline ("turn:"^string_of_int !turn);
            turn := !turn + 1;
            field := lifegame_update !field;
            Unix.sleep 1;
        done
    end

いろいろ新しく触った部分があるので紹介していく、ます。

|>

F# で大人気のパイプライン演算子です。 f a って言う関数適用を a |> f って書けるようにする演算子で、実装は let (|>) a f = f a とかだと思う。中置記号強いね。

これがあると流れるプログラムが書ける。UFCSと似たりよったり。

type

type を使うと独自型を定義できる。特定のデータ構造に依るプログラムを書くときは定義したほうがバグを生みにくそうだし読みやすそう。 0, 1って書くよりも Live, Dead って書いたほうがわかるという enum みたいな type と、 type lifegame_type = lifegame_cell array array っていう typedef みたいな type とがあった。

ref

OCamlは純粋じゃないから、 mutable な変数を作れて、それを作るときには let x = ref v ってする。値を参照するときは !x で、何かを代入するときは := を使ってあげる。

begin ... end

(...) の構文糖衣。こっちのほうがみためがよさそうなので使ってます。基本的にOcamlLispというか一つの式しかかけないので、() でくくって ; でつなげて、無理くり一つの式にしてあげる。 Lisp 系の do とか begin と同じだね。ということは do ... done は暗黙の begin のような気がしてきた。

<-

Array は実は mutable なデータ構造で、任意のデータを書き換えられるんだけど xs.set i v ってする代わりに xs.(i) <- v って書いてもいいそうです。

print_string "\027c"

画面のクリア。 printf "\x1bc" みたいなことをします。OCamlでは '\xxx で任意の文字コードを持つ文字を書ける(必ず三桁。そして10進)。


だいたいこんな感じです。スリープのために Unix.sleep を使ったから、実行とかコンパイルのときは ocaml unix.cma lifegame.ml ってする。順番を遵守しないといけない……。

ところで lifegame_cell_update の match 式の部分、最初は

match (lifegame_at field x y, !envcount) with
|(a,b) when a == Dead && b == 3 -> Live
|(a,b) when a == Live && (b == 2 || b == 3) -> Live
|(a,b) when a == Live && b <= 1 -> Dead
|(a,b) when a == Live && b >= 4 -> Dead
|_ -> Dead

って超絶まどろっこしく書いてたけど、 shinkwhek くんのおかげで今のように短く読みやすくなりました。感謝 :pray:

OCaml入門 ひとりAdventCalendar 5日目

そろそろOCamlのObjectiveなところをやりたいですね。

今日の目標

昨日のライフゲームを、GUIなViewerに移したい。

lablgtk のインストール

OCamlgtkバインディングのようなものだと思います。 GTK2 に対応。GTK3に対応してくれ。

普通に opam install lablgtk したら失敗するので、 sudo apt install libgtk2.0-dev してから opam install lablgtk したら成功しました。

ただのWindowの表示

とにかくWindowを表示します。

let main =
    begin
        let _ = GtkMain.Main.init () in
        let window = GWindow.window ()in
        window#show();
        GMain.Main.main()
    end

サードパーティのライブラリを使ってるので、愚直に ocaml lifegame_gui.ml としてもうまく動きません。そこで、

ocamlfind ocamlc -package lablgtk2  -linkpkg lifegame_gui.ml

とします。 ocamlfind ocamlcサードパーティのライブラリを使うときには定番のコンパイル方法だってチュートリアルさんが言ってました。

-package <name> オプションで利用するパッケージ(ここでは lablegtk2)を指定して、 -linkpkg でパッケージをちゃんとリンクしてくれるようにお願いします。これで a.out を吐いてくれるので、実行してみると、Windowがでます。ぺけを叩くとWindowは消えますが、プログラムは終了しないので C-c で止めます。

ビルドの自動化

毎回 ocamlfind ... を叩くのはしんどい気がするので、今のうちにまとめてしまいます。これくらいならシェルスクリプトにぺってするのが一番なんですが、入門ということもあるので、OCamlistic な方法を取っていきたいです。

標準では ocamlbuild というコマンドがあるみたいですが、なんかそりが泡なさそうな気がしたので、サードパーティの、 omake というツールを使ってみることにします。おまけ。

omakeopam install omake で入れて、プロジェクトのディレクトリで omake --install とすると OMakerootOMakefile が生成されます。

OMakeroot は基本的にいじらなくていいらしいので OMakefile を今回のプロジェクトっぽく編集したいですね。

デフォルトでは160行にも及ぶ長いコメントが書いてあるので、これをいい感じにしていきます。

感覚で適当にやるとこうなりました

# This project requires ocamlfind (default - false).
USE_OCAMLFIND = true

OCAMLPACKS[] =
    lablgtk2

if $(not $(OCAMLFIND_EXISTS))
   eprintln(This project requires ocamlfind, but is was not found.)
   eprintln(You need to install ocamlfind and run "omake --configure".)
   exit 1

#
# Include path
#
# OCAMLINCLUDES +=

NATIVE_ENABLED = $(OCAMLOPT_EXISTS)
BYTE_ENABLED = $(not $(OCAMLOPT_EXISTS))

################################################
# Build an OCaml program

FILES[] =
    lifegame_gui

PROGRAM = lifegame_gui

.DEFAULT: $(OCamlProgram $(PROGRAM), $(FILES))

私が編集したのは、 OCAMLPACKS[] = lablgtk2FILES[] = lifegame_guiPROGRAM = lifegame_gui の部分だけです。

これで omake してみます。ファイルがどががっと生成されて、その中には lifegame_gui もありました。最高。ちゃんと動きました。

自動実行とかclean とか

make clean とか make run 的なことがしたいですよね。調子に乗って書きます。

……。一瞬で書けてしまった……。

.PHONY: clean, run
clean:
    rm $(filter-proper-targets $(ls R, .))
run: .DEFAULT
    ./$(PROGRAM)

filter-proper-targetsomake が生成するターゲットのリストを返してくれるらしいので、よくわからない加工をして、 rm すれば clean が完成します。

run.DEFAULT 呼んでプログラムをビルドしたあとに ./$(PROGRAM) ってして成果物を実行します。いい感じですね。

さらにやばいことに -p オプションをつけて omake -p run とするか、 -P オプションでもいいですが、ファイルを監視して更新されたら run してくれます。多分だけど -Pコンパイルエラーしてもめげない。

Window の破棄と同時に終了

window#connect#destroy ~callback:GMain.Main.quit;

を追加します。

矩形を書く

ライフゲームのために、矩形を書きたいです。

結構苦労しながら、書くことができました。

let draw_rect drawing_area _ =
    begin
        let drawing = (
            drawing_area#misc#realize();
            new GDraw.drawable (drawing_area#misc#window)
        ) in
        drawing#set_foreground `BLACK;
        drawing#rectangle ~x:0 ~y:0 ~width:100 ~height:100 ~filled:true ();

        true
    end
let main =
    begin
        let _ = GtkMain.Main.init () in
        let width = 640 in
        let height = 480 in
        let window = GWindow.window 
            ~width:width ~height:height
            ~title:"Lifegame" ()in
        let drawing_area = GMisc.drawing_area
                            ~width:width ~height:height ~packing:window#add () in
        drawing_area#misc#set_double_buffered true;
        drawing_area#event#connect#expose (draw_rect drawing_area);
        window#show();
        window#connect#destroy ~callback:GMain.Main.quit;
        GMain.Main.main()
    end

とりあえずWindowの大きさなどを変更しています。 ~width:width と書いてあるのは、ラベル付きのオプション引数 width に 変数 width を渡している図です。

描画は、 drawing_area というものを作って、これを window に追加します(これは ~paking:window#add でやってそう)、それから expose にコールバックを登録して、exposeのタイミングで描画を行います。 expose は再描画要求を受け付けたときに呼び出されて、 GdkEvent.Expose.t というよくわからない型を受けて bool を返す関数をコールバック引数に取りますが、このよくわからないイベント型は使ってないので draw_rect の第二引数にダミーの _ をいれてあります。

あとはわかりそう。 new は他の言語で言うところの new です。こいつは関数じゃなさそうだよね。

個人的に新しかったのは let の右辺に (...) って二式以上を書くやつ。考えてみればできるだろうけどなるほどなって感じになってしまった。

あ、 set_foreground の引数は全然わかってないです `BLACK`WHITE にしたらたしかに白で描画されるんだけどこれなに……。

↓こんな感じになります

ライフゲームの描画っぽくする

ががっとこういう関数を書きます

let draw_lifegame_field drawing_area field _ ?(cell_size=5) =
    begin
        let drawing = (
            drawing_area#misc#realize();
            new GDraw.drawable (drawing_area#misc#window)
        ) in
        let size = Array.length field in
        begin
            for y=0 to size-1 do
                for x=0 to size-1 do
                    drawing#set_foreground `BLACK;
                    drawing#rectangle
                        ~x:(cell_size*x) ~y:(cell_size*y) ~width:cell_size ~height:cell_size ~filled:false ();
                    if field.(y).(x) = Live then
                        drawing#set_foreground `BLACK
                    else
                        drawing#set_foreground `WHITE;
                    drawing#rectangle
                        ~x:(cell_size*x+1) ~y:(cell_size*y+1) ~width:(cell_size-1) ~height:(cell_size-1) ~filled:true ();

                done
            done;
            true
        end
    end

かなり読みにくいですがやってることは素直なのでよめる。 cell_size はラベル付きオプション引数にしてみました。

定期的に再描画する

探したら、多分こういうのは timeout を使えば行けそうということがわかりました。

GMain.Timeout.add ~ms:1000 ~callback:(timer_func drawing_area field);

こういう行を追加しておいて、 timer_func の中身はこんな感じです。

let timer_func drawing_area field =
    fun () ->
        begin
            field := (lifegame_initialize 10);
            draw_lifegame_field drawing_area !field ~cell_size:10;
            true
        end

Timeout.add のコールバックは ()->bool 型になるので、そうなるようなクロージャを作って返しています。

なんか気持ち悪くなってきたので、 draw_lifegame_field_ になってる引数を消し飛ばして、 expose にはこんな感じ (fun _ -> draw_lifegame_field drawing_area !field ~cell_size:10); で渡しておきます。

それにしても、 timer_func から直接 draw_lifegame_field を呼び出してるの気持ち悪い……。 repaint に相当するものが欲しくなりますね。あと fieldref になっちゃったのでキモい

昨日のライフゲームとくっつける。

まずは lifegame.ml をカレントにコピーしてきます。 main はきっと邪魔になるので削除しました。

OMakefileFILES[]

FILES[] =
    lifegame
    lifegame_gui

に更新します。

そして、 lifegame_gui.ml の先頭に open Lifegame と書けば準備は完了で、あとは timer_funcfield の更新行を field := update_field !field と書けば終わりです。

最終的にこうなった

open Lifegame

let lifegame_cell_color = function
    | Live -> `BLACK
    | Dead -> `WHITE

let draw_lifegame_field drawing_area field ?(cell_size=5) =
    begin
        let drawing = (
            drawing_area#misc#realize();
            new GDraw.drawable (drawing_area#misc#window)
        ) in
        let size = Array.length field in
        begin
            for y=0 to size-1 do
                for x=0 to size-1 do
                    drawing#set_foreground `BLACK;
                    drawing#rectangle
                        ~x:(cell_size*x) ~y:(cell_size*y) ~width:cell_size ~height:cell_size ~filled:false ();
                    drawing#set_foreground (lifegame_cell_color field.(y).(x));
                    drawing#rectangle
                        ~x:(cell_size*x+1) ~y:(cell_size*y+1) ~width:(cell_size-1) ~height:(cell_size-1) ~filled:true ();
                done
            done
        end
    end

let update_draw drawing_area field =
    begin
        field := lifegame_update !field;
        draw_lifegame_field drawing_area !field ~cell_size:10
    end

let main =
    begin
        Random.self_init();
        let field = ref(lifegame_initialize 10) in
        let _ = GtkMain.Main.init () in
        let width = 640 in
        let height = 480 in
        let window = GWindow.window ~width:width ~height:height ~title:"Lifegame" () in
        let drawing_area = GMisc.drawing_area
                            ~width:width ~height:height ~packing:window#add () in
        drawing_area#misc#set_double_buffered true;
        GMain.Timeout.add ~ms:100 ~callback:(fun _ -> update_draw drawing_area field; true);
        drawing_area#event#connect#expose (fun _ -> draw_lifegame_field drawing_area !field ~cell_size:10; true);
        window#show();
        window#connect#destroy ~callback:GMain.Main.quit;
        GMain.Main.main();
    end

rectangle のあたりで横に長い、 50行のプログラムができました。ロジックは昨日出来上がっていたので、行数としては短いです。

OCamlにもGtkにも慣れないのですごい時間取られた……。手抜きのつもりだったのに。

f:id:Furutsuki:20171219212504g:plain

OCaml入門 ひとりAdventCalendar 6日目

そろそろレコードとか出てきてほしいんですが使う機械がないですね……ということは触らなくてもいいということ……。

今日の目標

昨日でてきた `BLACK `WHITE の正体を探っていきたいですね。これだけだと内容が少ないようなら、よく知らないOCamlのクラスシステムにふれていきたいところです 。

`BLACK とは何か

調べたところ、「多相バリアント」らしいです。「バリアント」ってカタカナで書くのが無限にダサいので、以後は "polymorphic variant" と書きます。

variant

polymorphic でない variant もあって、これは既に使っていました。type lifegame_cell = Live | Dead がそうです。 LiveDead が variantで lifegame_cell は variant type です。

ここまで知って、variant って enum みたいなものか、と思ったのですが、曰く

バリアントは、C でいうところの enum 及び union を複合したもの

ということです。 variant は値を持つこともできて、有名な例が option です。

option

option は None または値を持つことができる variant type で、

type norec 'a option = None | Some of 'a

と定義されています。これは OCaml Toplevel で調べた。 'a は型変数で、任意の型になりそうです。'a option は適当な型を引数としてとって int option とか string option みたいな型になる。

polymorphic variant

ですが、勉強したところ、なんとなく何かはわかったし必要になる場面が出てきそうということもわかったのですが、説明をしろといわれると途端に難易度がEclipseなので

http://osiire.hatenablog.com/entry/20090510/1241957550

あたりを読んで勉強するのが楽だよということだけメモしておきます。個人的には mouse_eventkeyboard_event の統合のところが分かりやすかった。

拡張しやすさを残したり、あるいは明らかに他のところでもおんなじようにこの概念つかいそう、というときは polymorphic variant にしておけばいいんですかね……。

これってある種の interface のようなものかな。


昨日サボりそこねたのできょうはこんな感じでおわりです。

実は記事のストックは明日までしかなくて実質入門してない

OCaml入門 ひとりAdventCalendar 7日目

今日の目標

だんだんStep-by-Stepで進めなくなってるので、今日の目標というか「今日の成果物」になりつつありますね……。

ということで、簡易jsonパーサを書きました。ちゃんとしたやつじゃないです。

少し前に、まだ一週間経ってないくらいですが、D言語jsonのパーサを書きました。ちゃんとしたやつじゃないです。ので、OCamlでも書いてみようという気持ちになったので、D言語で書いたのと同じような仕様のやつを書きました。色々足りてなくて、 1e10 とかもちろんパースできないし、 "\uxxxx"もだめです。 "\n" は読めるけど "\t" は実装してません。

D言語だと、2時間半とすこしかけて https://github.com/theoldmoon0602/jparse を書いたんですが、OCaml だと更に1時間くらいかかりましたかね……。

今回のコードは https://github.com/theoldmoon0602/jparse-ocaml にあります。

コード中のいろいろ

jparse.ml の一番最初に、 json_type を定義しています。これは、そのまま json を表すための型で、 variant がすごく生きてる気がします。

D言語では 型を表す enum と 値を表す union を使いましたが、OCamlでは、すっと短く、スマートにjson型を表すことができました。

続く parse_info は初登場のレコードです。名前付きタプルみたいなもの、あるいは Cのstruct のようなものと認識してます。 i はどこまで読んだかを表すポインタで、 mutable な値なので info.i <- info.i+1 としてるコードが散見されます。関数型でプログラミングするならここは immutable で毎度新しい parse_info を作ったりするんでしょうが、明らかに面倒なのにバグが減る嬉しい作用もなさそうなのでこうしました。

更に続いて exception です。OCamlでは例外を自分でぱぱっと定義してしまうのが良さげのようなのでやりました。

あとはだいたい素直です。エラー処理がいちいち可読性を下げてる気しかしません。特に文字列の範囲外エラーはめっちゃ気を使って色んな所に同じようなコードを書く羽目になったので、もっとまとめたいです。例外が出てから try with で受け止めて投げ直すコードで関数全部をまるっと括ってやるとかになるんですかね。

あーあと相互再帰がありました。これは踏んだときになんでエラーになるのかわからないコンパイルエラーが出て困りましたね。

parseparse_array parse_object は相互に呼び出し合う関係なのですが、こういうときは

let rec A =
    ...
and B =
    ...

と特別な書き方をしないとだめで萎え萎えです。まあいいか。

それから、これは jparse_main.ml の方ですが、何故かOCamlのHashtblには iter はあるのに map がなく、自分で

let hashtbl_map f tbl =
    Hashtbl.fold (fun k v acc -> (f k v) :: acc) tbl []

fold を使って書かざるを得ませんでした。ちなみに accaccumlator とか accumlation の略です。

Hashtbl に map が入ってないのは型が ('a, 'b) -> 'c -> ('a , 'b) Hashtbl.t -> 'c list みたいになって(Hashtblがレシーバなのにlistで返すところが)気持ち悪いからですかね。

次何すればいいかな……

longとboolのoverload

こういうはまり方をした。

https://run.dlang.io/is/LpLTja

longを引数に取る f が呼ばれると思ってたんだけど、その実 bool のほうが呼ばれてた。0と1に限っては、こういうことになるらしい。 回避策としては 0L、1Lと書いてlongを明示するか、 int も overload して、 long を呼ぶか。