ESM アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

大阪Ruby会議03のLTに@maimux2xが登壇します

2023年9月9日 (土) に大阪で開催される『大阪Ruby会議03』の LT に弊社 @maimux2x が登壇します。

regional.rubykaigi.org

本編には @junk0612@koic が登壇し、@haruguchiyuma がスタッフとして参加します。

blog.agile.esm.co.jp

大阪Ruby会議03は絶賛参加受付されています。

rubykansai.doorkeeper.jp

大阪の心斎橋でお会いしましょう。


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

Rubyメソッドかるたのゲームを作りました

はじめに

こんにちは、wai-doiです。

アジャイル事業部は RubyKaigi 2023 で 「Rubyメソッドかるた」を配布しました。 Rubyメソッドかるたの詳細については下記エントリを参照してください。

blog.agile.esm.co.jp

今回は、このRubyメソッドかるたをブラウザで1人で遊べるゲームを作ったことについて書きます。

Rubyメソッドかるたのゲームについて

下記のリンクからゲームを遊べます。

wai-doi.github.io

ゲームのスクリーンショット

基本の操作は、「読み札を読み上げる」のボタンを押すとメソッドの説明が読まれるので、答えのメソッドの札を画面上から探してクリックしていくのを繰り返していきます。

下記のリンクはゲームのソースコードです。 github.com

ちなみに、このゲームは業務で作ったものではなく、個人的に趣味で作ったものです。

作ろうと思ったきかっけ

本家のRubyメソッドかるたを遊ぶためには、取り手と読み手の人が必要なので複数人で遊ぶ必要があります。 そこで、1人で遊べるようにすれば練習として使えるため、個人的に欲しいなと思い作りました。

使った技術

以下はゲームの開発に使った主な技術です。

  • React
  • TypeScript
  • Tailwind CSS

ReactとTypeScriptは個人的に勉強してみたかったため使用しました。 ゲームを作る前にReactのチュートリアルでReactを勉強しました。お題が三目並べのゲームを作るものなので、わかりやすくて良かったです。

CSSライブラリには、最近名前をよく耳にしていたTailwind CSSを使いました。 Tailwind CSS はコンポーネントを提供しておらず、より自由なデザインにできるのがよいと思ったので使ってみました。

ゲームの工夫について

以下はゲームの工夫をした点です。

  • 読み上げをしてくれるようにした
  • お手つきの回数とタイムを計測できるようにした
  • ゲーム終了までの枚数をプレイヤーが決められるようにした
  • 読み上げの速度を自由に変えることができるようにした
  • 読み上げのテキストは表示することができるようにした
  • 取り札が正解か不正解かを音を出すようにした
  • かるたの雰囲気に合うように和室っぽい畳の背景にした
  • 取り札を本物っぽいデザインにした

ここでは読み上げ機能について詳しく書いていきます。

読み上げ機能について

ゲームを作る前提として、1人だけで遊べるようにしたかったため、ゲームが読み上げをしてくれるようにしました。

JavaScriptで日本語のテキストの読み上げができないかを調べると Web Speech API というものがあることがわかりました。

developer.mozilla.org

ライブラリなどは不要ですぐに使うことができました。以下はサンプルコードです。

const utterance = new SpeechSynthesisUtterance()

utterance.text = "Rubyメソッドかるたは楽しい。"
utterance.lang = "ja-JP"

window.speechSynthesis.speak(utterance)

まずSpeechSynthesisUtterance オブジェクトのインスタンスを生成します。 次にオブジェクトに設定を渡します。 .textは読ませるテキストを、lang は言語を指定しています。 最後に speechSynthesis.speak() の引数に先ほどのオブジェクトを渡すことで読み上げが開始します。

この機能が最初のハードルだと思っていたのですが、簡単に実現できてありがたかったです。

ゲームの公開方法について

ゲームの公開にGitHubだけで完結できると楽なので、GitHub Pages を使いました。

公開するために、npmパッケージの gh-pages を使いました。

www.npmjs.com

使い方はまず gh-pages をインストールします。

