やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【Elixir/supervisor入門】通常のモジュールに対してsupervisorを定義する

なぜ書いたのか

Elixirにおいてプロセスの死活管理に用いるsupervisorについて学習を進めていたが、どれもGenServer、すなわちElixirbehaviorを用いたサンプルばかりだった。また、そのプロジェクトはmix new --sup project_nameとして生成されるもので、後から「supervisor入れたい」となる事が割とあったが自前でmixプロジェクトにsupervisorを定義する方法についてよく分かっていなかった。

気軽に自身で作成したElixirbehaviorを用いないモジュールに対してsupervisorを定義するための手順がまとまった日本語記事が確認出来なかったので自身のためにこの記事を手順のまとめとして作成した。

今回扱うサンプルについて

プロセスの起動時に特定のメッセージをメインのプロセスに指定回数だけ送信し、上限に達した際にプロセスをkillするという簡単なものを扱う。せっかくなので、message passingが扱えるものを採用してみた。参考にした記事ではアキュムレーターをデクリメントしてカウントが0になったら終了するというシンプルなものだったので少し拡張させた。

medium.com

プロセスの関係性は以下のようになる。
f:id:takamizawa46:20200418124309p:plain:w550

プロジェクトの作成

コードの全体はこちら。
github.com

今回はmixを使わずに、プレーンに作っていくため、まずは適当にディレクトリを作る。

$ mkdir supervisor_sample
$ cd supervisor_sample

次にexファイルを用意。ファイル分割を考えたが手間になるため、今回は1ファイルに全てを記述する。

$ touch supervisor_sample/supervisor_sample.ex

各プロセスにて実行するモジュール内関数

関数名はsupervisorの公式のサンプルにあるような形式に乗っ取ってそれぞれ、start_linkinitとしておくが現在の推奨バージョンでは、特に関数名の縛りはないため自由な命名をすることが出来る。参考にした記事の実装は現在は非推奨 となっているため、一部を書き換えている。

defmodule Child do
  @moduledoc """
    supervisorから起動されるタスクを実行するプロセス
    上限回数まで親プロセスにメッセージを送信して、上限に達したら終了してkillされる
  """

  @doc """
    supervisorから実行される関数。
    supervisorからlinkされたプロセスの生成を行う
  """
  def start_link(receiver, max_send) do
    # launch process link to supervisor process
    pid = spawn_link(__MODULE__, :init, [receiver, max_send])
    {:ok, pid}
  end

  def init(receiver, max_send) do
    # set seed for each process
    IO.puts("Start child with pid #{inspect(self())}")
    Process.sleep(200)
  end
end

これでsupervisorからこのモジュールが呼び出されてタスクを実行するプロセスが立ち上がるようになった。spawn_linkを使っているのはプロセスが死んだことをsupervisorに通知するため。
しかし、このままではプロセスが立ち上がって何もせずに終了してしまうため、initに親プロセスに対してメッセージを送信するための処理を追加する。指定回数まで実行させたいので新たな関数を定義する。

defmodule Child do
  def init(receiver, max_send) do
    # set seed for each process
    IO.puts("Start child with pid #{inspect(self())}")
    Process.sleep(200)
    sender(receiver, max_send)
  end

  @doc """
    上限回数まで再帰的にメッセージを親プロセスに送信する関数
  """
  def sender(_, 0), do: :ok
  def sender(receiver, max_send) do
    send(receiver, {:MESSAGE, "hello! from #{inspect(self())}"})
    sender(receiver, max_send-1)
  end
end

supervisorの起動時に受け取った親プロセスのPIDを元に{:MESSAGE, *本文}を送信する。再帰的に実行し、アキュムレーターの値が0になった時点で再帰を終了する。これでプロセスに実行させる処理の記述が完了した。

supervisorの定義

次に呼び出す元となる親プロセス、すなわちsupervisorの定義をする。ただ単にsupervisorbehaviorに従って実装しただけになる。

defmodule Parent do
  @moduledoc """
    supervisorの定義モジュール
  """
  use Supervisor

  @doc """
    supervisorの起動
  """
  def start_link(receiver, total_process) do
    Supervisor.start_link(__MODULE__, {receiver, total_process}, name: __MODULE__)
  end

  @doc """
    supervisorの設定と戦略をまとめた関数
  """
  def init({receiver, total_process}) do
    children = Enum.map(1..total_process, fn n ->
      %{
        id: "#{__MODULE__}_#{n}",
        # Childのstart_link関数に引数を渡して呼び出す。
        start: {Child, :start_link, [receiver, total_process]},
        restart: :permanent
      }
    end)

    # Aプロセスが死んだ時にAプロセスを復活させる -> 上限は10回(全プロセスで合算)
    Supervisor.init(children, strategy: :one_for_one, max_restarts: 10)
  end
end

実装が必要なのはstart_linkinit命名された2つの関数。start_linksupervisorの起動のための関数で、initsupervisorの設定、戦略をまとめた関数になる。start_linkに関しては特に手を加える部分はないが、第2引数に値を設定することでinit関数に引数を渡すことが可能となる。

init関数でsupervisorで管理するプロセスの設定をしている。設定を定義したmapを要素に持つリストを作成するか、Supervisor.child_spec/2という関数を用いる。どんな設定が出来るのかは下記に記述されている。特に重要なのはstart:の部分で、ここにsupervisorから起動させたいモジュールと関数名、引数値を指定する。TaskAgentで指定する形式と同じなので特に困惑する部分はない。

Supervisor.initではsupervisorの戦略を定義する。今回は特殊な設定はしておらず、死んだプロセスを1つ1つ都度、復活させる*one_for_one、最大復活プロセス数は10とした。

これにて実行するプロセスとsupervisorの設定が可能となった。

実行ファイル

最後に作成したsupervisorを気軽に呼び出すため、送信されてきたメッセージを受信するための再帰receiveを実装したwrapperモジュールを用意する。

defmodule SupervisorSample do
  @moduledoc """
    supervisorの呼び出しとメッセージの受信をサボるためのwrapperモジュール
  """

  @doc """
    supervisorの起動と再帰的メッセージ受信ループを実行
  """
  def launch(total_process) do
    # launch supervisor
    Parent.start_link(self(), total_process)
    receiver()
  end

  def receiver() do
    receive do
      {:MESSAGE, content} ->
        IO.puts("received message!: #{inspect(content)}")
        receiver()
    end
  end
end

実行結果

iexを立ち上げて、.exファイルをコンパイルして実行する。

$ iex

iex(1)> c("supervisor_sample.ex")  
[Child, Parent, SupervisorSample]  

今回は簡単のために立ち上げるプロセスは2つとして実行する。実行すると以下のようなlogが出力される。

