エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Elixir で Gettext を使う

由緒正し I18n の仕組みということを、 Phoenix を触るようになって初めて知りました。

Phoenix ではプロジェクトを作成すると自動的にパッケージが追加されますが、通常の Elixir プロジェクトで利用する手順を調べました。

くわしくは公式ドキュメントに全部書いてあります。

プロジェクトを用意する

$ mix new my_app
$ cd my_app

パッケージを追加する

バージョンはそのときどきの適切な(たいていは最新の)バージョンを指定してください。

  defp deps do
    [
      {:gettext, "~> 0.15.0"}
    ]
  end

パッケージの取得とコンパイル

$ mix do deps.get, deps.compile

実行すると gettext 関連のタスクが追加されます。

$ mix help
...
mix gettext.extract   # Extracts translations from source code
mix gettext.merge     # Merge template files into translation files
...

プロジェクトの Gettext モジュールを作る

こんな感じで。

# lib/my_app/gettext.ex
defmodule MyApp.Gettext do
  use Gettext, opt_app: :my_app
end

Gettext を利用したコードを書く

たとえばこんな感じで。

# lib/my_app.ex
defmodule MyApp do
  import MyApp.Gettext

  def say do
    IO.puts gettext("Hello, %{name}!", name: "世界")
    IO.puts gettext("Hi.")
    IO.puts gettext("Bye.")
  end
end

実行するとこんな感じ。まだなにもしていないので gettext に与えられた文字列がそのまま出力されています。

$ mix run -e 'MyApp.say'
Hello, 世界!
Hi.
Bye.

文字列を抽出する

mix gettext.extract を実行して文字列を抽出します。 priv/gettext/default.pot というファイルが作成され、抽出した文字列が格納されます。

$ mix gettext.extract
Compiling 2 files (.ex)
Extracted priv/gettext/default.pot

ロケールに対応した .po ファイルを作成する

$ mix gettext.merge priv/gettext --locale=ja
Created directory priv/gettext/ja/LC_MESSAGES
Wrote priv/gettext/ja/LC_MESSAGES/default.po

priv/gettext の下に指定したロケールディレクトリが作成され、その中に default.po というファイルが作成されます。

msgidgettext に与えた文字列で、文字列を置き換える時の ID になります。 msgstr に置き換える文字列を記述します。このときプレイスホルダ(ここでは %{name} の部分)は置き換え後も機能するようにそのまま残します。

たとえばこんな感じ。

## `msgid`s in this file come from POT (.pot) files.
##
## Do not add, change, or remove `msgid`s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use `mix gettext.extract --merge` or `mix gettext.merge`
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: ja\n"
"Plural-Forms: nplurals=1\n"

#, elixir-format
#: lib/my_app.ex:7
msgid "Bye."
msgstr "ぢゃ、そんな感じで。"

#, elixir-format
#: lib/my_app.ex:5
msgid "Hello, %{name}!"
msgstr "こんにちは、%{name}!"

#, elixir-format
#: lib/my_app.ex:6
msgid "Hi."
msgstr "やはぁ。"

デフォルトのロケールを指定する

config/config.exs にデフォルトロケールの指定を追加します。

# config/config.exs
config :my_app, MyApp.Gettext,
  default_locale: "ja"

コンパイルして実行

$ mix compile --force
Compiling 2 files (.ex)
Generated my_app app
$ mix run -e 'MyApp.say'
こんにちは、世界!
やはぁ。
ぢゃ、そんな感じで。

文字列を追加する

Gettext を使う文字列を追加してみます。

# lib/my_app.ex
defmodule MyApp do
  import MyApp.Gettext

  def say do
    IO.puts gettext("Hello, %{name}!", name: "世界")
    IO.puts gettext("Hi.")
    IO.puts gettext("Leave it to me!")
    IO.puts gettext("Bye.")
  end
end

抽出します。

$ mix gettext.extract
Compiling 2 files (.ex)
Extracted priv/gettext/default.pot

priv/gettext/default.pot に追加した文字列が追加されます。

ロケール.po ファイルにマージします。

$ mix gettext.merge priv/gettext --locale=ja
Wrote priv/gettext/ja/LC_MESSAGES/default.po

