やわらかテック

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

TODOコメントってあんまり意味がないんじゃないか説

とりあえず書き込まれるTODO

ある日のことです。新機能開発のためにコードを眺めていると該当箇所に対応するテストコードが全くないことに気づきました。 「TDDでやってるのになぁ」と思っていると、テストコードのファイルだけが作られていて、中にTODOコメントで想定されうる全てのパターンが記載されていました。

その数も中々のもので、ファイル内でTODOを検索してみると40件近く、TODOが書き込まれていることに気づきました。「〜したい」「〜の場合どうするか」という誰かが残したであろうTODOコメントが各所に溢れていました。

TODOあるなぁ。で、何をすればいいのか

TODOコメントがあって、後に何かをやる必要がある、やってほしいというのは分かるのですが、それがいつ発生した問題で、どのように解決するのか、他に関連する箇所はないのか、今は問題となり得るのか...と考えることがたくさんあります。

しかしながら、ただTODOと記述がされていても、上記の情報を得ることは難しいです。コメントを記載した人が詳細を覚えていればラッキーですが、そうでなければ何をしていいのか分からないコードに残された永遠のTODOとなってしまいます。

TODOという名前のくせに永遠にDOされることはないのです。

_人人人人人人人_
> 永遠のTODO <
 ̄Y^Y^Y^Y^Y^Y^Y^ ̄

generated: > 突然の死 < ジェネレーター

TODOは使わない方がいいのか

使うのは問題ないと思いますし、動作に影響を与えるものでもありません。また実装時間というのもプロジェクトによっては大きく制限もあるかなと思います。 しかし、ただTODOを書くだけでは、何が問題で何が重要なのか、いつまでにやる必要があるのか...など伝えられる情報が限られてしまいます。

なのでTODOコメントを使う場合はチームでフォーマットを定義しておくのが良いのではないでしょうか。自分はこんな感じでTODOコメントを書くようにしています。

# TODO: 2021/05/31 -> 来月まで
# validationが弱い。空白文字が入っているとそのまま登録されてしまうのでtrimする。

どうでしょうか。記載日と記載時に想定した締め日を1行目に書いてあげて、2行目に何が問題で何をすればいいのか書いておきます。
改めて記事にしてみて思ったのですが、TODOに優先順位が付けられるように情報が添付されているという状態が望ましいかなと感じました。

参考になればと思います。

参考文献

プログラマが知るべき97のこと

またブログを頑張るための振り返りと反省

サボりがちになったブログ

思えば、昨年には一週間に最低でも1記事。一昨年には何と一週間に2、3個の記事を書いていました。
時期としては大学の学部を卒業して新社会人としては働き始めた頃で決して、時間的に余裕があったわけではないはずです。さらに今と異なり、当時はリモートワークではなく毎日、片道1時間半かけてオフィスに通っていました。そんな状況で、記事を書いていたのに今はどうでしょうか。

リモートワークになり、参加しているプロジェクトも当時と異なり、かなり落ち着いた状況で進行しています。
なのにブログを書くことにたいして消極的になってしまいました。今回は言い訳と振り返りをしつつ、またブログを頑張っていくための糧にしようかなと思います。

なぜブログを書かなくなったのか

振り返ってみたところ、2つほど該当する理由を思いつきました。どれも言い訳にしかなりませんが、記しておこうと思います。

求められるスキルが変わった

昨年の10月からだったでしょうか。
今まで開発者としてガンガンコードを書いていたのですが、なんだかんだで業界4年目、勤務先企業でそこそこ籍を長く置いているという理由から、開発チームのリーダー(実態はメンバーのマネジメントとプロジェクト管理)になりました。メンバーは10人以下の小規模なチームです。
スクラムという開発方式を採用しているので、結果的に何も分からぬままスクラムマスターにもなりました。

ポジションが変わったことで、自分に求められるスキルも変わりました。これまでは早く、正確に。美しく保守可能なコードをガンガン書ける存在であることを求められていたのですが、今ではチームのメンバーを導き、スムーズに開発を継続させなければいけません。