npm install gh-pages --save-dev`

次に、package.json を開いて設定を書きます。

"scripts": {
  "deploy": "npm run build && gh-pages -d build"
},

"homepage": "https://wai-doi.github.io/ruby-method-karuta"

"scripts" にデプロイ用のコマンドを書き、 "homepage" にGitHubのリポジトリのURLを書きます。

そして npm run deploy を実行すれば、 gh-pages ブランチが作られてリポジトリに push されます。

最後にリポジトリの Settings -> Pages を開いて、公開するブランチを指定する箇所に gh-pages ブランチを設定したら終わりです。

おわりに

今回、Rubyメソッドかるたのゲームを作りました。開発してみて、フロントエンドのあまり触ったことがなかった技術の勉強になったのでとてもよかったです。

手軽に遊べるので、Rubyメソッドかるたを持っている方も持っていない方もぜひ遊んでみて下さい。

Rubyメソッドかるたゲーム


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

Rails / OSS パッチ会オンライン 2023年8月のお知らせ

2023年8月の Rails / OSS パッチ会を 8月30日(水)に Discord でオンライン開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS について、upstream にパッチを送る会です。

会には Ruby と Rails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。

Discord の Rails/OSS パッチ会サーバーへの招待 URL は以下です👇

discord.gg

カンファレンスの秋が近づいてきたこともあり、大阪Ruby会議03、松江Ruby会議10、大江戸Ruby会議10、Kaigi on Rails 2023、RubyWorld Conference 2023 などの話題があるかもしれません。

regional.rubykaigi.org

matsue.rubyist.net

regional.rubykaigi.org

kaigionrails.org

2023.rubyworld-conf.org

これからパッチ会に参加してみたいという方や、OSS 開発者間の会話に興味があるので聞いてみたいという方もお気軽にどうぞ。


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

永和システムマネジメントは Kaigi on Rails 2023 に Gold Sponsor として協賛します

2023年10月27日(金)、10月28日(土) の2日間で開催される Kaigi on Rails 2023 に、株式会社永和システムマネジメントは Kaigi on Rails 2023 に Gold Sponsor として協賛します。

kaigionrails.org

弊社からスピーカーの登壇ならびに、スポンサーブースを出展する予定です。続報をお楽しみに。

https://kaigionrails.org/2023/sponsors/


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

計算結果を記憶する法〜オブジェクト・プロセス・プログラム・テーブル

仕事の Ruby と趣味の Elixir を反復横跳びしている e.mattsan です。

近ごろ仕事では Ruby 以外ばかり書いている気もしなくもないですが、それはそれとして。

Elixir は関数型言語に分類されるプログラミング言語なわけですが。 関数型言語は変数が不変であるとか、ある関数に対して入力が同じならば出力も同じになる、などと一般的に言われていますが、そのようなプログラミング言語でも計算の途中経過を覚えておきたいときがあるわけです。 いわゆるメモ化などがそれに当たります。

では値を変更できるオブジェクトなどを持たない言語はどのようなことができるだろうか、というのが今回のお話です。

ここではメモ化の例でよく使われるフィボナッチ数を例に、いろいろ実装して考えてみます。

オブジェクトで記憶する〜Ruby

最初に比較対象として、オブジェクトで記憶する例を Ruby で実装してみます。

と、さらにその前に。 計算結果を記憶しない愚直な実装を確認しておきます。

記憶しない実装

class FibNaive
  def get(n)
    case
    when n < 1
      raise "'#{n} is not a positive number."
    when n == 1
      1
    when n == 2
      1
    else
      get(n - 1) + get(n - 2)
    end
  end
end

fib_naive.rb という名前で保存して irb で確認してみます。

$ irb
irb(main):001:0> require_relative 'fib_naive'
=> true
irb(main):002:0> fib = FibNaive.new
=> #<FibNaive:0x0000000117758330>
irb(main):003:0> fib.get(1)
=> 1
irb(main):004:0> fib.get(2)
=> 1
irb(main):005:0> fib.get(3)
=> 2
irb(main):006:0> fib.get(10)
=> 55
irb(main):007:1> fib.get(40)
=> 102334155

実行する環境にもよりますが、40 くらいになると結果が返るまでに時間がかかることが感じられると思います。

オブジェクトで記憶する

シンプルに。 インスタンス変数のハッシュを用意して、そこに計算結果を記録してゆきます。

class FibMemo
  def initialize
    @memo = {1 => 1, 2 => 1}
  end

  def get(n)
    case
    when n.negative?
      raise "'#{n} is a negative number."
    when f = @memo[n]
      f
    else
      @memo[n] = get(n - 1) + get(n - 2)
    end
  end
end

同様に fib_memo.rb という名前で保存して irb で確認します。

$ irb
irb(main):001:0> require_relative 'fib_memo'
=> true
irb(main):002:0> fib = FibMemo.new
=> #<FibMemo:0x000000010b405900 @memo={1=>1, 2=>1}>
irb(main):003:0> fib.get(1)
=> 1
irb(main):004:0> fib.get(2)
=> 1
irb(main):005:0> fib.get(3)
=> 2
irb(main):006:0> fib.get(10)
=> 55
irb(main):007:0> fib.get(40)
=> 102334155

前段の計算結果を再利用できるので、先ほどは応答に時間のかかっていた計算でもすぐに結果が返ることがわかると思います。

プロセスで記憶する〜Elixir

さて、Elixir です。 今回も最初に愚直な実装を確認しておきます。

記憶しない実装

defmodule FibNaive do
  def get(n) when n < 1, do: raise "'#{n} is no a positive number."

  def get(1), do: 1

  def get(2), do: 1

  def get(n) do
    get(n - 1) + get(n - 2)
  end
end

fib_naive.ex という名前で保存して、Elixir のインタラクティブシェルである iex で確認してみます。

ソースコードをコンパイルして読み込むために、プロンプトから c "fib_naive.ex" を入力します。

$ iex
iex(1)> c "fib_naive.ex"
[FibNaive]
iex(2)> FibNaive.get(1)
1
iex(3)> FibNaive.get(2)
1
iex(4)> FibNaive.get(3)
2
iex(5)> FibNaive.get(10)
55
iex(6)> FibNaive.get(40)
102334155

これを踏まえて。 Elixir で記憶する方法を考えます。

自分のプロセスで記憶する

一番簡単な方法は、計算をしている主体が計算結果を持ち回り、次の計算の引数として受け渡す方法です。

関数は n の他に、それまでの計算結果を記録した memo を受け取ります。 そして計算の結果として n に対応する数と、その数を算出するための計算結果を記録した新しいメモを組み(タプル)として返します。

defmodule FibMemo do
  def initialize do
    %{1 => 1, 2 => 1}
  end

  def get(_, n) when n < 1, do: raise "'#{n} is not a positive number."

  def get(memo, n) do
    case memo[n] do
      nil ->
        {f1, memo1} = get(memo, n - 1)
        {f2, memo2} = get(memo1, n - 2)
        f = f1 + f2
        {f, Map.put(memo2, n, f)}

      f ->
        {f, memo}
    end
  end
end

fib_memo.ex という名前で保存して iex で確認してみます。

まず初期化で最初の memo を取得します。 12 に対応する数が記憶されています。

$ iex
iex(1)> c "fib_memo.ex"
[FibMemo]
iex(2)> memo = FibMemo.initialize()
%{1 => 1, 2 => 1}
iex(3)> {f1, memo1} = FibMemo.get(memo, 1)
{1, %{1 => 1, 2 => 1}}

次に nmemo を渡すとn に対応する数と新しいメモのタプルが返ります。 新しく取得したメモを次の関数の入力にすることで、それまで記憶した計算結果を利用することができます。

iex(4)> f1
1
iex(5)> {f2, memo2} = FibMemo.get(memo1, 2)
{1, %{1 => 1, 2 => 1}}
iex(6)> f2
1
iex(7)> {f3, memo3} = FibMemo.get(memo2, 3)
{2, %{1 => 1, 2 => 1, 3 => 2}}
iex(8)> f3
2
iex(9)> {f10, memo10} = FibMemo.get(memo3, 10)
{55,
 %{
   1 => 1,
   2 => 1,
   3 => 2,
   4 => 3,
   5 => 5,
   6 => 8,
   7 => 13,
   8 => 21,
   9 => 34,
   10 => 55
 }}
iex(10)> f10
55
iex(11)> {f40, memo40} = FibMemo.get(memo10, 40)
{102334155,
 %{
    ...(略)...
 }}
iex(12)> f40
102334155

この例では次の計算に値を渡すとき、いったん変数で受け取ってから次の計算に渡していますが、このようなときパイプ演算子 |> を使うと、途中の変数を使わずに前段の出力を次段の入力とすることができます。

パイプ演算子を使うと f(a, b) という記述を a |> f(b) と書き換えることができるので、たとえば g(f(a, b), c) という記述を a |> f(b) |> g(c) と記述することができるようになります。

本筋から離れますが、iex はパイプ演算子から記述すると直前の結果を入力として利用できるので、iex でも結果を確認することができます。

iex(1)> c "fib_memo.ex"
[FibMemo]
iex(2)> FibMemo.initialize()
%{1 => 1, 2 => 1}
iex(3)> |> FibMemo.get(1)
{1, %{1 => 1, 2 => 1}}
iex(4)> |> elem(1) |> FibMemo.get(2)
{1, %{1 => 1, 2 => 1}}
iex(5)> |> elem(1) |> FibMemo.get(3)
{2, %{1 => 1, 2 => 1, 3 => 2}}
iex(6)> |> elem(1) |> FibMemo.get(10)
{55,
 %{
   1 => 1,
   2 => 1,
   3 => 2,
   4 => 3,
   5 => 5,
   6 => 8,
   7 => 13,
   8 => 21,
   9 => 34,
   10 => 55
 }}

ちなみに elem(tuple, n) はタプルの n 番目(0始まり)の値を取り出す関数で、ここでは戻り値からメモの値だけを取り出しています。

課題

自分で計算結果を持ち回るばあいの問題は、計算結果の管理が必要になる点です。

たとえば FibMemo.get(memo, 10) で 1 〜 10 までの計算結果を記憶した memo10 を取得したにもかかわらず、FibMemo.get(memo, 40) のように初期化で取得した memo の値を使ってしまうと、10 までの計算をもう一度実行してしまいます。

iex> memo = FibMemo.initialize()           # memo は 1, 2 に対応する数を記憶している
iex> {f10, memo10} = FibMemo.get(memo, 10) # memo10 は 1〜10 に対応するを記憶している
iex> {f40, memo40} = FibMemo.get(memo, 40) # memo は 1, 2 に対応する数しか記憶していないので、3〜10 をふたたび計算する

本来注目している値の以外に計算結果の記録が式の中に混在していることが、煩雑になる一つの原因のようです。

そこで、計算結果を記録する部分を別のプロセスに分離して、注目している値の取得に専念できるようにしてみます。

別のプロセスで記憶する

ここでは Elixir の標準ライブラリにある Agent というモジュールを利用します。

Agent は新しいプロセスを生成し、Agent.get_and_update 関数が呼び出されたばあい、前段の結果を入力として二つの計算結果の組みを返します。 一つは関数の戻り値になり、もう一つは次にこの関数を呼び出されたときの関数の入力になります。

またプロセスの識別にはプロセス ID を利用します。

記憶そのものには先ほど実装した FibMemo を引き続き利用し、ここではプロセス制御のためのコードだけを追加します。

defmodule FibAgent do
  def start do
    # プロセスを作成する
    # 初期値は FibMemo.initialize() の戻り値
    # 作成に成功すると :ok とプロセス ID のタプルを返す
    Agent.start(fn -> FibMemo.initialize() end)
  end

  def get(pid, n) do
    Agent.get_and_update(
      # 第1引数: プロセス ID
      pid,
      # 第2引数: memo を受け取り f と新しい memo の組みを返す関数
      # f は get_and_update の戻り値になり、新しい memo は次回呼び出される関数の引数になる
      fn memo -> FibMemo.get(memo, n) end
    )
  end
end

fib_agent.ex という名前で保存して iex で確認してみます。 引き続き fib_memo.ex も利用するので c にはリストで二つのファイル名を渡します。

$ iex
iex(1)> c ["fib_memo.ex", "fib_agent.ex"]
[FibAgent, FibMemo]
iex(2)> {:ok, pid} = FibAgent.start()
{:ok, #PID<0.125.0>}
iex(3)> FibAgent.get(pid, 1)
1
iex(4)> FibAgent.get(pid, 2)
1
iex(5)> FibAgent.get(pid, 3)
2
iex(6)> FibAgent.get(pid, 10)
55
iex(7)> FibAgent.get(pid, 40)
102334155

考察

別プロセスに記憶をまかせることで、計算結果を持ち回る煩わしさからは解放されましたが、取得したい結果のための引数の他にもう一つ引数が必要な状況には変わりません。

しかし実際のところオブジェクト指向言語でも、オブジェクト自身を指し示す selfthis を暗黙の引数として渡しているという事実があります。

たとえば Rust では、構造体とメソッドの実装は分離していて、メソッドの実装では self を明示します。 Rust はオブジェクト指向言語ではないので説明には不適かもしれませんが、self を受け渡している様子がよくわかると思います。

use std::collections::HashMap;

#[derive(Debug)]
struct Fib {
    memo: HashMap<u32, u32>,
}

impl Fib {
    fn new() -> Fib {
        let mut fib = Fib{memo: HashMap::new()};
        fib.memo.insert(1, 1);
        fib.memo.insert(2, 1);

        fib
    }

    fn get(&mut self, n: u32) -> u32 {
        match self.memo.get(&n) {
            Some(f) => *f,
            None =>
            {
                let f1 = self.get(n - 1);
                let f2 = self.get(n - 2);
                let f = f1 + f2;
                self.memo.insert(n, f);
                f
            }
        }
        
    }
}

fn main() {
    let mut fib = Fib::new();
    println!("{:?}", fib);

    println!("{}", fib.get(1));
    println!("{}", fib.get(2));
    println!("{}", fib.get(3));
    println!("{}", fib.get(10));

    println!("{:?}", fib);
}
$ cargo run
Fib { memo: {2: 1, 1: 1} }
1
1
2
55
Fib { memo: {9: 34, 2: 1, 5: 5, 3: 2, 8: 21, 4: 3, 1: 1, 7: 13, 6: 8, 10: 55} }

実は。 Elixir で、パイプ演算子を使って引数と関数の記述の順序を入れ替えると、メソッド呼び出しと似たような記述になります。

iex(1)> c ["fib_memo.ex", "fib_agent.ex"]
[FibAgent, FibMemo]
iex(2)> {:ok, fib} = FibAgent.start()
{:ok, #PID<0.125.0>}
iex(3)> fib |> FibAgent.get(1)
1
iex(4)> fib |> FibAgent.get(2)
1
iex(5)> fib |> FibAgent.get(3)
2
iex(6)> fib |> FibAgent.get(10)
55

import を使ってモジュール名の装飾を省略して関数を記述できるようになると、コード上の差異はさらに少なくなります。

iex(7)> import FibAgent
FibAgent
iex(8)> fib |> get(10)         
55
iex(9)> fib |> get(40)
102334155

オブジェクトとプロセスという、異なるものを扱っているということを意識する必要はありますが、記憶する側と利用する側の分離という文脈で考えたときに、似たような構造が見えてくるのが面白いところです。

プログラムで記憶する〜Prolog

オブジェクトで記憶する。 プロセスで記憶する。 他に計算結果を記憶する方法がないかと探してみました。 そしてどちらとも異なる方法で実現できるものを見つけました。

はい、Prolog です。

記憶しない実装

ここでもまず愚直な実装から。 処理系は今回も GNU Prolog を利用しています。

fib_naive(N, _) :- N < 1, !, false.
fib_naive(1, 1) :- !.
fib_naive(2, 1) :- !.
fib_naive(N, F) :-
  N1 is N - 1,
  N2 is N - 2,
  fib_naive(N1, F1),
  fib_naive(N2, F2),
  F is F1 + F2.

説明は省略しますが、Prolog は真偽を判定する述語でプログラムを実装します。 そして引数に変数を与えたばあいは述語の式が真になるような変数の値を返します。 このコードのばあい、第 2 引数に変数を与えると、述語 fib_naive が真になるような変数の値を得ることができます。

fib_naive.prolog という名前で保存して、GNU Prolog のコマンド gprolog を起動します。 プロンプト | ?- に続いて ['fib_naive.prolog']. を入力してソースコードを読み込みます。

$ gprolog
| ?- ['fib_naive.prolog'].
yes

第 1 引数に数値を与え、第 2 引数に変数 F を与えて式を評価すると、式が真になる F の値が得られることがわかります。

| ?- fib_naive(1, F).
F = 1
yes
| ?- fib_naive(2, F).
F = 1
yes
| ?- fib_naive(3, F).
F = 2
yes
| ?- fib_naive(10, F).
F = 55
yes
| ?- fib_naive(40, F).
Fatal Error: global stack overflow (size: 32768 Kb, reached: 32765 Kb, environment var
iable used: GLOBALSZ)

40 を渡したところでスタックオーバーフローが発生しました。

プログラムを書き換える

最初のコードでは fib_naive(1, 1)fib_naive(2, 1) を最初から定義しました。 定義の数をもっと増やせば、計算せずに結果を取得することができるはずです。

そのようなアイディアのもと、動的に定義を増やすコードを書いてみます。

最初に dynamicfib_memo/2 が動的な定義であることを宣言します。 ちなみに /2fib_memo が 2 引数の述語であることを表しています。

さらに計算の最後に asserta を使って新しい定義を先頭に追加します。 fib_memo は最後の定義で計算をしているので、末尾に追加すると再び計算が実行されてしまうので、定義を先頭に追加するようにしています。

:- dynamic(fib_memo/2).

fib_memo(N, _) :- N < 1, !, false.
fib_memo(1, 1) :- !.
fib_memo(2, 1) :- !.
fib_memo(N, F) :-
  N1 is N - 1,
  N2 is N - 2,
  fib_memo(N1, F1),
  fib_memo(N2, F2),
  F is F1 + F2,
  asserta((fib_memo(N, F) :- !)).

ふたたび gprolog を起動し N = 40 を計算します。 先ほどはスタックオーバーフローになりましたが、今回はきちんと計算できました。

$ gprolog
| ?- ['fib_memo.prolog'].
yes
| ?- fib_memo(10, F).
F = 55
yes
| ?- fib_memo(40, F).
F = 102334155
yes

定義を確認する

計算実行後の定義の状態を findallclause を使って確認してみます。 ここでも詳細は省略しますので、雰囲気だけでも感じてもらえたらと思います。

確認してみると、計算前では 1 と 2 のケースの定義のみですが、計算後では 1 〜 10 のケースも定義されていることがわかります。 なお _ になっているケースは引数を変数で定義している箇所を表しています。

$ gprolog 
| ?- ['fib_memo.prolog'].
yes
| ?- findall([N, F], clause(fib_memo(N, F), _), NFs).
NFs = [[_,_],[1,1],[2,1],[_,_]]
yes
| ?- fib_memo(10, F).
F = 55
yes
| ?- findall([N, F], clause(fib_memo(N, F), _), NFs).
NFs = [[10,55],[9,34],[8,21],[7,13],[6,8],[5,5],[4,3],[3,2],[_,_],[1,1],[2,1],[_,_]]
yes

fib_memo(10, F) を評価したあとの結果は、次のように実装したのと同じ状態になっていることを示しています。 実際、この以下のコードを読み込んで定義を確認してみると、上と同じ結果が表示されます。

:- dynamic(fib_memo/2).

fib_memo(10, 55) :- !.
fib_memo(9, 34) :- !.
fib_memo(8, 21) :- !.
fib_memo(7, 13) :- !.
fib_memo(6, 8) :- !.
fib_memo(5, 5) :- !.
fib_memo(4, 3) :- !.
fib_memo(3, 2) :- !.
fib_memo(N, _) :- N < 1, !, false.
fib_memo(1, 1) :- !.
fib_memo(2, 1) :- !.
fib_memo(N, F) :-
  N1 is N - 1,
  N2 is N - 2,
  fib_memo(N1, F1),
  fib_memo(N2, F2),
  F is F1 + F2,
  asserta((fib_memo(N, F) :- !)).

考察

Prolog には、知識をプログラミングするという側面があります。 人工知能言語とも呼ばれる所以です。

たとえば次のコードは 1 から 10 までのフィボナッチ数を定義したものですが、

fib_memo(1, 1) :- !.
fib_memo(2, 1) :- !.
fib_memo(3, 2) :- !.
fib_memo(4, 3) :- !.
fib_memo(5, 5) :- !.
fib_memo(6, 8) :- !.
fib_memo(7, 13) :- !.
fib_memo(8, 21) :- !.
fib_memo(9, 34) :- !.
fib_memo(10, 55) :- !.

その真偽を「問い合わせ」ることができます。

| ?- fib(1, 1).    % f(1) = 1 は真
yes
| ?- fib(10, 55).  % f(10) = 55 は真
yes
| ?- fib(-1, 0).   % f(-1) = 0 は偽
no
| ?- fib(10, 123). % f(10) = 123 は偽
no

すでにお話ししたとおり、第2引数を変数にしたばあいは変数の値が真になる値を見つけるふるまいをします。

| ?- fib(10, F). % f(10) が真になるためには F = 55 でなければならない
F = 55
yes

ここで findall を使うと、真になる値をすべて見つけることができます。

findall(
  F,
  (
    between(1, 10, N),
    fib(N, F)
  ),
  Fs
).

なお between(L, M, N)NL から M までの間の整数のばあいに真を返す述語です。

実際に評価してみます。

| ?- findall(F, (between(1, 10, N), fib(N, F)), Fs).
Fs = [1,1,2,3,5,8,13,21,34,55]
yes

このように Prolog は、定義をデータベースとして利用しているとも言え、定義を追加することはデータベースにデータを追加する行為に相当します。

構文は違いますが、SQL によるデータベース問い合わせと似ていることがわかると思います。 実際、Prolog の構文はある種のデータベースの問い合わせにも利用されています。

 n  | f  
----+----
  1 |  1
  2 |  1
  3 |  2
  4 |  3
  5 |  5
  6 |  8
  7 | 13
  8 | 21
  9 | 34
 10 | 55 
select
  f
from
  fib
where
  n between 1 and 10
limit
  10;
 f  
----
  1
  1
  2
  3
  5
  8
 13
 21
 34
 55
(10 rows)

テーブルで記憶する〜SQL

計算結果をデータベースで記憶するととらえるのであれば、一般のデータベースでもそれができるんじゃないかという気がしてきますが…実際、できます。 ただし SQL で recursive を使えることが条件になりますが。

SQL の recursive を使って計算結果の行を再起的に連結してゆくことで、数列を取り出す例です。

with recursive fib(f, f1, f2) as (
  select 1, 1, 2
  union all
  select f1, f2, f1 + f2 from fib
)
select f from fib limit 10;
 f  
----
  1
  1
  2
  3
  5
  8
 13
 21
 34
 55
(10 rows)

ここで次のように n を追加して wheren の値を指定すれば、その n に対応する f の値を求めることができます。 このときの注意点として limit 1 を指定しておく必要があります。 これを指定しないと二つ目以降の n = 10 の行を検索し続け、検索が止まらなくなってしまいます。

with recursive fib(n, f, f1, f2) as (
  select 1, 1, 1, 2
  union all
  select n + 1, f1, f2, f1 + f2 from fib
)
select n, f from fib where n = 10 limit 1;
 n  | f  
----+----
 10 | 55
(1 row)

まとめ

ここまで計算結果を記憶する方法を、オブジェクトに記録する以外の方法で考えてみました。 いろいろと比較することで、あまり知られていない側面を見ることができたのではないかと思います。

個人的には。 いろんなコードをたくさん書けたので満足です。


最後に。

株式会社永和システムマネジメントアジャイル事業部はエンジニアを絶賛募集しています。

いろんなコードをたくさん書いて満たされる、プログラミング好きの方のエントリをお待ちしております。

agile.esm.co.jp

Rubyに導入されたparse.yのUniversal Parserをビルドしてみる

こんにちは、構文解析器研究部員の S.H. です。 最近Rubyに導入された parse.yのUniversal Parser 周りに興味がありパッチを投げています。

github.com

github.com

そういった背景からRubyの parse.yのUniversal Parser を頻繫にビルドする機会があり、せっかくなので手順などをまとめてみました。

今回紹介するビルド方法は以下の二つになります。

  • rbenv経由でparse.yのUniversal Parserをビルドする
  • Rubyのソースコードからparse.yのUniversal Parserをビルドする

parse.yのUniversal Parserとは?

今年開催されたRubyKaigi 2023で登壇されたyui-knkさんがRubyに導入されたものです。

導入の背景としては

  • parse.yに記載されている文法ルールを他のRuby処理系へ移植しやすくする
  • parse.yを参照しているRuboCopなどの周辺ツールの追従コストを下げる

などのメリットがあるためのようです。

またparse.yのUniversal ParserをRuboCopなどの周辺ツールで利用する場合、Rubyが提供しているC APIと同じものを自前で実装する必要があります。 そのため以下のようなC APIへの依存を減らすという対応も必要になっています。

github.com

github.com

parse.yのUniversal Parser化についてのより詳しい話は以下のyui-knkさんのスライドが参考になります。

speakerdeck.com

rbenv経由でparse.yのUniversal Parserをビルドする

rbenvを使ってparse.yのUniversal Parserをビルドする場合は以下のようにconfigure用のオプションにRUBY_CONFIGURE_OPTS="cppflags=-DUNIVERSAL_PARSER"を渡します。

$ RUBY_CONFIGURE_OPTS="cppflags=-DUNIVERSAL_PARSER" rbenv install 3.3.0-dev --verbose

--verboseを渡すことでビルドログを表示できるので、そこに以下のようなログが出ていればparse.yのUniversal Parserを使う形でビルドされています。

$ CPPFLAGS = -I/home/sh/.rbenv/versions/3.3.0-dev/include   -DUNIVERSAL_PARSER

Rubyのソースコードからparse.yのUniversal Parserをビルドする

前提条件や環境など

手順

作業用のディレクトリを作成する

まずは作業用のディレクトリを作成します。

$ mkdir rubydev

次に作業用ディレクトリへ移動します。

$ cd rubydev

CRubyのソースコードをcloneする

Rubyのソースコードを手元にcloneします。 cloneの方法は以下のどちらでもOKです。

# ssh
$ git clone git@github.com:ruby/ruby.git

# https
$ git clone https://github.com/ruby/ruby.git

CRubyでUniversal Parserを使う形でビルドする

cloneしてきたRubyのソースコードのディレクトリまで移動します。

$ cd ruby

次に./autogen.shrubyディレクトリ内で実行し、Ruby自体をビルドするために必要な設定などを生成します。

$  ./autogen.sh

次にrubydevディレクトリ配下にbuildというビルド用のディレクトリを作成し、そこへ移動します。

$ mkdir ../build && cd ../build

buildディレクトリ内で以下のコマンドを実行し、parse.yのUniversal Parserを使うようにconfigureでフラグを渡します。

$ ../ruby/configure --prefix=$PWD/../install --enable-shared cppflags=-DUNIVERSAL_PARSER

あとはmake all installコマンドを実行すればparse.yのUniversal Parserが有効になったRubyがビルドできます。

$ make all install

まとめ

現在ホットなトピックである parse.yのUniversal Parser を使ったビルドする方法を紹介しました。

今後 parse.yのUniversal Parser が発展していくことで

  • JRubyなどの他のRubyの処理系が新しいRubyの文法に追従しやすくなる
  • 新しいRubyの処理系が開発しやすくなる
  • RuboCopなどの周辺ツールの文法ルールのメンテナンスコストが削減できる

などの恩恵が受けられそうです。

またparse.yのUniversal Parserをより使いやすいものにするために C API への依存を減らす取り組みも進んでいます。 この記事を読んでparse.yのUniversal Parserに興味を持った方は、ぜひ僕と一緒にparse.yのUniversal Parserへコントリビューションしていきましょう!

参考資料


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発と構文解析器の研究を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

【入社エントリ】はじめまして、まいむです。

こんにちは!2023年8月に中途入社をしましたまいむと申します。

入社エントリとして、自己紹介、入社に至るまで入社後に感じたことなどを書いていきたいと思います。

自己紹介

改めまして、まいむと申します。前職では自社開発企業でPMや上流工程の担当をしていました。前職でもエンジニアの方々と日々一緒に仕事をしており、いつの間にか自分もプログラミングに興味を持ち始め、独学でRubyの勉強をしていました。

その後、仕事として自分自身も開発に関わりたいと思うようになりフィヨルドブートキャンプというプログラミングスクールで1年ほど学習してキャリアチェンジをした形になります。

永和システムマネジメントで働きたいと思ったきっかけ

私が永和システムマネジメントを知った一番初めのきっかけは前述のフィヨルドブートキャンプ(以降FBCと記載)で開催された卒業生LT会を見たことでした。そこではFBCを卒業されて永和システムマネジメントで働かれているima1zumiさんとふーがさんが発表をされていました。

このタイミングでは社名を知っただけでしたが2ヶ月後にFBC内の合同企業説明会で永和システムマネジメントの取り組みについて説明を聞き、社員の方々とお話をしてみた結果、この会社で働きたい!という気持ちが自分の中にむくむく湧いてきたのでした。

なぜ永和システムマネジメントで働きたいと思ったのか

理由は大きく分けて3つあります。

1. 働いている社員の方々に魅力を感じた

FBCの合同企業説明会にはたくさんの永和システムマネジメントの社員の方々が参加されていて、説明会後のフリートークの時間に質問や雑談をしたりすることが可能でした。その際、私は参加されていたほぼ全員の方と話した記憶なのですが、一方的に親近感を感じてこの方達と一緒に働きたいな〜と思いました。

永和システムマネジメントの社員の方々はRubyコミュニティにも積極的に参加されている方も多く、そういったイベントなどでもお話しする機会を何度か経て、一緒に働きたい!という気持ちは気のせいじゃないなと実感していきました。

2. チームで働くことを大切にしている

Webサービスの開発をするにあたり、そこに関わる人たちはエンジニアだけではありません。サービスやプロジェクトの規模によって変動はあると思いますが、それぞれの役割を抱えた立場の違う人達が一緒に仕事をすることで成り立っていると思います。

永和システムマネジメントと聞くと技術が大好きな人達が多数在籍している企業のイメージがあるかもしれませんが(これは正しいですが!)、社員の方とお話しすると皆さんが必ず「チームで働くことを大切にしている」と話してくれたのがとても印象に残っています。

自分もPMの経験からチームで働くということをとても意識していたため、働く姿勢にとても共感しました。

3. 技術について話している時、とても楽しそう

入社前後でこれは変わらない部分ですが、やはり社員の方々は技術が大好きで皆さん日々技術と向き合っているなと感じています。

開発をするにあたって裏では大変なこともたくさんあると思いますが、技術関連の話題について話をしていると皆さんが活き活きしているというかとても楽しそうです。いろいろな考え方や働き方があると思いますが、好きこそものの上手なれという言葉がある通り、この環境ならば仕事としての開発経験プラスアルファの経験が積めそうだなと感じたことも永和システムマネジメントで働きたいと思ったことの一つです。

入社後の日々

この会社で働きたい!と思った最初の直感は正しかったな〜と実感しています。

まだ入社して2週間しか経っていないため、慣れていない部分がたくさんありますが、日々社員の皆さんに支えていただきながら少しずつ永和システムマネジメントで働く解像度が上がっています。やりたいこと今後の課題なども段々見えてきたため、開発者として一人前になれるようこれから頑張っていきたいと思います!

よろしくお願いいたします!!


永和システムマネジメントでは Ruby やアジャイルを使ってより良い社会をともに目指していく仲間を募集しています。

agile.esm.co.jp