priv/gettext/ja/LC_MESSAGES/default.po を編集します。

#, elixir-format
#: lib/my_app.ex:7
msgid "Leave it to me!"
msgstr "むぁ〜かせて!"

コンパイルして実行します。

$ mix compile --force
Compiling 2 files (.ex)
Generated my_app app
$ run -e 'MyApp.say'e
こんにちは、世界!
やはぁ。
むぁ〜かせて!
ぢゃ、そんな感じで。

動的にロケールを変更する

Gettext.put_locale/1ロケールの文字列を指定すると動的にロケールを変更できます。

$ mix run -e 'Gettext.put_locale("en"); MyApp.say'
Hello, 世界!
Hi.
Leave it to me!
Bye.

もしくは。 MyApp.Gettextロケールのみを変更したいばあいは、Gettext.put_locale/2 を使ってモジュールを指定します。

$ mix run -e 'Gettext.put_locale(MyApp.Gettext, "en"); MyApp.say'
Hello, 世界!
Hi.
Leave it to me!
Bye.
$ mix run -e 'Gettext.put_locale(MyApp.Gettext, "en"); MyApp.say; Gettext.put_locale(MyApp.Gettext, "ja"); MyApp.say'
Hello, 世界!
Hi.
Leave it to me!
Bye.
こんにちは、世界!
やはぁ。
むぁ〜かせて!
ぢゃ、そんな感じで。

Phoenix で使う

Phoenix プロジェクトを作成すると、パッケージが追加された状態で priv/gettext も用意された状態になっています。

$ mix phx.new my_phx
my_phx/
└── priv/
     └── gettext/
          ├── en/
          │   └── LC_MESSAGES/
          │       └── errors.po
          └── errors.pot

ここで mix gettext.extract を実行すると、lib/my_phx_web/templates/page/index.html.eex"Welcome to %{name}!"priv/gettext/default.pot に抽出されます。

$ mix gettext.extract
Compiling 12 files (.ex)
Generated my_phx app
Extracted priv/gettext/errors.pot
Extracted priv/gettext/default.pot

同様に mix gettext.merge を実行します。

$ mix gettext.merge priv/gettext --locale=ja
Created directory priv/gettext/ja/LC_MESSAGES
Wrote priv/gettext/ja/LC_MESSAGES/errors.po
Wrote priv/gettext/ja/LC_MESSAGES/default.po

生成された priv/gettext/ja/LC_MESSAGES/default.po を編集します。

#, elixir-format
#: lib/my_phx_web/templates/page/index.html.eex:2
msgid "Welcome to %{name}!"
msgstr "%{name}へようこそ!"

config/config.exs を編集してデフォルトロケールの指定を追加します。

config :my_phx, MyPhxWeb.Gettext,
  default_locale: "ja"

サーバを起動します。

$ mix phx.server

ブラウザで http://localhost:4000 にアクセスします。

f:id:E_Mattsan:20180227213619p:plain

ぢゃ、そんな感じで。

いつか読むはずっと読まない:なぜなら、猫だから

本書の帯より。

この本に登場する猫たち

  • マシュマロを焼く天才猫
  • マスコットとして宇宙船に乗った猫
  • 銀河文明を変えた猫
  • 生き返ってから年を取らなくなった猫
  • テレパスになった猫

整数値でも浮動小数点数値でもパースしたい

Elixir で文字列から数値に変換するときに String.to_integer/1String.to_float/1 を使いますが、前者は浮動小数点数値の文字列を与えると、後者は整数値の文字列を与えると例外を投げてしまいます。

iex(1)> String.to_integer("123")  
123
iex(2)> String.to_integer("123.4")
** (ArgumentError) argument error
    :erlang.binary_to_integer("123.4")
iex(2)> String.to_float("123.4")
123.4
iex(3)> String.to_float("123")  
** (ArgumentError) argument error
    :erlang.binary_to_float("123")

例外を投げない関数として、Erlangstring モジュールにそれぞれ同名の関数があります。こちらはパースに成功すると、成功した部分の数値とパースできなかった部分を残りの文字列のタプルを返します。 パースできる文字列がなかった場合には :error とその理由のタプルを返します。