メンタリングの方法や、1on1のやり方、タスクの進捗管理スクラムスクラムマスターとは何か...などインプットの内容も大きく変わり、代わりに技術系の情報のインプット量が減ってしまいました。

ブログのネタの多くが技術系の実際にコードを書くものだったので、そもそも記事にしようというモチベーションがありませんでした。

ストレス過多で肌トラブルに

エンジニアになったのは、多くの人とコミュニケーションをとる必要がない、営業さんのように他社に多く訪問に行きたくない...etcといった理由もありました。喋るのは下手ではないと思うのですが、積極的に自分から話しかけるというのは苦手です。

そういったストレスを回避するためにエンジニアなったはず...なのですが、開発チームのリーダーとなったことで、コミュニケーションによるストレスが多く発生するようになりました。個人的に一番きつかったのは、zoomでの打ち合わせや、タスクチェックなどで「問題ないか」という問いかけに対して、シーン...となることです。
「俺だって好きで進行やってない!」と何度思ったことか。

上記が原因なのかわかりませんが、肌が荒れました。元々、肌が弱くて、ストレス過多になると体に湿疹が出たりすることはありましたが、顔にだけは症状が出たことがなかったです。しかし、今年の1月頃に初めて顔に症状が出てしまい、大きなストレスになりました。

(※少しずつ回復しており、かなり楽になりました。 )

でもブログをまた頑張る

ブログを書かなくなったことで明らかに自分の技術のインプットの成長スピードが遅くなりました。それにコードを書く機会が減って、自分の開発者としての価値も下がっているかと危機感を感じています。リーダーはリーダーで面白いところはあるのですが、まだ開発者として現役で現場にいたいので、技術に対しての気持ちを途切れないようにしないといけないと思いました。

なんだかんだで、PVは少ないブログではありますが2年以上続けられているわけなので、自分にあっているのだと思います。なので、改めて頻度を高められるようにブログの執筆を頑張ってみようと思います。

まずは一週間に1記事を書く感覚を取り戻していこう!!

【Elixir】無名関数で気軽に再帰的にメッセージの送受信を実装する

Elixirでの並行処理

Elixirでは並行処理を行うためにアクターモデルに従ってプロセス間同士でメッセージを送受(メッセージパッシング)し合います。
アクターモデルとは並行処理を効率よく実装するための手法の一つです。Elixirにおいては、アクター(軽量プロセス)はアクター(軽量プロセス)を生成することが出来ます。また、全てのアクターはメッセージボックスを持ち、それぞれがメッセージの送受信を行うことができます。

全ての値は暗黙的に変更されることはなく、メッセージの送受の結果としてだけ値の変更が許容されます。逆に言えば、メッセージの送受以外では値の変更はされることはありません。そのため、データ不整合の問題や、ロック(排他処理)を明示的に実装する必要がありません(任意の処理が終わるまで待つということはあるでしょう)。

ids = [1, 2, 3, 4]

=> message: push 5

ids = [1, 2, 3, 4, 5]

=> message: pop

ids = [1, 2, 3, 4]

Elixirでのメッセージパッシングの実装については過去に記事にしておりますので、合わせてご覧ください。

www.okb-shelf.work

今回は、メッセージパッシングを実装する上でメッセージを何度も受信して、処理を行うという再帰的なメッセージパッシングを無名関数でサクッと記述する方法についてまとめております。

通常の再帰的なメッセージ受信の記述

こんな感じで、気軽に実装しようとすると、わざわざmoduleを使って実装する必要があり、個人的にはボリューミーだなぁと思ってしまいます。とはいえ、可読性はかなり良いですね。素直にmoduleを使って実装するのが一番だなと改めて感じさせられました。

def receive_message([]), do: {:ok, []}
  def receive_message([h | t]) do
    receive do
      {:POP, _pid} ->
        IO.puts("[Info]: #{h} was popped")
        receive_message(t)
      _ -> exit(:safety)
  end
end

結果確認のため、簡単にプロセスの起動と、メッセージの送信部分を関数化してmoduleに内包しました。