iex(2)> SupervisorSample.launch(2)
Start child with pid #PID<0.116.0>
Start child with pid #PID<0.117.0>
Start child with pid #PID<0.118.0>
Start child with pid #PID<0.119.0>
received message!: "hello! from #PID<0.116.0>"
received message!: "hello! from #PID<0.116.0>"
received message!: "hello! from #PID<0.117.0>"
received message!: "hello! from #PID<0.117.0>"
received message!: "hello! from #PID<0.119.0>"
received message!: "hello! from #PID<0.119.0>"
Start child with pid #PID<0.120.0>
Start child with pid #PID<0.121.0>
received message!: "hello! from #PID<0.118.0>"
received message!: "hello! from #PID<0.118.0>"
Start child with pid #PID<0.122.0>
Start child with pid #PID<0.123.0>
received message!: "hello! from #PID<0.120.0>"
received message!: "hello! from #PID<0.120.0>"
received message!: "hello! from #PID<0.121.0>"
received message!: "hello! from #PID<0.121.0>"
received message!: "hello! from #PID<0.122.0>"
Start child with pid #PID<0.124.0>
Start child with pid #PID<0.125.0>
received message!: "hello! from #PID<0.122.0>"
received message!: "hello! from #PID<0.123.0>"
received message!: "hello! from #PID<0.123.0>"
received message!: "hello! from #PID<0.124.0>"
Start child with pid #PID<0.126.0>
Start child with pid #PID<0.127.0>
received message!: "hello! from #PID<0.124.0>"
received message!: "hello! from #PID<0.125.0>"
received message!: "hello! from #PID<0.125.0>"
received message!: "hello! from #PID<0.126.0>"
** (EXIT from #PID<0.104.0>) shell process exited with reason: shutdown

こいつをElixirで整形して何回プロセスが再起動されているかを確認してみると12となっている。

log =
"""
Start child with pid #PID<0.116.0>
:
:
received message!: "hello! from #PID<0.126.0>"
"""

log
|> String.split("\n")
|> Enum.filter(fn s -> String.starts_with?(s, "Start child with") end)
|> length()
|> IO.puts() # 12

これは最初に起動される2つのプロセスは当然ながら再起動のカウントに含まれていないということを意味している。どうやら上手くsupervisorが動作しているようだ。

github.com

参考文献

【収益ほぼゼロ】何の考えもなしにブログ100記事を書き終えたので反省会をしました

100記事を書くことが凄いのか

f:id:takamizawa46:20200413225135p:plain
よく著名なブロガーが「まずは100記事書くといいですよ」ということを仰っている。なぜ100件なのかという根拠はないが、意図としては100も記事を書けばブログを書く習慣も身に付き、言葉の扱い方も多少は様になってくる...ということだと考えられる。あとはとりあえず始めることが重要なんだとか。

正確な数字は確認出来なかったが、ブログを始めて100記事の執筆に到達する前に全体の90%近くは挫折して、継続不可能になるのだそう。いわゆるブログは無理ゲーと言われる所以。そう言われてみると、自分が達成したブログ100記事の執筆は凄いことなのかもしれない。
そういえば、プログラミングの独学の継続率も確かそのぐらいの数字だった気がする。自分は独学で学習をして本業にするところまで辿り着けたが、当時、自分の周りでプログラミングをやっていた人は確かに全滅した。自分が唯一のサバイバーだった(気がする)。

そんなこともあり、ブログのジャンルとしては本業のエンジニアリングに関連する技術や、試した事を中心にたまにレビューなんかや、所感についてまとめたものを執筆してきた。

なぜ100記事書けたのか

今まさにブログを始めたばかりでブログをこれから書き進めていく方や、一度ブログを挫折した方に自分がなぜ100記事も書けたのかをより詳細に話しておこうと思う。

おそらく、この記事にたどり着いた方がの多くは「ブログを書いて稼ぎたい」という考えの人が多いと思う。自分ももちろん「稼げるに越したことはない」と思っているし、ハッキリ言って稼ぎたい。そりゃもちろん。

しかし、ブログを始めた当時のマインドと100記事を執筆し終わった今に至るマインドの多くは

  • 文章を書くのが好き
  • 技術のインプットが好き

というもの。収益のために書いたという記事はほんの数記事しかない。

実は過去にブログを挫折済み

偉そうに「100記事も執筆してやったぜ」と豪語しているものの、実はブログに関しては過去に一度、挫折をしている。それは大学の2年生の時で当時は「ガリガリ男が筋トレする」というコンセプトでブログをWordPressを使って運営していた。

www.okb-shelf.work

ブログを立ち上げた当初は順調で、毎日のように書く記事が思いついた。20記事程度、執筆したところでGoogle Adsenseの審査が通り、ブログ内に広告の配置をすることも出来た。その後、ゆるやかに執筆頻度が落ちていき50記事に到達する前に、あえなく挫折してしまった。

なぜ挫折したのかという理由の詳細は上の記事でも話してるが改めて、100記事を執筆し終わった今思うこととしては以下の3つが良くなかったと思う。

  • 筋トレという知識がほとんどない分野を攻めた
  • 筋トレの効果が現れるまでにかなり時間があるため、記事にしにくい
  • 稼ぎたいという欲が先行して作業的になってしまった

一言で言うとジャンル選びとモチベーションのキープが非常に重要。書き続けても苦にならない、毎日情報収集したくなるようなジャンルを選ぶ事をオススメしたい。

とりあえず書きたいことをひたすら書いた

色々と言われているが、ただ記事を執筆してもダメでユーザーが必要としているテーマを選定して、Googleの検索ページの1ページに表示されるように努めるというのが、いわゆる良い記事のことだろう。90記事を書くあたりで、上記のようなことを意識をし始めるようにはなったが、それまでは何の考えもなしにひたすらに書きたい記事を書きたい時に書き続けていた。

(ただ書くだけではダメだと気づいた記事)

www.okb-shelf.work

気づけば、需要があるのかないのかよく分からないテーマの記事が大量に出来上がった。自分にとっては学習のアウトプットになっているし、好きなテーマで書けているので幸福度が非常に高い。振り返ってみると自分の書いた記事はかなりニッチを攻めている。日本語で同じような記事を書いている人は恐らくいない。

www.okb-shelf.work

www.okb-shelf.work

100記事書いて得られた知見・能力

100記事の執筆が終わった時点で個人的に得られたと思う知見について4つほど共有しておこうと思う。

文章を書くことへの習慣

一番、大きいと感じるのはこれ。中々、普段これだけの文量のデータを作ることはないが、ブログを書いていれば否が応でも文量は多くなる。結果的に書いた文量というのは一般的な人と比べるとかなり多いと思う。

文量が多くなれば、ボキャブラリーも増えるし、より良い言葉を選ぶようにもなってくる。何よりも恩恵を感じるのは業務でのチャットコミュニケーション。他の人たちの文章が稚拙とは言わないが、明らかに自分の文章とレベルが違うと感じる。これはブログを習慣的に書き始めてから気づくようになった。まぁ、自分の文章も別に上手いとは思わないが、少なくとも素人では無くなった。

技術に関する知識

自分にとってブログを書くことはイコール、技術のアウトプットになっている。新しく覚えたことを抽象化して記事にまとめる、この作業の繰り返し。つまり記事数が多いと言うことはこのルーティーンを多く行ってきたということになる。

ブログを始めた当初と比べると技術的な知識もかなり増えたと感じる。濃い一年を過ごすことが出来た。合わせて、ブログがポートフォリオとなっているし、転職する際や自己紹介をする際にもブログのリンクを貼っておけば、何をやってきて何に興味があるのかを言葉ではなくデータで提示することが出来る。

エンジニアがブログなりアウトプットをやらないのは非常にもったいない。文章が下手だろうが、コードに自信がなくても続けていればマシになってくるのでくすぶっているなら絶対にやったほうがいい。

Rubyの作者である「まつもとゆきひろ」さんもエンジニアのアウトプットには言及している。

インプットは必要、でも差別化要因にならない
しかし、アウトプットすることで差別化になる

引用
qiita.com

PVを伸ばしたいならただ書くだけではダメ

ただ書いていてはテーマが良くない限りはPVは絶対に増えていかない。PVが増えないということは一般的に言えば、収益も発生しにくい。ブログで収益を得たいと考えている方にとってはこの負のサイクルは致命的になる。

先ほども少し触れたように、PVを伸ばす記事を書くには以下3点のようなことを少なくとも意識する必要がある。

  • ユーザーが抱えている課題を解決するための記事となっていること
  • Googleの検索結果の1ページに出るような構成・文字量etcにすること
  • 競合が多い分野を分析し、チャンスのある分野を攻めること

詳しいことは当ブログでは解説しないが、「ブログ PV増やし方」とでも検索すれば、著名な結果を出しているブロガーのSEO対策の情報を見ることが出来るのでそちらへ。少なくとも、自分が感じたことは、かなりニッチなジャンルを扱っているのであれば、何の考えもなしにブログを書いても絶対にPVは増えていかないということ。

f:id:takamizawa46:20200414095038p:plain

単語が持つパワーを理解することも非常に重要になる。PythonElixirでは元々の単語が持つパワーが違いすぎる。Pythonについて書かれた記事一つのPVに対してElixirについて書かれた100記事のPVを合わせても敵わないというのが現実である。事実、Pythonjanomeを使う方法をまとめた記事が当ブログでは1番のPV稼ぎ頭となっている。

(これがその記事)

www.okb-shelf.work

あと収益を作りたいならページにアフェリエイトリンクへの導線を作ることも非常に重要。この記事に「ブログの始め方」という記事へのリンクや、レンタルサーバーのアフェリエイトリンクもないことから「あ、こいつ、収益出せていないんだなー」と察してもられば。

内容よりも誰が書いたかの方が重要

これはnoteに始めて有料記事を投稿した時に感じたこと。名前の知られていない者が書いた内容の濃い記事よりも、著名な方が書いた決しても内容の濃くない記事(個人的な感想で批判ではないのでご注意)の方が影響力はでかいしPVも桁違いになる。

(めちゃくちゃ熱を込めて書いたので良ければぜひ)
note.com

今の歌謡業界に通ずるところはあり、どんな歌を歌っているかよりも、誰が歌っているかの方が重要ということと同じなのだろう。

プログラミングの未経験者向けに入門のための熱い記事を一つ書くよりも著名な方が書いたプログラミング初心者向けに書かれたスクールへの誘導記事の方が見られる。つまり、セルフブランディングも非常に重要で自分のキャラクターを作って名と共に信頼度を上げていく必要があると感じた。

最後に

文字を書く、すなわちブログは自分の肌に合っていた。アウトプットのために使うという目的も良かった。純粋に書きたいという思いだけで続けることが出来た。しかし、最近はせっかくなので、書いた記事を見てもらいたいという思いが強くなっており、テーマ、キーワード選定に意識を置くようになってきた。

100記事執筆したという自信を持って、少しは収益のために記事を書いてもいいんじゃないかと考えている。

本編としては以上で終了。 以降はおまけでせっかくなので一年間続けて、どれだけの収益とPVを得られたのかを公開しておこうと思う。

収益とPVについて

まずはPVから。よく100記事書きました系の記事で見られるPV数の推移は指数関数のようになっているが、当ブログは一定のPV数を上下しているのみで、全くPV数が増加して行く傾向がない。よく続いているなぁと改めて思ってしまった。

f:id:takamizawa46:20200414211843p:plain

ちょっとプチバズりがある時は、twitterに記事を投稿したところ、多くの方に拡散してもらえた時だったり、最近だとnoteのはてなブログのリンクを貼った時にかなりPVが増える日があった。それも常習的に発生するわけではないので、常にPV数がその状態の数値になるわけではない。

次に収益について。一応結果は出ているが、前に運営していたブログで発生した収益もいくらか含まれているので実質は1000円程度がこのブログで発生した収益になる。トホホ...

f:id:takamizawa46:20200414212133p:plain

最近は記事のリライトやタイトルを作成し直してPV数が増えるような施策を行っているが目に見えて変化を感じることは出来なかった。残念。

参考文献

【golangのサンプル有り】クロージャ(closure)完全に理解した人のためのクロージャを使った便利サンプル集

クロージャ完全に理解した...で何に使うの?

実は使いこなせると結構便利。特定の値に対する操作を共通化することが出来て、想定しているもの、言い方を変えれば作者の作成しているもの以外の操作を制限することも出来る。あと、かっこいい。
クロージャユースケースとしては小さなデータベースに対してコマンド経由で何かをしたい時と考えると分かりやすい。今回はそのサンプルを4つ用意したので、便利さを体感してみてほしい。1つでも刺さるものがあれば嬉しい限り。

サンプル集

設計方針としては2つの考えをベースにしている。

  • 初期値となる変数の用意(データ構造によってやりたいことを考える)
  • 用意した初期値に対する操作(取得, 更新, 削除)を行うコマンドを引数で受け取る無名関数を作成してコマンドの判定を行い、それぞれに対応する処理を記述する

クロージャを使ったタイマー

動作確認はこちらから
play.golang.org

// closure timer -> 経過時間をsecondsで返す
func ClosureTimer() func(c string) float64 {
    start := time.Now()
    return func(command string) float64 {
        switch command {
        case "GET":
            return time.Now().Sub(start).Seconds()
        case "STOP":
            return time.Now().Sub(start).Seconds()
        case "RESET":
            start = time.Now()
            return 0
        }
        return 0
    }
}

実行結果

func main(){
    // 新たなtimerを作成
    newTimer := ClosureTimer()
    time.Sleep(1 * time.Second)
    // 経過時間が1sとなるはず
    fmt.Println(int(newTimer("STOP")))
    
    newTimer("RESET")
    // timerがリセットされて0sとなるはず
    fmt.Println(int(newTimer("STOP")))
}
1
0

高階関数を使って関数内のローカルなスコープを持つ変数、この場合はClosureTimerを呼び出した時の時刻を初期値として保持しておく。この時刻と現在時刻との差分を返すのが1番の目的(GET or STOP)となるが、その他に比較時間のリセットのためのRESETというコマンドも用意してある。switch caseを増やせばいくらでも拡張できるため、都度都度、時刻時間の初期値を用意して差分を出して...値をリセットして...という変数管理をコマンドだけで行うことが出来る上にコードも共通化することが出来る。
自分は関数の実行時間を測る際にパッケージに記述したこの関数をよく使っていた。

クロージャを使ったカウンター

動作確認はこちらから

play.golang.org

func ClosureCounter(init, inc int) func(c string) int {
  count := init
  return func(command string) int {
    switch command {
      case "ADD":
        count += inc
        return count
      case "RESET":
        count = init
        return count
      case "GET":
        return count
    }
    return -1
  }
}

実行結果

func main(){
    // 新たなcounterを作成
    counter := ClosureCounter(0, 1)
    // 初期値の確認
    fmt.Println(counter("GET"))
    for i := 0; i < 10; i++ {
      counter("ADD")
    }
    
    // 正しくカウントされているかどうか
    fmt.Println(counter("GET"))
    // 値のリセット
    fmt.Println(counter("RESET"))
}
0 
10
0

やっていることは先ほど全く同じでデータ構造がintに変わったのみ。ClosureCounterを使うことでインクリメント変数の用意と管理を先ほどと同じように共通化、コマンド経由でのみ実行することが出来るため非常に便利。動作させているスレッドのカウントや、集計処理で条件にマッチしたデータ数をカウントするのによく使っていた。

クロージャを使ったkvs(キーバリューストア)

いわゆるredisに近いことがスコープが有効な範囲でやれる。

動作確認はこちらから
play.golang.org

func ClosureKVS() func(c string, k string, v interface{}) interface{} {
  init := make(map[string]interface{})
  return func(cmd string, k string, v interface{}) interface{} {
    switch cmd {
      case "ADD":
        init[k] = v
        return nil
      case "GET":
        res := init[k]
        return res
      case "RESET":
        init = make(map[string]interface{})
        return nil
      case "KEYS":
        resp := make([]string, 0)
        for k, _ := range init {
          resp = append(resp, k)
        }
        return resp
      case "VALUES":
        resp := make([]interface{}, 0)
        for _, v := range init {
          resp = append(resp, v)
        }
        return resp
    }
    return nil
  }
}

実行結果

func main(){
    // 新たなkvsを作成
    kvs := ClosureKVS()
    
    // key valueを追加
    kvs("ADD", "okb", "cool")
    fmt.Println(kvs("GET", "okb", ""))
    
    // さらにkey valueを追加
    kvs("ADD", "bko", "bad")
    fmt.Println(kvs("GET", "bko", ""))
    
    // 現在登録されているkeyを全取得
    fmt.Println(kvs("KEYS", "", ""))
    
    // 現在登録されているvalueを全取得
    fmt.Println(kvs("VALUES", "", ""))
    
    // 一度kvsをリセット
    fmt.Println(kvs("RESET", "", ""))
    
    // リセットが問題なく行われたかを確認
    fmt.Println(kvs("KEYS", "", ""))
}
cool
bad
[okb bko]
[bad cool]
<nil>
[]

正直、interface{}型ってあんまり使うのは好きではないのですが、半ばしょうがなく採用。keyが常にstring型なのは良いとしてvalueの値の型を固定すると、都度都度、対応する型のkvsを作る必要があるのでこんな形となった。

クロージャを使ったキュー

ここまで来たらもう何でも出来そう。やっぱりコマンド経由で...(ry

動作確認はこちらから
play.golang.org

func ClosureQueue() func(c string, v interface{}) interface{} {
  queue := make([]interface{}, 0)
  return func(cmd string, v interface{}) interface{} {
    switch cmd {
      case "QUEUE":
        queue = append(queue, v)
        return true
      case "DEQUEUE":
        if len(queue) > 0 {
          head := queue[0]
          queue = queue[1:len(queue)]
          return head
        }
        return nil
      case "SIZE":
        return len(queue)
    }
    return nil
  }
}

実行結果

func main(){
    // 新たなキューを作成
    queue := ClosureQueue()
    queue("QUEUE", 1)
    queue("QUEUE", 2)
    queue("QUEUE", 3)
    
    fmt.Println(queue("SIZE", ""))
    
    fmt.Println(queue("DEQUEUE", ""))
    fmt.Println(queue("DEQUEUE", ""))
    fmt.Println(queue("DEQUEUE", ""))
    
    fmt.Println(queue("DEQUEUE", ""))
    
    queue("QUEUE", 99)
    
    fmt.Println(queue("DEQUEUE", ""))
}
3
1
2
3
<nil>
99

最後に

どうでしょうか。クロージャも使ってみると意外とやれることが多いことに気づいてもらえれば何より。やはり値に対する操作を制限出来るというのが良い。想定外が発生しにくくなるし、共通化もされるため、インクリメント処理などを後から修正する事になっても、クロージャを使って共通化しておけば退屈な修正は最小限に済むだろう。

参考文献

【goroutineによる並行処理】サンプルを作りながら学ぶgoroutine入門

golangを学ぶ上での壁goroutine

golangを業務で使い始めてから約半年になりました。業務ではgolangを使ってWebsocketを使ったチャットサーバーを作っています。golangの文法は非常にシンプルで分かりやすくシンプルなので、可読性が非常に良いです。普通にgolangを逐次処理として記述する中では大した問題は発生しないかと思いますが、golangの何が難しいのかというとgoroutineと呼ばれるgolangに実装されている並行処理をするための軽量なスレッドの扱いです。

go-tour-jp.appspot.com
(concurrencyって公式が言っているので並行処理なんでしょう)

goroutineの文法自体は非常にシンプルで実行したい関数の前にgoと記述するのみです。

package main
import (
  "fmt"
  "time"
)  
  
func main(){
    // Your code here!
    go print("hello")
    go print("world")
    
    // 先にメインスレッドが終了してしまうため出力されるまで待機
    time.Sleep(1 * time.Second)
    
}

func print(v interface{}) {
  fmt.Println(v)
}

実行結果

hello
world

play.golang.org

書いたきっかけ

goroutineの習得は元々、並行処理に関する知識があったものの手こずりました。 業務でチャットサーバーを書いており、その大半以上がgoroutineを使ってマルチスレッドでの並行処理になります。なので嫌でもgoroutineを書かなければなりませんでした。おかげさまでgoroutineを使ったgolangでの並行処理はある程度思うように記述出来るようになりました。

ふと「他の人はどうやって学習してんだろ...」と思い「goroutine 入門」と検索してみました。サンプルコードをいくつか見てみましたが、goroutineを使ってfmt.Println("hello")を出すようなものが多い印象を受けました。「うーん、これって面白いのかな」という思いとそのコードから実務に繋がるイメージが湧かなかったので「なんか面白いサンプル作りながら学べる記事があったら最高やな」と思い立って自分で書いてみようという気持ちになりました。

参考としている考え方

並行処理を記述する上でElixirというプログラミング言語で学んだアクターモデルメッセージパッシングといった分散メモリにおけるマルチプロセスの同期方法は非常に参考になりました。今回は詳しくは話しませんが、自分の記述するgoroutineのコードは非常にElixirの影響を受けていると思います。のちに登場します、channelというデータ型はElixirでのメッセージッパッシングのような操作を行うことが出来ます。

そうすることで排他制御や同期処理が非常に楽になり、golangに実装されているsyscパッケージを使うこともほとんどなくなるでしょう。実際に公式のgolangチュートリアル「A Tour of Go」でも言及されています。

sync パッケージは同期する際に役に立つ方法を提供していますが、別の方法があるためそれほど必要ありません。 (次のスライドで説明します)

今回作るもの

何かしらのイベントを駆動して動く処理にしたいなーと思い考えた結果、シンプルで分かりやすいタイマー処理に落ち着きました。今回作るものはusedTimerと勝手に名付けたものです。

usedTimerには以下のような仕様があります。

  • 特定の周期A毎にメインスレッドに対して「Golangはいいぞ」というメッセージを飛ばす
  • 特定の周期B毎に時計が壊れる(処理が停止)ため、再起動する必要がある
  • 電気代もかかるため、再起動の上限はN回まで

全体のコードを用意してありますので「どゆこと」という方はまず動かして試してみてください。golangがinstallされているかdockerがinstallされていればすぐに動かすことが出来ます。

github.com

仕様からコードへの置き換え

今は「なんとなーく」で問題ないので、こんな感じで作っていくというイメージを展開しておきます。

まず特定の周期A毎にメインスレッドに対してという仕様に対応するためには、メインスレッドと別にgoroutineを使ってスレッドを用意してあげる必要がありそうです。
さらに立ち上げた別のスレッドから「Golangはいいぞ」というメッセージを飛ばすとあるため周期A毎にメッセージを飛ばす必要があります。スレッド間でメッセージを飛ばすためにはchannelというデータ型を使います。立ち上げたスレッド -> メインスレッドにメッセージを飛ばす処理も記述する必要があるでしょう。

この立ち上げたスレッドは周期B毎に停止をするという処理は記述自体は出来そうですが、故障したことをメインスレッドに伝える必要があります。それは再起動の処理をしてもらうためです。先ほどと同じようにchannelを使ったメッセージを飛ばしてあげれば上手く出来そうな気がします。

補足: スレッド間でメッセージを飛ばすってどういうこと?何のため?

一言で言ってしまえば、スレッド同士でイベントを管理するため、主に同期をとる必要があるためです。AのスレッドからはBのスレッドで何が起きているのか分かりません。共通のグローバルな変数に何が起きているのかを都度、更新しても良いのですが排他制御(順番に書き込める、誰かが書き込んでいる最中は書き込めないようにする...etc)を作る必要があり、非常に面倒です。

そのため、スレッド同士でメッセージを通知することで「あ、Aのスレッドのこの処理が終わったんだ」「あ、Aでこのイベントが始まった」ということを認知することが出来るのです。

作成開始

では早速、作っていきましょう。まずは以下のようにディレクトリとファイルを作成してください。$GOPATH配下に配置するようにお願いします。

main
└ main.go
timer
├ model.go
└ timer.go

メッセージの定義

最初に行うのはスレッド間で送信しあうメッセージの設計です。/timer/model.goに対してメッセージの定義を行います。今回は先ほど記述したようにメッセージを使って2つの通知をする必要があります。

  • Golangはいいぞ」というメッセージを飛ばす
  • 故障したことをメインスレッドに伝える必要

この2つに対応できて、後に上司から「やっぱこんときもメインスレッドにメッセージ飛ばしてくれる?」と追い討ちをかけられても問題ないようにメッセージの定義を行います。メッセージの定義には構造体を用います。

/timer/timer.go

package timer

// スレッド間で用いるchannel用のstruct定義
type Protocol struct {
    // 失敗時のmessageなのかを判定するために用意
    Type    string
    Message string
    Error   error
}

これは業務でチャットサーバーを作り続ける中で洗礼された最終形に近い形のものです。Typeというフィールドに何のメッセージなのかを与えます(eg: 通常のメッセージ, 故障時のメッセージ etc...)。このTypeのフィールドを参照してもらうことで何のメッセージが飛んできたのかを判別することが出来ます。Typestring型なのでメッセージの種類はいくらでも増やすことが出来ます。

Typeに対応するデータをMessageErrorに与えます。通常のメッセージであれば以下のようになります。

msg := &Protocol{
      Type: "MESSAGE",
      Message: "Golangはいいぞ",
    }

しかし、残念なことにせっかく定義した構造体もこのままではスレッド間で飛ばすメッセージとしては使用することが出来ません。先ほど紹介したようにこの構造体をchannel型というデータにしてあげる必要があります。usedTimerを使ってくれる人に「うわーchannel型ってどうやって作るねん」と意識してもらいたくないので、関数で作成出来るようにしておきましょう。

/timer/timer.go

// 構造体を元にチャネルを作成
func CreateProtocolChannel() chan *Protocol {
    return make(chan *Protocol)
}

make(chan *対象の構造体)とすることで構造体をベースとしてchannel型を作成することが出来ます。

timerの作成

スレッド間で飛ばすメッセージの定義が完了したので、ついにメインとなるusedTimerを作成していきます。といっても、やることは単純です。まずは特定周期A毎に「Golangはいいぞ」というメッセージを飛ばす処理までを作成しましょう。
goroutineで立ち上げることを想定しているので関数にします。また、このusedTimer内は3つの引数を受け取ることにします。

  • iterTimeInt -> メインスレッドにメッセージを送る周期Aの秒数
  • breakTimeInt -> usedTimerが故障を起こす周期Bの秒数
  • ptc(protocol) -> メッセージをメインスレッドに送信するためのchannel型

もちろんこれが正解ではないので、他の方法で周期を指定して頂いても構いません。例えば、共通のスコープを持つ変数を用意する方法がありますが、個人的にはあまり好きではないので避けるようにしています。

まずは関数UsedTimerの全体を見てみてください。順に詳細を追っていきます。

// 仕様について
// 1. 指定した周期毎に" "というメッセージをchannel経由で送信する
// 2. 指定した周期毎に時間切れとなり、errorを返して実行loopをbreakする
func UsedTimer(iterTimeInt, breakTimeInt int, ptc chan *Protocol) {
    // int型をtime.Duration型へ変換(second)
    iterTime := time.Duration(iterTimeInt) * time.Second
    breakTime := time.Duration(breakTimeInt) * time.Second

    // 周期毎にイベントを発生させるtickerを作成
    iterTicker := time.NewTicker(iterTime)
    breakTicker := time.NewTicker(breakTime)
    defer func() {
        iterTicker.Stop()
        breakTicker.Stop()
    }()

    for {
        select {
        // メッセージを送信する処理
        case <-iterTicker.C:
            ptc <- &Protocol{
                Type:    "MESSAGE",
                Message: "Golangはいいぞ",
            }
        case <-breakTicker.C:
            ptc <- &Protocol{
                Type:  "ERROR",
                Error: errors.New("[Error] Timer was broken"),
            }
            // breakだとスレッドがkillできないので注意
            return
        }
    }
}

まずは引数で受け取った周期A,Bの値をint型からtime.Duration型に変換します。変換した値を用いて、timeモジュールに用意されているTickerを作成します。Tickerは指定した周期毎に作成したスレッドに対してchannel型を通じてメッセージを飛ばしてくれます。
最後にdefer構文を使用して、スレッドが停止する際(goroutineで実行する関数がreturnする時)にTickerを停止させるようにしておきます。

// int型をtime.Duration型へ変換(second)
iterTime := time.Duration(iterTimeInt) * time.Second
breakTime := time.Duration(breakTimeInt) * time.Second

// 周期毎にイベントを発生させるtickerを作成
iterTicker := time.NewTicker(iterTime)
breakTicker := time.NewTicker(breakTime)
defer func() {
    iterTicker.Stop()
    breakTicker.Stop()
}()

準備は出来ました。次にTickerから周期A,B毎にメッセージを受け取るための処理を記述しましょう。メッセージを受信し続ける必要があるため、多言語で言う所のwhile loopfor構文を用いて作成します。条件を与えなければ意図的に無限ループを作り出すことが出来ます。仮にfor構文がないとどうなるでしょうか。一度、メッセージを受信した後に関数がnilを返すか分かりませんが実行を終了します。その結果、次にメッセージを受信することは出来ません。

for {
        select {
        // メッセージを送信する処理
        case <-iterTicker.C:
            ptc <- &Protocol{
                Type:    "MESSAGE",
                Message: "Golangはいいぞ",
            }
        case <-breakTicker.C:
            ptc <- &Protocol{
                Type:  "ERROR",
                Error: errors.New("[Error] Timer was broken"),
            }
            // breakだとスレッドがkillできないので注意
            return
        }
    }

次に見慣れない構文selectについてです。これはswitch構文のgoroutine版だと思えば良いです。case <- channel型とすることで特定のchannel型を持つメッセージを受信することが出来ます。今回は先ほど作成した2つのTickerを受信するようにしています。
iterTicker.Cでは周期A毎にメインスレッドにメッセージを送るため、breakTicker.Cは周期B毎に故障したことをメッセージとして送るために用意しています。もしやりたことが増えて、第3のTickerを追加した時にはselectにも同じように追加するだけで簡単に拡張することが出来ます。

select {
        // メッセージを送信する処理
        case <-iterTicker.C:
            ptc <- &Protocol{
                Type:    "MESSAGE",
                Message: "Golangはいいぞ",
            }
            :
        case <- newTiceker.C:
            :
            :
        }

次にメインスレッドにメッセージを送る部分についてです。この項目が終了した時点でgoroutineを快適に扱うために必要なchannel型, メッセージの受信, メッセージの受信に触れたことになります。少し練習をすれば、すぐにgoroutineを使った並行処理が書けるようになるでしょう。
メッセージの送信はselectの内部でメッセージを受信した時と書き方が似ています。対象のchannel型に対してch <- データ構造とすることでchannel型を通じてメッセージを送ることが出来ます。今回、メッセージとして定義しているのはProtocolという構造体なので、該当するデータを作成してメッセージを送信します。

// 周期A毎に送るメッセージ
ptc <- &Protocol{
    Type:    "MESSAGE",
    Message: "Golangはいいぞ",
}

// 周期B毎に送るエラーメッセージ
ptc <- &Protocol{
    Type:    "ERROR",
    Message: errors.New("[Error] Timer was broken"),
}

これでgoroutineで実行する関数側の処理は記述が完了しました。あとはgoroutineを管理するためのスケジューラーを記述してあげるだけです。

Schedulerの作成

まずは全体のコードをご覧ください。無名関数を使っている部分はありますが、すでに最もややこしいchannel型でのメッセージ受信部分については触れているので最低限、何をやっているのかは分かるのではないかと思います。

// timerの動作管理を行うスケジューラー。再起動まで行う
func Scheduler(iterTimeInt, breakTimeInt, revivalNum int) {
    // 通信用のチャネルを作成
    ch := CreateProtocolChannel()
    // 何度も再起動させたいので簡略化のため無名関数を採用
    receiver := func() error {
        // 作成したUsedTimerからのメッセージを受け取るloopを作成
        for {
            select {
            case msg := <-ch:
                switch msg.Type {
                case "MESSAGE":
                    log.Print(msg.Message)
                case "ERROR":
                    log.Print(msg.Error.Error())
                    return msg.Error
                }
            }
        }
    }

    // 監視のための処理を記述
    counter := 0
    for {
        // goroutineを用いてスレッドを作成
        go UsedTimer(iterTimeInt, breakTimeInt, ch)
        if err := receiver(); err != nil {
            log.Print("[Main] Restart timer thread. Please wait 3 seconds ...")
            time.Sleep(3 * time.Second)
            counter += 1
        }

        if counter >= revivalNum {
            log.Println("[Main] Reached revival limit. so stop worker ...")
            return
        }
    }
}

最初に立ち上げた別スレッドからメッセージを受け取るためのchannel型のデータを作成します。channel型の作成は先ほどすでに関数化したので、呼び出した値を変数に保持するのみです。

// 通信用のチャネルを作成
ch := CreateProtocolChannel()

今回は、スレッドを特定関数まで再起動するという仕様があるでの繰り返し使うことを想定してchannel型を通じてメッセージを受信する部分を無名関数で使いやすくしてみました。受信の処理は先ほどとほとんど同じですが、1点だけ異なるところがあります。channel型を通じて得たメッセージの値を参照したい時はselectのブロックの中でcase 変数名 := <-channel型:とします。
この場合はUsedTimerから送られてくるProtocol構造体の値がmsgに保持されます。送られきたメッセージに保持されているTypeのfiledを見ることで何のメッセージなのかを判定します。

for {
    select {
        case msg := <-ch:
            switch msg.Type {
        case "MESSAGE":
            log.Print(msg.Message)
        case "ERROR":
            log.Print(msg.Error.Error())
            return msg.Error
        }
    }
}

MESSAGEという周期A毎に送られてくるメッセージを受信した場合には送られてきたメッセージのMessagefieldの値を表示します。ERRORというusedTimerが故障した際に送信されるメッセージを受信した時には無名関数内からreturn errとしてエラーを返して受信ループを停止させます。

この作成したメッセージ受信の無名関数とgoroutineを使って別スレッドの立ち上げとメッセージの受信を行います。

// 監視のための処理を記述
counter := 0
for {
    // goroutineを用いてスレッドを作成
    go UsedTimer(iterTimeInt, breakTimeInt, ch)
    if err := receiver(); err != nil {
        log.Print("[Main] Restart timer thread. Please wait 3 seconds ...")
        time.Sleep(3 * time.Second)
        counter += 1
    }
    if counter >= revivalNum {
        log.Println("[Main] Reached revival limit. so stop worker ...")
        return
    }
}

再起動にはforの無限ループを使いますが、今回は再起動回数に上限があるので、再起動を行なった回数を記録するためのインクリメント変数counterを用意して、再起動するたびに+1します。この値が引数で受け取った上限値を超えた時に処理を終了させます。

再起動は別スレッドからエラーメッセージを受け取った際に行いますが、都度都度、即再起動していては思わぬ負荷をかけてしまう可能性が考えられるため、必ず3秒待機してから再起動するようにもしてみました(eg: usedTimerの壊れる周期が不定期で0.1sごとに壊れるごとが重なるような場合など)。

// 特に問題がなければメッセージの受信ループが発生
if err := receiver(); err != nil {
    log.Print("[Main] Restart timer thread. Please wait 3 seconds ...")
    time.Sleep(3 * time.Second)
    counter += 1
}

これでtimerパッケージの記述が終了しました。最後にmain.goから処理を呼び出します。

/main/main.go

package main

import (
    "log"
    "used_timer/timer"
)

func main() {
    log.Print("[Main] Start timer ...")
    // 2秒毎にlogを出し、起動から10秒経過した時にbreak
    timer.Scheduler(2, 10, 5)
}

動作確認

さっそく動作を確認してみましょう。golangがinstall済みで今回作成したプロジェクトが$GOPATH配下にあるとして以下のコマンドを実行します。

$ cd used_timer $ go run main/main.go

上手く作れていれば、2秒毎に「Golangはいいぞ」というlogが表示されて、10秒毎にusedTimerが故障して「[Error] Timer was broken」というlogが出力されます。一度、スレッドはkillされてしまいますが、3秒後に再び新たなスレッドが作成されて同じようにlogが表示されるようになるでしょう。
この一連の処理が5回まで行われれば成功です。

実行結果

2020/04/07 00:36:48 [Main] Start timer ...
2020/04/07 00:36:50 Golangはいいぞ
2020/04/07 00:36:52 Golangはいいぞ
2020/04/07 00:36:54 Golangはいいぞ
2020/04/07 00:36:56 Golangはいいぞ
2020/04/07 00:36:58 Golangはいいぞ
2020/04/07 00:36:58 [Error] Timer was broken
2020/04/07 00:36:58 [Main] Restart timer thread. Please wait 3 seconds ...
2020/04/07 00:37:03 Golangはいいぞ
2020/04/07 00:37:05 Golangはいいぞ
2020/04/07 00:37:07 Golangはいいぞ
2020/04/07 00:37:09 Golangはいいぞ
2020/04/07 00:37:11 Golangはいいぞ
2020/04/07 00:37:11 [Error] Timer was broken
2020/04/07 00:37:11 [Main] Restart timer thread. Please wait 3 seconds ...
2020/04/07 00:37:16 Golangはいいぞ
2020/04/07 00:37:18 Golangはいいぞ
2020/04/07 00:37:20 Golangはいいぞ
2020/04/07 00:37:22 Golangはいいぞ
2020/04/07 00:37:24 Golangはいいぞ
2020/04/07 00:37:24 [Error] Timer was broken
2020/04/07 00:37:24 [Main] Restart timer thread. Please wait 3 seconds ...
2020/04/07 00:37:29 Golangはいいぞ
2020/04/07 00:37:31 Golangはいいぞ
2020/04/07 00:37:33 Golangはいいぞ
2020/04/07 00:37:35 Golangはいいぞ
2020/04/07 00:37:37 Golangはいいぞ
2020/04/07 00:37:37 [Error] Timer was broken
2020/04/07 00:37:37 [Main] Restart timer thread. Please wait 3 seconds ...
2020/04/07 00:37:42 Golangはいいぞ
2020/04/07 00:37:44 Golangはいいぞ
2020/04/07 00:37:46 Golangはいいぞ
2020/04/07 00:37:48 Golangはいいぞ
2020/04/07 00:37:50 Golangはいいぞ
2020/04/07 00:37:50 [Error] Timer was broken
2020/04/07 00:37:50 [Main] Restart timer thread. Please wait 3 seconds ...
2020/04/07 00:37:53 [Main] Reached revival limit. so stop worker ...

上手くいっているようです!!

まとめ

  • goroutineを使用したスレッドの立ち上げ
  • channel型のデータを使ったスレッド同士のメッセージ送受信
  • channel型に構造体を使用したイベントを切り分け
  • スレッドの再起動と簡単なサンプル

と簡単なサンプルに実務で役に立つであろうgoroutineに関する知識を詰め込みました。このサンプルを通して少しでもgoroutineへの理解が深まったのであれば何よりです。では、良い並行処理ライフを。

おまけ1: 関数がnilを返しているのかどうか

返していないっぽいですね。 play.golang.org

おまけ2: 並行処理の学習方法について

筆者は1年前までは並行処理の「へ」の字も知りませんでした。並行処理の知識が深まったのはElixirというプログラミング言語のおかげです。今回のサンプルコードの設計のベースになっている考え方の多くはElixirから輸入したものです。

「え、どんな言語やねん」と興味を持っていただけたのなら以下の記事をぜひ読んでみてください。きっと楽しんで頂けると思います。

www.okb-shelf.work

参考文献

【現役エンジニア】SCRATCH(スクラッチ)で遊んで何が出来るのかを体験してみた

ビジュアルプログラミングSCRATCH

SCRATCH(スクラッチ)に関しての説明はもう不要だと思いますので省きます。小学生などの低年齢層を対象にプログラミングをブラウザとGUIを使って学習することが出来るMIT開発のビジュアルプログラミングのプラットホームですが、どれぐらいのものが作れるのかが気になったので少しだけ遊んでみました。

また、普段は業務でプログラミング言語を使ってコードを記述しています。ビジュアルプログラミング言語からプログラミング言語の学習に進む際にどんなことが壁になるのかを体験してみました。

いざSCRATCHにアクセス

scratch.mit.edu

とりあえずアクセスしました。どうやらログインなしで早速プロジェクトを作成することが出来るようです。ログインをすると作品の保存と共有機能が使えるようになりました。

作りたいものがまだ決まっていないですが、プロジェクトを作って編集画面?にアクセスしてみました。

f:id:takamizawa46:20200329121748p:plain

まだ何も作成されていない状態ですが、何をやっていけばいいのかは何となく分かりました。左側にあるブロックと呼ばれる部品をつなげていって、処理を作成していけばいいようです。このブロックが非常にバリエーション豊かで色々なことが出来るようです。
音の出力から画像の回転や移動といった子供に喜ばれそうなブロックもあれば、変数の定義や分岐処理、繰り返し処理、はたまた配列操作に関するブロックまでありました。
「ほとんどプログラミング言語を記述するのと分からないんじゃないの?」と非常に驚かされました。

f:id:takamizawa46:20200329122249p:plain:h450

作るものを決めた

思っていたよりもプログラミング言語によせたブロッグが実装されていたので、結構やれそうだなと感じたので以前Twitterで見かけたSCRATCHの問題に取り組んでみることにしました。

その問題がこちら。SCRATCH再帰処理を用いて二分探索(binary search)を行うというもの。これをSCRATCHで作成してねという中々に鬼畜な問題...😅

まぁ、これだけプログラミング言語に寄せたブロックがあるんだから作れそうだなぁと思って挑戦することにしました。二分探索(binary search)については過去の記事で解説していますので、何なのか全く分からないという方はご参照下さい。

www.okb-shelf.work

試行錯誤を繰り返し...

気軽に実装を始めてみましたが、これが思うようにいかない...
f:id:takamizawa46:20200329122914p:plain

関数の定義が出来るブロックがあり、そこで再帰させてあげればいいのかなと思ったものの、SCRATCHで配列を分割する方法が思いつかずに一回、リセット(スコップを持った人に怒られそう)。

次は、元の配列から探したい数値以上の要素だけを抽出するfilter関数のような処理が出来る関数ブロックを作ってから二分探索をしようと思い、配列の分割(split_lst)を作ろうとします。

f:id:takamizawa46:20200329123213p:plain

元の配列に対して抽出処理をしてしまうと、元データが消えてしまうので、もう1つ配列を用意して、そちらに条件を満たした要素をコピーする方針としました。が、変数管理で大苦戦。なかなか思ったように出来ず、気づけば無限ループを引き起こしてしまっていた。

ああああ...
f:id:takamizawa46:20200329123402p:plain

何とか分割処理までは作れたものの、変数管理と全体の見通しがつかず、頭がワチャワチャしてきて、気づいたら散漫なブロックが出来上がってしまっていた。

f:id:takamizawa46:20200329123500p:plain

コメント機能は便利だと思いますが、どうせならコメントブロックを用意してほしかったなぁと感じました。普段コードを書いている身としては違和感がありました。

それから2時間ほど悩みましたが、最終的なブロックはこんな感じになりました。先に結果をいうと、作れませんでした。すみません。何ていうか飽きてしまいました笑

f:id:takamizawa46:20200329123715p:plain

何が辛かったか

プログラミング言語に寄せたブロックが多く用意されている一方で、普段記述しているプログラミング言語とは大きく異なる仕様に混乱させられました。順に紹介していきます。

配列のindexが0始まりではなく1で始まる

filterする処理を作っていた時に、何回やっても思った値が抽出出来ないなぁと思って調べたところ、配列の先頭要素のIndex番号は0ではなく1になるとのこと。この仕様には素直にやられたーと思いました。

なぜ1始まりの仕様にしたのかは分かりませんが、SCRATCHから著名なPythonRubyのようなプログラミング言語へ進む際は気をつけなければハマりそうですね。

自分も全く知らなかったのですが、COBOLFORTRANといった比較的古典的なプログラミング言語では配列の先頭要素にアクセスするためのIndex番号は1になるとのこと。勉強になった。

rubikitch.hatenadiary.org

全ての変数がグローバルスコープ

作成(定義)した全ての変数がどこからでも参照可能で、更新可能という仕様が個人的には合わなかった。この意図は、複雑な変数に対するスコープ管理を意識させないということなのだということは伝わってきます。そうすることで、変数は便利なものという、かなり抽象的ながらもパワフルな道具として利用してもらうことが出来るからです。

COBALのバージョン何かは忘れましたが、全ての変数がグローバルスコープでスパゲッティコード(複雑に絡み合い改修が難しい状態のコード)になってしまうという話を以前、聞いたことがあります。

・・・しかし、僕はCOBOLに出会った。それは衝撃的な出会いだった。全ての変数がグローバル
worar.hatenadiary.org

関数の引数に配列が渡せない

関数型言語再帰処理をしている立場としてはこれはつらい。引数として渡すことができるのはstring型, int型, float型, bool型のみで用意されているデータ構造の中で配列だけが渡せませんでした。

都度、グローバルスコープを利用して配列を参照する必要があり、うーんと思いました。配列の状態管理にすごく気を遣う必要があります。感じた意図としては、そもそも配列をあまり重要視していない、使われる想定をしていないといったところでしょうか。

配列に対してのイテレーションが実行できない

先ほども記述下通りですがSCRATCHでは配列に対して、それほど重要視されていないでしょう。そのためにこのような仕様になっていると感じました。filter処理を記述している時に、この不便さを感じました。

都度、インクリメントするための変数を用意(しかもグローバルスコープだから要注意)してあげて、配列のIndexを指定してアクセスするというオールドな書き方をする必要があり、面倒でした。

値がリセットされない

実行するたびに最終結果が全ての変数に残ったままになります。これがやっかいでした。全てがグローバルスコープなので実行するたびに毎回リセットする処理を作成しておかないと、とんでもない結果を生み出す可能性があります。

特にインクリメント用の変数や配列には要注意。無限ループになりがちです(めっちゃなった)。

総評

SCRATCHは非常によく出来ていました。ビジュアルプログラミングという点を生かして、実行できる処理が目に見えるものが多く子供が楽しんで学ぶことが出来ると好評を受けている理由も分かりました。一番の驚きだったのはやはり、実際にプログラミング言語を記述する際に意識する文法に近いブロックが多く用意されていることです。

まさか関数の定義まで出来るとは驚きました。

その一方で現代のモダンなプログラミング言語とは異なる仕様であったりとビジュアルプログラミングを完璧にしたからプログラミング言語をスラスラと記述出来るとは感じませんでした。
変数の管理や配列の操作をあまり意識させない方がストレスなく、子供に使ってもらえるでしょう。遊び方としては、イベントリスナーをたくさん用意して、それぞれで処理を発火させていくゲームのようなものを作ってもらうのが良いと感じました。

実際にこんなゲームまで作れるんですね。すごい...

このままでは終われない

せっかく挑戦したのに何の爪跡も残せず終わってしまうのは心もとないので、一作品を作ってみました。魔法使いが猫に挨拶をしたら、猫がイキってきたものの、魔法使いがマッチョであったため、萎縮するという謎すぎるストーリを作りました。

今思うとなぜこんなものを作ろうと思ったのが不思議ですが、「SCRATCHすげー!」と思った機能にメッセージの送受信というブロックが用意されていることです。

それぞれのオブジェクトにどんなメッセージを送るか、受け取った時に何をするのかという処理を作成することが出来ます。これは並行処理でメッセージを飛ばし合うメッセージパッシングそのものであり、複数の処理系が同時に走るという概念を学ぶには非常にいい機能ですね。

f:id:takamizawa46:20200329131819p:plain
f:id:takamizawa46:20200329131833p:plain

参考文献