iex(1)> :string.to_integer("123+987")
{123, "+987"}
iex(2)> :string.to_float("123.4+987.6")
{123.4, "+987.6"}
iex(3)> :string.to_integer("abc")      
{:error, :no_integer}
iex(4)> :string.to_float("abc")  
{:error, :no_float}

ここまではいいのですが。任意の数値の文字列を返す関数がなくて困りました。string:to_integer/1浮動小数点数値の文字列を与えると小数点の手前までしかパースしませんし、string:to_float/1 に整数値の文字列を与えるとエラーになります。

iex(1)> :string.to_integer("123.4")
{123, ".4"}
iex(2)> :string.to_float("123")    
{:error, :no_float}

対策としては。最初に string:to_float/1 でパースして失敗したら string:to_integer/1 でパースするという手があります。

defmodule MyParser do
  def to_number(source) do
    case :string.to_float(source) do
      {:error, _} -> :string.to_integer(source)
      result -> result
    end
  end
end

これでだいたいうまくいきます。

iex(1)> MyParser.to_number("123")         
{123, ""}
iex(2)> MyParser.to_number("123.4")
{123.4, ""}
iex(3)> MyParser.to_number("123+987")
{123, "+987"}
iex(4)> MyParser.to_number("123.4+987.6")
{123.4, "+987.6"}
iex(5) MyParser.to_number("abc")
{:error, :no_integer}

ほぼ問題ないのですが、エラーのときに {:error, :no_integer} と返ってくるのでこれを整えたい。

case をもう一段重ねればそれなりに動きます。

defmodule MyParser do
  def to_number(source) do
    case :string.to_float(source) do
      {:error, _} ->
        case :string.to_integer(source) do
          {:error, _} -> {:error, :no_number}
          result -> result
        end
      result -> result
    end
  end
end
iex(1)> MyParser.to_number("abc")
{:error, :no_number}

もうちょっとどうにかならないかとライブラリを読んでいたら。Enum.reduce_while/3 という関数を見つけました。

名前の通り条件が満たされるまで第 3 引数で与える関数を適用し、満たされたら適用を中断するというものです。

第 3 引数で与える関数は 2 要素のタプルを返す必要があります。タプルの第 1 要素が :cont だったばあいは処理を継続しタプルの第 2 要素が次の処理に引き継がれます。タプルの第 1 要素が :halt だったばあいは処理を中断しタプルの第 2 要素が Enum.reduce_while/3 の戻り値になります。

それらをふまえて。

defmodule MyParser do
  def to_number(source) do
    [&:string.to_float/1, &:string.to_integer/1]
    |> Enum.reduce_while(nil, fn f, _ ->
      case f.(source) do
        {:error, _} -> {:cont, {:error, :no_number}}
        result -> {:halt, result}
      end
    end)
  end
end

第 2 引数の値は利用していないので nil を与え、適用する関数の第 2 引数も _ にしています。

実行。

iex(1)> MyParser.to_number("123")        
{123, ""}
iex(2)> MyParser.to_number("123.4")      
{123.4, ""}
iex(3)> MyParser.to_number("123+987")    
{123, "+987"}
iex(4)> MyParser.to_number("123.4+987.6")
{123.4, "+987.6"}
iex(5)> MyParser.to_number("abc")        
{:error, :no_number}

今回は適用する関数がふたつだったので Enum.reduce_while/3 を利用する利点があまりありませんでしたが、適用する関数の数がもっと多くなったばあいには重宝しそうです。

いつか読むはずっと読まない:二十世紀前半のイギリスの赤毛の女性の物語

図らずしも。偶然に続けてよんだ小説が、どちらも二十世紀前半の(とはいえ一方は初頭、一方は第二次大戦時)のイギリスを舞台にした赤毛の女性が主人公の物語。

こちらは既刊が 6 巻。

チャーチル閣下の秘書 (創元推理文庫)

チャーチル閣下の秘書 (創元推理文庫)

こちらは全 3 巻。第 3 巻はもうじき刊行予定。

紙の魔術師 (ハヤカワ文庫FT)

紙の魔術師 (ハヤカワ文庫FT)