defmodule Recursive.Reciver do
  def process do
    pid = Enum.map(1..10, &(&1)) |> spawn_receiver()
    Enum.each(1..10, fn _ ->
      :timer.sleep(1000)
      send(pid, {:POP, self()})
    end)
  end
  def spawn_receiver(lst) do
    spawn(fn -> receive_message(lst) end)
  end
  def receive_message([]), do: {:ok, []}
  def receive_message([h | t]) do
    receive do
      {:POP, _pid} ->
        IO.puts("[Info]: #{h} was popped")
        receive_message(t)
      _ -> exit(:safety)
    end
  end
end

実行結果

iex(5)> Recursive.Reciver.process
[Info]: 1 was popped
[Info]: 2 was popped
[Info]: 3 was popped
[Info]: 4 was popped
[Info]: 5 was popped
[Info]: 6 was popped
[Info]: 7 was popped
[Info]: 8 was popped
[Info]: 9 was popped
[Info]: 10 was popped
:ok

無名関数を使った再帰的なメッセージ受信の記述

以下の記事で紹介した無名関数の再起処理のテクニックを用いて、こんな感じで無名関数だけでmoduleを使わずに記述することが出来ました。

www.okb-shelf.work

(あれ、moduleの方がいいんじゃないか...??)
(もはやロマン)

receiver = fn lst ->
  recurcive_ = fn lst_, own ->
    receive do
      {:POP, _pid} ->
        [h | t] = lst_
        IO.puts("[Info]: #{h} was popped")
        own.(t, own)
      _ -> exit(:safety)
    end
  end
  recurcive_.(lst, recurcive_)
end

無名関数は自身のスコープを持っておらず、自己参照することは出来ないので、引数に自分自身を渡してあげます。こうすることで無名関数から自分自身を呼び出し再帰処理を行うことが可能になります。あとは先ほどと同じように再帰的なメッセージの受信の処理を記述してあげれば良いですね。

# 実行系
process = fn ->
  lst = Enum.map(1..10, &(&1))
  selecter_pid = spawn(fn -> receiver.(lst) end)
  Enum.each(1..10, fn _ ->
    :timer.sleep(1000)
    send(selecter_pid, {:POP, self()})
  end)
end

実行結果

iex(8)> process.()
[Info]: 1 was popped
[Info]: 2 was popped
[Info]: 3 was popped
[Info]: 4 was popped
[Info]: 5 was popped
[Info]: 6 was popped
[Info]: 7 was popped
[Info]: 8 was popped
[Info]: 9 was popped
[Info]: 10 was popped
:ok

メッセージの種類や、データの量が多くなってくるようであれば、素直にTaskAgentGenServerを使って実装した方が楽になっていきます。

参考文献

GoogleCloudStorageでファイル検索を簡単にする命名方法

あらすじ

とあるAPIからのレスポンスの内容が、どうやら仕様書で共有されていたモノと中身が異なっているということで、どういうレスポンスが返ってきているのかを確認してほしいと言われました。とはいえ、本番環境では1時間に約10万回ものリクエストを送っているため、単純にログにデータを出力すると、えらいことになります。

そのため、受け取ったレスポンス(json)を.json形式のファイルを作成して、GoogleCloudStorageにアップロードすることにしました。ただログを集約したいだけだったので、特に階層は作らず、バケットの直下にファイルをアップロードさせるようにしています。

ファイル名は重複がなく、いつ、どこで受け取ったレスポンスなのかが分かるように以下のように命名をしています。

uuid + timestamp + リクエストしたユーザー名 + 種別(絞り込みのための値) + .json

例:
5300f326-9fcd-d392-798b-5b7a253983ff_2021-10-08-21-45-00_ayatsuzi_student.json

ログをアップロードするところまでは良かったんですが、この後に問題に直面しました。

検索が出来ない

いざ、アップロードされたログを絞り込んで一覧を取得して、問題になっていそうなログを確認しようと思った頃に問題が発生しました。 開発はRubyOnRailsを用いて進めています。GoogleCloudStorageAPIを使用するために、Googleより公開されているRuby用のライブラリを使用する必要があります。他言語でも同様の手順を踏む必要があるでしょう。