標準入力、標準出力、標準エラーをコードでリダイレクトする

cstdio と iostream が混ざっているのが少々座りが悪いのですが。iostream からファイルディスクリプタを取得できればよいのですが仕様上は仕組みが用意されていないようなので、cstdio を利用しています。

#ifndef REDIRECT_H__
#define REDIRECT_H__

#include <unistd.h>

template<int FD>
class RedirectT {
public:
    RedirectT(int fd) : dup_fd_(dup(FD)) {
        dup2(fd, FD);
    }

    ~RedirectT() {
        sync();
        dup2(dup_fd_, FD);
        close(dup_fd_);
    }

private:
    int dup_fd_;
};

typedef RedirectT<0> RedirectStdin;
typedef RedirectT<1> RedirectStdout;
typedef RedirectT<2> RedirectStderr;

#endif//REDIRECT_H__

使用例。

#include <iostream>
#include <cstdio>

#include "redirect.h"

int main(int, char* []) {
    {
        FILE* file = std::fopen("temp.txt", "w");
        RedirectStdout to(fileno(file));
        std::cout << "This message is redirected to file." << std::endl;
        fclose(file);
    }

    {
        FILE* file = std::fopen("temp.txt", "r");
        RedirectStdin from(fileno(file));
        std::cout << std::cin.rdbuf() << std::endl;
        fclose(file);
    }

    std::cout << "This message is not redirected to file." << std::endl;

    return 0;
}

コンパイルして実行。

$ g++ -o redirect_sample redirect_sample.cpp 
$ ./redirect_sample 
This message is redirected to file.

This message is not redirected to file.
$ cat temp.txt 
This message is redirected to file.

標準出力への文字列の出力が temp.txt への書き込みになり、標準入力からの文字列の入力が text.txt からの読み出しになっています。

式を式のまま受け取り左辺と右辺を別々に評価する

Elixir のテストの assert は引数が一つで、一つの式を与えるのですが、左辺と右辺のそれぞれの値を把握しています。

例えば。

適当なプロジェクトを作り、

$ mix new foo
$ cd foo

テストをこんな風に書いて、

# test/foo_test.exs
defmodule FooTest do
  use ExUnit.Case

  test "1 + 2 == 4 ...?" do
    assert 1 + 2 == 4
  end
end

テストを実行してみると、

$ mix test


  1) test 1 + 2 == 4 ...? (FooTest)
     test/foo_test.exs:4
     Assertion with == failed
     code:  assert 1 + 2 == 4
     left:  3
     right: 4
     stacktrace:
       test/foo_test.exs:5: (test)



Finished in 0.02 seconds
1 test, 1 failure

こんな感じになります。

Elixir の assert はマクロで記述されていて、引数を評価前の式の構造のままで受け取ることで実現しているようです。

おもしろい…と思っていたのだけれど、似たようなことが Prolog でも書けることに気がつきました。

% foo_test.prolog

judge(L, L) :-
  format("OK", []),
  !.

judge(L, R) :-
  format("failed~n  left:  ~d~n  right: ~d~n", [L, R]).

assert(LHS == RHS) :-
  L is LHS,
  R is RHS,
  judge(L, R).

いつものように GNU Prolog で実行。

$ gprolog
GNU Prolog 1.4.4 (64 bits)
| ?- ['foo_test.prolog'].
| ?- assert(1 + 2 == 4).
failed
  left:  3
  right: 4

yes

述語 assert は引数に LHS == RHS という構造の式を受け取ります。Prolog では引数として式を書くと評価されずに式のまま述語に渡されます。受け取る側では式の形でマッチさせることで右辺と左辺を取り出すことができるわけです。

以前にも評価されるまで式の形のままで受け渡しができることを利用した Key-Value ペアを扱う方法について書きましたので、興味がある方はどうぞ。

PlatformIO で Arduino のプロジェクトを管理する

Arduino!

最近、Arduino にはまっています。

エントリモデルである Arduino UNO R3 や、それと同等の構成で小型化した Arduino NANOAtmelATmega328P というICを使っていますが、このワンチップは私がプログラミングに本格的に没入した頃に使っていた PC と遜色ない性能を持っています。Arduino に触っているとその深みにハマったときの感覚が蘇ってきます。

そして Arduino の深みにはまっていくと、Arduino IDE でちまちまとスケッチを描いていくのがおっくうになりコマンドラインで管理できる方法がないかと探すことになります。

見つけたのが PlatfomIO 。

Arduino に限らず複数のチップやプラットフォームに対応しているようですが、ここでは Arduino での使い方だけまとめておきます。

PlatformIO!

Arduino にならって「ぷらっとふぉみーお」と呼びたくなるのですが、つづりでは“IO”が大文字になっているので「ぷらっとふぉーむあいおー」と読むのだと思います。どう読むかは見つけられませんでした。

インストールする

Mac のばあい、Homebrew でインストールできます。

$ brew install platformio

Homebrew を使わないばあいは pip を使ってインストールできるようです。

利用できるボードを確認する

boards コマンドで利用できるボードを確認できます。

$ platform boards

けっこうな数が表示されます。Arduino だけでも 40 種類以上出てきます。 情報をしぼりたいばあいはコマンドのうしろにフィルタをつけます。

$ platformio boards "arduino uno" # Arduino UNO の情報だけを表示したい

Platform: atmelavr
------------------------------------------------------------------------------------------------------------------------------------------------------
ID                    MCU            Frequency  Flash   RAM    Name
------------------------------------------------------------------------------------------------------------------------------------------------------
uno                   ATMEGA328P     16Mhz     31kB    2kB    Arduino Uno

これで Arduino UNO の ID は uno とわかります。この ID はボードの指定をするときなどに利用します。

プロジェクトを準備する

プロジェクトを作成するディレクトリに移動して init コマンドを実行します。

$ mkdir sample
$ cd sample
$ platformio init

src/lib/ という二つのディレクトリと platformio.inireadme.txt という二つのファイルが作成されます。

sample
├── lib/
│   └── readme.txt
├── platformio.ini
└── src/

オプションを付けずに init コマンドを実行すると platformio.ini には実際の設定は追加されません。ターゲットにするボードの ID を指定することでその設定が追加されます。

$ platformio init -b uno # Arduino UNO を指定

次のように Arduino UNO の設定のセクションを持つ platformio.ini ファイルが作成されます。

[env:uno]
platform = atmelavr
board = uno
framework = arduino

(コメントは省略しています)

すでに初期化をしたディレクトリで別のボードを指定して init コマンドを実行すると、そのボードの設定が platformio.ini ファイルに追加されます。一つのプロジェクトで複数のボードに対応することができます。

Arduino NANO の ID を調べます。

$ platformio boards "arduino nano"

Platform: atmelavr
------------------------------------------------------------------------------------------------------------------------------------------------------
ID                    MCU            Frequency  Flash   RAM    Name
------------------------------------------------------------------------------------------------------------------------------------------------------
nanoatmega168         ATMEGA168      16Mhz     14kB    1kB    Arduino Nano ATmega168
nanoatmega328         ATMEGA328P     16Mhz     30kB    2kB    Arduino Nano ATmega328

現在一般的に手に入る Arduino NANO は ATmega382P が載っているので nanoatmega328 を指定します。

上と同じディレクトリで init コマンドを実行します。

$ platformio init -b nanoatmega328

Arduino UNO の設定のセクションはそのままに Arduino NANO の設定のセクションが追加されます。

[env:uno]
platform = atmelavr
board = uno
framework = arduino

[env:nanoatmega328]
platform = atmelavr
board = nanoatmega328
framework = arduino

ビルドする/アップロードする

ビルドとアップロードをします。アップロードのターゲットを指定しないとビルドのみを行います。

src/ ディレクトリの下に Arduino IDE で書くときと同じようにスケッチのファイルを作成します。拡張子も .ino にします。

void setup() {
  // ...
}

void loop() {
  // ...
}

ビルドします。

$ platformio run

上で書いたように複数の環境が設定されているばあい、それぞれでビルドが実行されます。

Environment uno              [SUCCESS]
Environment nanoatmega328   [SUCCESS]

特定の環境だけビルドしたい場合は -e オプションで指定します。