github.com

公式にサンプルがあるので、真似て書いてみました。どうやらprefixという引数(Rubyだとdefaultがnil)を渡してあげることで、一覧表示するファイルを絞り込むことが出来るようです。コードサンプルの方にはprefixワイルドカードを使って、*.jsonだけを絞り込むという感じのサンプルは載っていないのですが、gsutilの方にはワイルドカードを使ったサンプルが載っていたので、「これ使ってユーザーを絞り込むか」と思い立ちます。

cloud.google.com

以下のコードを書いてみて、試してみましたが空配列が返ってきます。書き方が違うのかな?と思い正規表現を渡したり....いろいろ試してみましたが、やはり空配列が返ってきます。

# NG
bucket.files prefix: 'ayatsuzi'
bucket.files prefix: /ayatsuzi/
bucket.files prefix: './*_ayatsuzi'

途方に暮れていたところで、ポカしていることに気付きました。Googleのコードサンプルにこんな記述があります。

# prefix      = "Filter results to files whose names begin with this prefix"

ref: オブジェクトのリスト  |  Cloud Storage  |  Google Cloud

文字通り、prefixから始まるファイルの一覧が取得出来るようです。加えて、上記には説明がありませんが、ワイルドカードを用いた絞り込みは少なくともRuby製のライブラリには提供されていませんでした。(Python, Goでも出来ないっぽい??)

結果的に、ファイルの一覧を全件取得して、条件式を書いて絞り込みをしようかと思いましたが、どうやら取得可能なファイル数orファイルサイズに上限があるようで、全てのファイル一覧を取得することは出来ませんでした。

結果的にgsutilを用いてファイル一覧を絞り込むという形に落ち着きました。(最初からそうしろ)

gsutil ls gs:://project-hogehoge/**_ayatsuzi

命名のススメ

prefixが接頭辞にしか用いることが出来ないため、絞り込みを強力にすることが出来る順に命名するのが良いかと考えます。今回の場合だと、リクエストしたユーザー名を用いることでデータ数をかなり絞り込むことが出来ます。その次は種別、その次は...と順に情報を与えていきます。
先ほど、uuidを重複回避のために先頭に持ってきたのは明らかな失敗でした...。

リクエストしたユーザー名 + 種別(絞り込みのための値) + timestamp + uuid + .json

例:
ayatsuzi_student_2021-10-08-21-45-00_5300f326-9fcd-d392-798b-5b7a253983ff.json

副作用としては、一覧に表示されるデータの順番が意図したものではなくなる可能性がありますので、要注意です。 (とはいえ、データ数が多いとそんなに気になりませんし、普通にソートも出来ます)