$ platformio run -e uno
...
Environment uno             [SUCCESS]
Environment nanoatmega328   [SKIP]

アップロードまで行う場合は -t オプションで upload を指定します。

$ platformio run -e uno -t upload

Mac のばあい、Arduino を USB で接続すると /dev/ ディレクトリに USB デバイスとしてマウントされます。PlatformIO は自動的にそれを見つけて Arduino にスケッチをアップロードします。

アップロード先を指定したいばあいは --upload-port オプションで指定することができます。

デフォルトの環境を指定する

環境を指定しないで run コマンドを実行した時のデフォルトの環境を platfromio.ini で指定できます。

platformio.ini ファイルに platfromio セクションを追加し、env_default でデフォルトの環境を指定します。

[platformio]
env_default=uno

[env:uno]
platform = atmelavr
board = uno
framework = arduino

[env:nanoatmega328]
platform = atmelavr
board = nanoatmega328
framework = arduino

環境を指定せずに run コマンドを実行します。uno 環境のみビルドされます。

$ platformio run
...
Environment uno             [SUCCESS]
Environment nanoatmega328   [SKIP]

依存するライブラリを指定する

各環境のセクションに lib_deps を追加します。

たとえば TimerOne を利用したいばあい次のように記述します。 run コマンドを実行すると必要であれば自動的にライブラリをダウンロードしてライブラリのビルドが実行されます。

[env:uno]
platform = atmelavr
board = uno
framework = arduino
lib_deps = TimerOne

複数のライブラリを指定したいばあいはライブラリごとに改行して記述します。

[env:uno]
platform = atmelavr
board = uno
framework = arduino
lib_deps =
  TimerOne
  DHT11

登録されているライブラリは Libraries · PlatformIO で検索できます。

GitHub などで公開されていれば URL で指定することもできます。自作のライブラリを使いたいけれども PlatformIO には登録していないというばあいなどに利用できます。GitHub のばあい http 形式でも ssh 形式でも指定できます。

[env:uno]
platform = atmelavr
board = uno
framework = arduino
lib_deps =
  https://github.com/PaulStoffregen/TimerOne.git
  git@github.com:adidax/dht11.git

ビルドオプションを指定する

たとえば ESP8266 というライブラリはソフトウェアシリアルを利用できますが、利用するにはマクロで ESP8266_USE_SOFTWARE_SERIAL という値が定義されていなければなりません。

#ifdef ESP8266_USE_SOFTWARE_SERIAL
ESP8266::ESP8266(SoftwareSerial &uart, uint32_t baud): m_puart(&uart)
{
    m_puart->begin(baud);
    rx_empty();
}
#else
ESP8266::ESP8266(HardwareSerial &uart, uint32_t baud): m_puart(&uart)
{
    m_puart->begin(baud);
    rx_empty();
}
#endif

ITEADLIB_Arduino_WeeESP8266/ESP8266.cpp at master · itead/ITEADLIB_Arduino_WeeESP8266 · GitHub

このようなばあいのために build_flags を使ってコンパイラにオプションを渡すことができます。

[env:uno]
platform = atmelavr
board = uno
framework = arduino
lib_deps =
  ESP8266
build_flags = -D ESP8266_USE_SOFTWARE_SERIAL

これで ESP8266SoftwareSerial を渡すことができるようになります。

#include <SoftwareSerial.h>
#include <ESP8266.h>

SoftwareSerial mySerial(11, 12);
ESP8266 wifi(mySerial);

void setup() {
  // ...
}

void loop() {
  // ...
}

設定を共有する

platformio.ini にはすべての環境に影響を及ぼす設定を書くセクションというのはないようです。

共通する設定を記述したセクションを用意して、それを各セクションが参照することで実現しています。

環境 uno と環境 nanoatmega328 で同じライブラリを利用するばあい、次のように記述します。 ここでは common としましたが、セクション名は任意のようです。

[common]
lib_deps =
  TimerOne
  DHT11
  ESP8266
build_flags = -D ESP8266_USE_SOFTWARE_SERIAL

[env:uno]
platform = atmelavr
board = uno
framework = arduino
build_flags = ${common.build_flags}
lib_deps = ${common.lib_deps}