階層を用いてファイルを管理する場合(例: /2020-02-08/ayatsuzi/student/***.json)にはprefixを用いて簡単に検索することが出来ます。データを直下に集約しなければ、遭遇しない問題なので、Googleの意図としては「素直に階層作ってくれよな」というところでしょう。

参考文献

JavaScriptでのスマートな再帰関数の書き方【ECMAscript2015以降対応】

仕事でJavaScriptを書くことが多くなりました。以前より学んできた多くの関数型言語で一般的に使用されている再帰関数をJavaScriptでもスマートに書けないものかなと模索してみました。
「普通にforや、mapを使えば良いのでは?」という問いかけに対する答えは「イエス」です。使う機会があるかは分かりませんですが、参考になればと思います。

JavaScript再帰関数が書きにくい理由

元々、再帰関数を書く必要がある場面はかなり少ないかと思います。ツリー構造のようなデータを扱う場合などが該当するでしょうか。多くの場合は前述の通り、forや配列に対してmap関数を実行すれば事足ります。

しかし、その一方で関数型言語では一般的にループ処理のための文法が用意されたおらず、ループ処理を再帰関数で実装する必要があります。その際に再帰関数を実装しやすくしてくれるのはパターンマッチと呼ばれる機能です。広い機能として定義されている事が多いですが、ここでは分かりやすさのため関数の引数でのパターンマッチを一例にしておきます。

(サンプルコードはElixirを用いて記述しました)

sum([], total), do: total
sum([head| tail], total), do: sum(tail, total + head)

二行で記述することが出来ました。
関数が関数を呼び出しますので再帰関数となっています。この関数は第一引数に渡されている配列が空になった時に停止するようになっています。とはいえ、どこにもifの記述はありません。sum関数の第一引数によって実行される処理が変わります。

空配列の場合には第二引数のtotalを返し、空配列ではない時は先頭の要素を取り出して、合計の値に先頭の値を加え、再度、sum関数を呼び出します。この際に先頭要素の値が配列から取り出されているので配列の要素数-1されています。つまり、いずれは空の配列になるため、totalが帰るということになります。

JavaScriptではこの強力なパターンマッチを使うことが出来ないため、愚直に書けば雑多なコードを書く羽目になります。

const sum = (lst, total) => {
    if(lst.length === 0) {
        return total;
    } else {
        const head = lst[0];
        const tail = lst.slice(1, lst.length);
        return sum(tail, total + head);
    }
}

const main = () => {
    const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    const res = sum(data, 0);
    console.log("result: ", res);
}

main(); # result:  55

最終形態(ぼくのかんがえたさいきょうのさいきかんすう)

多分、これが一番スマートだと思います。

const sum = (lst) => _sum(lst, 0);
const _sum = (lst, accum) => {
    const [h, ...t] = lst;
    return lst.length === 0 ? accum : _sum(t, accum + h);
}

ECMAscript2015からサポートされたDestructuring assignmentsを利用して、先頭の値と、先頭以外の値(配列)に値を束縛します。

const lst = [1, 2, 3, 4, 5]
const [h, ...t] = lst;

h # 1
t # [2, 3, 4, 5]

あとは三項演算子を使って、条件を分岐させます。ここで再帰関数の停止性を保証します。

return lst.length === 0 ? accum : _sum(t, accum + h);

もう少し関数型言語っぽく書いてあげるとこんな感じでしょうか。Array.prototypeに追加して上げても良いですが、やりすぎ感はやはり否めないですね。

const sum = (lst) => _sum(lst, 0);
const _sum = (lst, accum) => lst.length === 0 ? accum : _sum(tail(lst), accum + head(lst));
 
const head = (lst) => {
    const [h, ...t] = lst;
    return h;
}
const tail = (lst) => {
    const [h, ...t] = lst;
    return t;
}

sumだけではなく、reduceもスマートな再帰関数の記述を使って書いてみました。また、このreduceを使って、sumの実装をする事も出来ました。

const reduce = (lst, callback, accum) => {
    const [h, ...t] = lst;
    const next = callback(h, accum);
    return lst.length === 0 ? accum : reduce(t, callback, next);
}

const callback = (h, accum) => accum + h;
const sum = (lst) => reduce(lst, callback, 0);

再帰関数のスマートな書き方の紹介については以上になります。以降、与太話です。

とはいえ

この程度の処理であれば、再帰関数を使わずに、map, filter, reduce を使って実装させてあげれば良いかなと思います。わざわざ、複雑な再帰関数を記述する必要はないかと思います。

const sum = (lst) => lst.reduce((accum, h) => accum + h, 0);

map, filter, reduce の使い方とコンボについては過去の記事にまとめています。

www.okb-shelf.work

www.okb-shelf.work

おまけ(最終形態になるまでの過程)

これが

// var1
const sum = (lst) => _sum(lst, 0);
const _sum = (lst, accum) => {
    if (lst.length === 0) {
        return accum;
    }
    const [h, ...t] = lst;
    return _sum(t, accum + h);
}

こうで

const sum = (lst) => _sum(lst, 0);
const _sum = (lst, accum) => {
    const [h, ...t] = lst
    const next = accum + h
    return t.length === 0 ? next : _sum(t, next);
};

こうじゃ(最終系)

const _sum = (lst, accum) => {
    const [h, ...t] = lst;
    return lst.length === 0 ? accum : _sum(t, accum + h);
}

参考文献