[env:nanoatmega328]
platform = atmelavr
board = nanoatmega328
framework = arduino
build_flags = ${common.build_flags}
lib_deps = ${common.lib_deps}

いつか読むはずっと読まない:あれから数え30年

1988年に発売され一世を風靡した、カードゲームの金字塔『モンスターメーカー』。 オリジナルの楽しさをそのままに、短時間で濃密に遊べるようリメイクされた新版が登場!! モンスターを倒しながら財宝を集め、誰よりも早く迷宮から帰還して名声を得よう!

プログラミングに没入したころに発表されたゲーム。心なしか最近リバイバルが増えている気がします。ゲームブックKindleで復刻されたり。ハイパーリンクは以前からゲームブック向きだなとは思っていましたが。

MONSTER MAKER モンスターメーカー

MONSTER MAKER モンスターメーカー

Duck Typing 〜 Elixirの多態

# 鶩
defmodule Duck do
  defstruct name: nil
end

# 犬
defmodule Dog do
  defstruct name: nil
end

# 猫
defmodule Cat do
  defstruct name: nil
end

# 鳴くプロトタイプ
defprotocol Sound do
  def sound(_)
end

# 鶩が鳴く実装
defimpl Sound, for: Duck do
  def sound(animal) do
    IO.puts "#{animal.name} the Duck sounds quack quack."
  end
end

# 犬が鳴く実装
defimpl Sound, for: Dog do
  def sound(animal) do
    IO.puts "#{animal.name} the Dog sounds bow-wow."
  end
end

# 猫が鳴く実装
defimpl Sound, for: Cat do
  def sound(animal) do
    IO.puts "#{animal.name} the Cat sounds miaow miaow."
  end
end

defmodule Test do
  duck = %Duck{name: "Donald"} # ドナルド
  dog = %Dog{name: "Shiro"}    # しろ
  cat = %Cat{name: "Tama"}     # たま

  Sound.sound(duck)
  Sound.sound(dog)
  Sound.sound(cat)
end

実行。

$ elixir sound.ex 
Donald the Duck sounds quack quack.
Shiro the Dog sounds bow-wow.
Tama the Cat sounds miaow miaow.

Amazon CloudWatch Logs からログを取得する gem を書いた

AWS に CloudWatch Logs というサービスがあります。

コンソールが用意されていますが、正直使いやすくありません。

awslogs という、CloudWatch Logs からログを取得するコマンドラインツールがあり使っていたいのですが、だんだんと不満な点が出てきました。

そんなわけなので

CloudWatch Logs からログを取得する gem を書きました。

実体は aws-sdk gem に薄い層を被せたものです。

使う

ここでの例は Thor gem を使ってコマンドにしたものです。

Gemfile

source 'https://rubygems.org'

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'aws_cloudwatch_logs', github: 'mattsan/aws_cloudwatch_logs'
gem 'thor'

command

#!/usr/bin/env ruby

require 'thor'
require 'time'
require 'aws_cloudwatch_logs'

class Logs < Thor
  default_command :logs

  desc :logs, 'get logs'
  option :start, aliases: '-s', type: :string, desc: 'start time'
  option :end, aliases: '-e', type: :string, desc: 'end time'
  option :group, aliases: '-g', type: :string, desc: 'log group'
  option :filter, aliases: '-f', type: :string, desc: 'filter pattern'
  def logs
    start_time = Time.parse(options[:start])
    end_time = Time.parse(options[:end])
    log_group = options[:group]
    filter_pattern = options[:filter]

    AwsCloudwatchLogs.extract(log_group, start_time, end_time, filter_pattern) do |event|
      time_string = event.timestamp.strftime('%Y-%m-%dT%H:%M:%S%Z')
      puts "#{time_string} #{event.message}"
    end
  end
end

Logs.start

logs というファイル名で保存して実行できるようにします。

$ chmod +x logs

実行

$ bundle exec logs -g foo-bar-baz -s '2017/11/01 00:00' -e '2017/11/01 12:00'

このように aws-sdk を使うときのメンドクサイ部分を隠しただけのものですがメンドクサイ部分を繰り返す必要がないだけ楽になります。

そんな感じで。