yu-tarrrrの日記

完全に個人の趣味でその時々書きたいことを書く

アジャイルマニフェストの一部分について考えてみた

はじめに

アジャイルマニフェストとは

  • アジャイルソフトウェア開発宣言の方が聞き馴染みがあるかもしれないが、今から20年くらい前にpublishされたもの
  • 下の文章のこと
私たちは、ソフトウェア開発の実践
あるいは実践を手助けをする活動を通じて、
よりよい開発方法を見つけだそうとしている。
この活動を通して、私たちは以下の価値に至った。

プロセスやツールよりも個人と対話を、
包括的なドキュメントよりも動くソフトウェアを、
契約交渉よりも顧客との協調を、
計画に従うことよりも変化への対応を、

価値とする。すなわち、左記のことがらに価値があることを
認めながらも、私たちは右記のことがらにより価値をおく。

引用元

今回考えたこと

  • たまに読み返していて、「包括的なドキュメントよりも動くソフトウェアを」と「契約交渉よりも顧客との協調を」と「計画に従うことよりも変化への対応を」については、ふむふむって感じで前から読んでたのですが、「プロセスやツールよりも個人と対話を」についてはなんか腹落ち感がなかった
  • ですが、今回は「プロセスやツールよりも個人と対話を」について、なんか理解できたので言語化していこうと思った

プロセスやツールよりも個人と対話を

  • 本来は「Individuals and interactions over processes and tools」というのが原文
  • もやもやしてたポイントが、「プロセスとツール」が「個人と対話」の対比構造?比較対象?になっていることが、イマイチ理解できずにいた

「プロセスやツールよりも個人と対話を」の裏側にありそうなこと

  • これを紐解いていくなかで、アジャイルウォーターフォールを比較するとわかりやすいと思った
  • ウォーターフォールは、なるべく属人性を排除する。だから、マニュアル化するのであって、テスト項目書を作って誰がテストしても同じ結果が得られるようにするし、誰が使っても同じようにバグが見つかるようなツールを求める(これは言いすぎかも)
  • アジャイルは、人の違いに理解をおく。あなたと私は違いますよね、だから対話をして理解をしていきましょうという考え方。なので、全員同席して色んな人が対話をするようにしている。
  • 例を用いると、ウォーターフォールだったらバグがあったときに、それを誰でもが検知できるような仕組みを考えると思う。(たぶん)
  • アジャイルだったら、仕様についてテストエンジニアと洗い出すとか、コミュニケーションで防げたよねみたいな対話をすると思う(してる)
  • なので、アジャイルウォーターフォールは根底のところで、プロジェクトにおける人への考え方が違うんだろうなーと思い、そこが起因してるんじゃなかろうかというのが今回の仮説
  • 炎上してるプロジェクトに人を投入するか否か、というのも一定上記の違いからきてるんだと思う。
  • これはどっちが良いとかではなく、あくまでも考え方の違いなんだろう

一方で

  • アジャイルが属人性を排除してないかというと、それは違う
  • ペアプロで知識を伝播してるし、コードの共同所有みたいな考え方がある
  • なので、両者ともに属人性を排除しようとはしていて、属人性を排除するためのアプローチの仕方が違うだけって考えるのが一番しっくりくる

要約すると

  • 「プロセスやツールよりも個人と対話を」については、属人性を排除するためのアプローチの仕方がアジャイルウォーターフォールでは違うんだろうなと読んでいて思った
  • 人の違いに注目して、対話を繰り返すことで知識やノウハウの伝播をしていくアジャイルに対して、人という要素をなるべく除外して誰がやっても同じような結果を得られるような仕組みやプロセスを構築するウォーターフォールという考え方の違いなんだと理解した。

おしまい

OKRの立て方について

はじめに

  • あくまでも主観でしかないので,他の人に当てはまらない可能性もあるので「ふーん」程度に読んでくれたら嬉しい
  • メモ程度に意識していることを残す
  • 半年後くらいには違うわ!ってなってるかもしれない

そもそもOKRって

  • Objective: 「目標」
  • KR(key result): 「目標を達成するための手段」「そのカギとなる成果指標」 的なもの

感じてること

  • ちゃんと立てられると機能する
  • 上手く立てるには慣れるしかない
  • 慣れてきたので,個人的に意識してるTipsを残しておく

Objective

エモめの文章を考える

  • ネタみたいだけど,割と重要だと思う
  • パッと思い出せるくらいにスマートでキャッチーな文言がいい
  • そして,それを説明する文章もちゃんと書く
  • そうすることで,どうしてこれをやるのか。自分の意思でちゃんと立てているのか。一種の宣言みたいになって意思表明みたいになって良い。
  • あと,見返した時にワクワクするからエモさは重要な指標

手広くしない

  • OKRは短い期間(3ヶ月とか半年)で立てるので,あっという間に終わるので,いろいろ中途半端になりがち
  • なのでできるだけ絞る
  • (後述するが)ストレッチなポイントを,ここの手広さに求めることはあまりおすすめしない
  • 何にフォーカスするか意思に基づいて取捨選択をきちんと行う

KR

ストレッチ

  • 前述したストレッチについて
  • 簡単に達成するものだけで,構成しない
  • 普通にやって100%達成できるものではなく,120%くらいの力を出し続けられれば達成できるもの
  • イメージはマグロ(一定のスピードで泳ぎ続けないと沈むってことを言いたい)
  • 本数で勝負みたいなのは絶対やめておく

仕事と絡める

  • これはObjectiveも同じ
  • 個人の時間だけに依存するものだけで構成するのは正直きつい(プライベートなOKRは話は別)
  • うまーく業務の中で成長できるようなストレッチ目標を立てた方が,省エネで最大限のリターンを得られる

定量

  • ラッキングしやすいから絶対したほうがいい
  • あと,メモを残す。(進捗があがったときはそうだし,詰まってるときも)

1週間に1度は更新する

  • ゲームのログインボーナスをもらう気持ちでとりあえず見る
  • 更新できないか考える。
  • 更新できたら,進捗が上がるので素直に喜ぶ。
  • 更新できなかったら,頑張りましょう。

中期的に見直す

  • 進捗,大丈夫かな?みたいなのを確認するためにも途中立ち止まる
  • KRはマグロと違って多少途中で止まっても,沈むまでは行かないので早めに気づくと良い。

結婚式の準備をアジャイル風に進めた結果

はじめに

  • 結婚式の準備ってなんとなく大変そうなイメージがありました(実際めちゃ大変)
  • そこで、夫婦一体感をもって取り組めないかなと思って、楽しめる要素を取り組みつつ漏れなく準備を進めたいなと思い、アジャイル風(あくまでも風)に進めていこうと思いました ※あくまでも風であって、MVPとか小さくリリースとか、改善サイクルを回すとか不可能な要素があるので、可能な限りアジャイルのエッセンスを取り入れるというものです

用意したもの

カンバンについて

  • こんな感じで作りました f:id:yu-tarrrr:20210125233618p:plain

  • まず、夫婦で必要なストーリーを洗い出しました(ユーザーストーリー風)

  • そこから、見積もりをしてみることに。基準となるストーリーを1とし、そこから作業量や懸念点が多そうであれば数字を大きくすることを伝え、2人で見積もりをしてみます。
  • すると、作業のイメージがあってなくて、全然違う数字をだすということもあったり、考慮していなくて数字が大きくなった場合は他のストーリーにするというストーリー出しさながらの作業になりました。
  • ユーザーストーリー風にしたことで、これをやると誰に価値が届くんだっけというのが明確になるのと、会話するきっかけが生まれて面白かったです

バーンダウンチャート

f:id:yu-tarrrr:20210125233125p:plain

  • 結婚式の場合はゴールが決まっているので、そこからイテレーション数を計算し、イテレーションあたり消化しないといけないポイントを逆算しました。
  • 通常のアジャイルであれば、イテレーションを回してみて、自分たちのベロシティを計測してその後、リリース可能ラインを見極めますが、少なくとも現状出てるストーリーは削れないので、これを確実に消化するということで、このような考え方をしています。
  • ここで1週間あたりに消化しないといけないポイントが出てきたので、今度は1週間のプランニングをしました

プランニング

f:id:yu-tarrrr:20210125234430p:plain

  • こんな感じで、今週着手予定のものに加えて早めに検討しないといけないものを優先的に今週積むようにしました。

結果

f:id:yu-tarrrr:20210504114018p:plain

無事にリリース(当日を迎えることが)できた!!!

ふりかえる

  • さまざまな観点から今回の取り組みを振り返ってみようと思います

Good

そもそもの取り組み自体

  • これはパートナーから好評でした
  • 見積もり、プランニング、レトロスペクティブを小さく回して行くことで、ゲーム感覚で支度が進んでいくので、「今週はこんなに進んだね」とか「今週はやばいからどうにかしないと!!!」みたいな会話のきっかけになりました
  • あと夫婦で一体感を持ってやることができたと思います

ユーザーストーリー

  • これも良かったです
  • 着手するときに「これってなんだっけ?」みたいな会話のきっかけになったので、本来のユーザーストーリーの狙いに即した使い方をできたと思います
  • あと、なんのためにこれをやってるんだっけ?という点も明確になるのは良かったです

見積もり

  • これは難しかったですが、取り入れたことでゲーム要素が加わり楽しく取り組めたと思います
  • ゴールデンストーリーを一個決めて、「これより大変そうだよね!」とか「これはここがどうなるかわからないけど、一旦見えてる範囲だけならどっちの方が大変そう?」とか実際のアジャイルにおけるプランニングさながらの会話も発生したので楽しかったです

バーンダウンチャート

  • 自分たちの進捗が可視化されるので、とても楽しかったですしモチベーションになりました。

持続可能なペース

  • 僕たちはゴールから逆算して、最低ベロシティを計算してそれを目指すという割とアンチパターンでやってしまいましたが、結果としてそれが僕たちのベロシティとほとんど一緒だったのかなと思います
  • 直前期にめちゃめちゃ追い込むこともしなかったですし、途中でやるきがなくなるみたいなこともなかったので、結果的に持続可能なペースを維持し続けて走りきることができたと思います。

bad

優先度決め

  • 大体の優先度決めはできるのですが、外的要因に絡むストーリーが多くそこで順番を前後させる必要がありました
  • どちらかというと、ストーリーの独立性の話になってくるのですが、Aというストーリーが完了して初めてBというストーリーに着手できるとなった場合、Aというストーリーが外部依存になっているとBが優先度高いのにプランニングのときに積めないということが発生しました。
  • 幸いにもBはそんなに大きくないストーリーだった&他の大きめな物に先に着手するみたいな対応をして、暇状態を回避できましたが、他にもやりようはあったのでは?と思っています

MVP

  • どちらかというとスコープの話なのですが、そもそもスコープを途中から急に削るというのは難しいです
  • プランナーの人に相談するのは割と簡単かもしれませんが、今回のプロダクトオーナー(パートナー)からのYESを勝ち取るには、かなりの努力をする必要があるので、その努力をするくらいなら終わらせる方に労力を割いた方が全てが平和に解決するので、今後の参考にしてください

ベロシティ

  • 次の物にも関連するのですが、自分たちだけで完結するものとそうでないものを着手してるフェーズにおいて、ベロシティのブレが結構発生したなという印象です
  • あと、ベロシティがやばくなってきてスコープを削るみたいなことができないので、最後はパワープレーに倒すしかないなというのは、やっていて気づきました。
  • 幸いにも、僕たちは挙式当日の1週間前をリリース日に設定して、当日でも間に合うものたちだけがリリース日に間に合わなかったので、大きな影響がなかったのですが、ギリギリでバーンダウンチャートを引きすぎると最後は頑張るしかないのかなという印象を受けました。

完全DONE

  • 外的要因が絡むもの「何かを回収した結果、何かをするみたいなもの」については、完全DONEにできるタイミングがすごく難しかったです。
  • そもそも上記の場合、回収し終わるのに何イテレーションもまたぐこともあり、ストーリーがTODOまたはDOINGに残り続けてしまうという気持ち悪さがあったので、ここは改善の余地があるかなと思いました

属人化

  • これはイベントの性質上仕方ないのですが、どちらかしか着手できないというストーリーは絶対発生します
  • プランニング時にそこは考慮して、「どっちの方がやること多そうだね」とか「これは、君のタスクだからこっちは僕がやるようにするね」とか丁寧にコミュニケーションをとって公平性というか共創感を担保・維持するといいと思います

不確実性

  • 結果的に僕たちは、当初見積もりをしたときの1.2倍のことをやっていたようです
  • アジャイルでは割と有名な「不確実性コーン」に1.6倍とあるように、当初見積もっていたよりやることは確実に多くなるし、やりたいことも増えると思います。なので、一定そこも加味して楽しくやれたらいいんじゃないかなと思いました

まとめ

  • なんとなく、アジャイルのエッセンスを結婚式の準備に取り入れてみましたが、「楽しかった!」というのが正直な感想です
  • (もう1回はないはずですが)次にやることがあれば、またやりたいなと思うしもっとよくできるかもなという感覚を得ました
  • 完全なアジャイルではないものの、こういったアジャイルのエッセンスをこれからも日頃の取り組みにちょっとずつ試験的に落とし込んでみたいなと思います。

Elixirでさくっとスクレイピングする

はじめに

  • 今回はAPIが提供されていないWebサイトに対して、スクレイピングをして何かしら加工して出力したいと思います
  • また、あくまでも私的利用であり数回しか叩かない予定であり、スクレイピングを推奨している訳ではありません

用意したもの

実装していく

まず、依存ライブラリを追加していく

defmodule Scrape.MixProject do
  use Mix.Project

  def project do
    [
      app: :scrape,
      version: "0.1.0",
      elixir: "~> 1.10",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:floki, "~> 0.29.0"},
      {:httpoison, "~> 1.2.0"},
      {:poison, "~> 3.1"}
    ]
  end
end
  • 今回追加したものは下記の通り

  • HTTPリクエストを行うので、そのためのライブラリとしてHTTPoison hexdocs.pm

  • 一旦、DBなどは使わずJsonからmaster情報を取得して、Jsonへ出力するということをしたいのでPoisonを使う hexdocs.pm

  • API提供されてないサイトへのスクレイピングを行うので、そこら辺はFlokiが頑張ってくれる hexdocs.pm

Jsonをパースしていく

  • マスター情報はこんな感じで、とあるショップのショップ名とショップURLを持たせておく
[
    {
        "shopName": "website1",
        "shopUrl": "https://website1.jp/"
    },
    {
        "shopName": "website2",
        "shopUrl": "https://website2.jp/"
    }
]
defmodule ScrapeSample do
  # main関数からjsonを取得する関数を呼び出す
  def main do
    read_master()
  end

  def read_master do
    "master.json"
    |> File.read!()
    |> Poison.decode!()
  end
end

スクレイピングする

defmodule ScrapeSample do
  def main do
    read_master()
    |> Enum.map(& scrape(&1["shopUrl"]))
  end

  def read_master do
    "master.json"
    |> File.read!()
    |> Poison.decode!()
    end

  def scrape(url) do
    # 受け取ったURLに対してリクエストして、Floki.find()で取得したい要素を指定する
  HTTPoison.get!(url).body
    |> Floki.find("section")
    |> Floki.find("h2")
  end
end
  • ちなみにこの時点で、scrapeした結果を出力すると、下記のような感じになる。
[
  {"h2", [], ["aaaaa"]},
  {"h2", [], ["bbbbb "]},
  {"h2", [], ["ccccc"]}
]
[
  {"h2", [], ["ddddd"]},
  {"h2", [], ["eeeee"]},
]
  • この場合欲しい要素はTupleの3つ目の要素のみなので、修正する
 def scrape(url) do
    HTTPoison.get!(url).body
      |> Floki.find("section")
      |> Floki.find("h2")
      |> Enum.flat_map(&(Tuple.to_list(&1) |> Enum.at(2)))
  end
  • このあと、必要に応じてフィルタの処理を入れてもいいが、今回は割愛する

あとは、jsonに出力する

  • 出力するオブジェクトを作成して、それをFile.writeに渡したら終了
   def main do
    read_master()
    |> Enum.map(& %{shopName: &1["shop_name"], items: scrape(&1["shop_url"]), url: &1["shop_url"]})
    |> write_items()
  end

  def write_items(items) do
    File.write!("items.json", Poison.encode!(items))
  end

最終的なコード

defmodule ScrapeSample do

  def main do
    read_master()
    |> Enum.map(& %{shopName: &1["shopName"], items: scrape(&1["shopUrl"]), url: &1["shopUrl"]})
    |> write_items()
  end

  def read_master do
    "master.json"
    |> File.read!()
    |> Poison.decode!()
  end

  def scrape(url) do
    HTTPoison.get!(url).body
        |> Floki.find("section")
        |> Floki.find("h2")
        |> Enum.flat_map(&(Tuple.to_list(&1) |> Enum.at(2)))
  end

  def write_items(items) do
    File.write!("items.json", Poison.encode!(items))
  end
end

感想

  • スクレイピングするだけならめちゃくちゃ簡単でした
  • 特に、Flokiのハマりポイントもないので、Elixirの入門として書くコードとしてはまあまあ良さそうという印象です。

gaugeとTypeScriptでテストをいい感じにする

はじめに

  • 今回はgaugeというテストライブラリを使い、web driverを使ったテストを書いていこうと思います

そもそもgaugeとは

gauge.org

  • マークダウン形式でテストシナリオを定義でき、実行ファイルとシナリオを定義しているファイルを分離できるというもの
  • メリットとしては、テストを自然言語でかつマークダウンで書けることで、単にテストを書くよりも可読性が大幅に上がるという点があります。

はじめる

  • OSX前提で書きます
  • ターミナルにHomebrewが入ってる前提で進めます
  • nodeとnpmも入ってる前提で進めます

gaugeをインストールする

$ brew install gauge
# gaugeのバージョンが見られればOK
$ gauge -v
Gauge version: 1.0.9
Plugins
-------
html-report (4.0.8)
java (0.7.3)
screenshot (0.0.1)
ts (0.1.0)
xml-report (0.2.2)
  • VSCode のExtensionがあるのでそれを入れておく

marketplace.visualstudio.com

# 用意されているテンプレートを確認しておく
$ gauge init -t
dotnet
java
java_gradle
java_maven
java_maven_selenium
js
js_simple
python
python_selenium
ruby
ruby_selenium
ts
csharp

# 今回はtsで作成する
$ gauge init ts
Downloading ts.zip
Copying Gauge template ts to current directory ...

> protobufjs@6.10.1 postinstall /Users/yuta.aikawa/sources/gauge/sample/node_modules/protobufjs
> node scripts/postinstall

npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN gauge-ts-template@0.0.1 No repository field.
npm WARN gauge-ts-template@0.0.1 No license field.

added 88 packages from 134 contributors and audited 88 packages in 3.763s

3 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Successfully initialized the project. Run specifications with "gauge run specs/".
  • プロジェクトディレクトリをVSCodeで開いてみると下記のような構造になっているはず
.
├── env
├── logs
├── manifest.json
├── node_modules
├── package-lock.json
├── package.json
├── reports
├── specs
├── tests
└── tsconfig.json
  • 今回用があるのは、spectests のみなので、まずは、spec から見ていく。
  • gaugeの世界には、scenario/ suite/ stepなど色々粒度の異なる概念が登場してくるが、そこら辺の説明は公式に任せる

docs.gauge.org

  • テンプレートで作成した、spec以下には spec > scenario > stepという感じで定義されていて、雑に言えばこの粒度で定義していけば良い。

  • 次にtest側を見ていく。

  • test以下には既に、tsファイルが用意されていて、ファンクションに @step("hogehoge")みたいな物がついている
  • そのアノテーションの中で定義している文字をspecファイル側から呼び出すという簡単な話である

実行する

  • 概要はざっくり説明したので、あとは動くか確認する
$ gauge run
# Specification Heading
  ## Vowel counts in single word     ✔ ✔
  ## Vowel counts in multiple word   ✔ ✔

Successfully generated html-report to => /Users/yuta.aikawa/sources/gauge/sample/reports/html-report/index.html
Specifications: 1 executed  1 passed    0 failed    0 skipped
Scenarios:  2 executed  2 passed    0 failed    0 skipped

Total time taken: 285ms
Updates are available. Run `gauge update -c` for more info.
  • こんな感じで、動けばOK

selenium

  • webdriverを用意する
$ brew tap homebrew/cask
$ brew cask install chromedriver
$ chromedriver -v
$ ChromeDriver 85.0.4183.87 (cd6713ebf92fa1cacc0f1a598df280093af0c5d7-refs/branch-heads/4183@{#1689})
  • 先ほど作成したgaugeプロジェクトで selenium webdriver を使えるようにする
$ npm install -D typescript selenium-webdriver @types/selenium-webdriver
  • 実装していく
  • まずは、specファイル側から定義していく
# Specification Heading

## Sportsnaviにアクセスする
* sportsnaviにアクセスする
  • この状態では対応する実装がないので怒られると思うので、ts側の実装をしていく。

import { Step } from "gauge-ts";
import { Builder, Capabilities, WebDriver } from 'selenium-webdriver'


export default class Access {

  @Step("sportsnaviにアクセスする")
  public async accessSportsNaviTopPage() {
    await access("https://sports.yahoo.co.jp/")
  }
}

# SeleniumWebDriverが用意してるインターフェースを呼び出してDriverの設定を書いておく
const capabilities: Capabilities = Capabilities.chrome()
capabilities.set('chromeOptions', {
  args: [
    '--disable-gpu',
    '--window-size=1024,768'
  ],
  w3c: false
})

# WebDriverを呼び出して受け取ったURLにアクセスする
async function access(url: string): Promise<void> {
  const driver: WebDriver = await new Builder()
    .withCapabilities(capabilities)
    .build()
  try {
    await driver.get(url)
    
  } finally {
    driver && await driver.quit()
  }
}
  • これで実行する
$ gauge run
gauge run
# Specification Heading
  ## Sportsnaviにアクセスする     ✔

Successfully generated html-report to => /Users/yuta.aikawa/sources/gauge/e2e/reports/html-report/index.html
Specifications: 1 executed  1 passed    0 failed    0 skipped
Scenarios:  1 executed  1 passed    0 failed    0 skipped

Total time taken: 2.859s
Updates are available. Run `gauge update -c` for more info.
  • Chromeが立ち上がり、上記な感じで結果が出力される。

ここまでのまとめ

  • setup自体はめちゃくちゃ簡単
  • ただし、seleniumのインターフェースを呼び出して、直接諸々操作しないといけないので、selenideに比べるとキャッチアップにコストがかかりそう。 Selenide: concise UI tests in Java

puppeteerを使ってみる

  • もうちょっと、seleniumとかdriverを意識せずに使いたいので、puppeteerを試してみる github.com
$ npm install -D typescript puppeteer @types/puppeteer
  • 実装側の修正をする
import { Step } from "gauge-ts";
import { launch } from 'puppeteer'


export default class StepImplementation {

  @Step("sportsnaviにアクセスする")
  public async accessSportsNaviTopPage() {
    await access("https://sports.yahoo.co.jp/")
  }
}

async function access(url: string): Promise<void> {
  const browser = await launch(
    { 
        headless: false
    }
  );
  const page = await browser.newPage();
  await page.goto(url)
  await browser.close();
}
  • とりあえず、chroniumが動いてサイトまで飛ぶことはできた。
  • もう一歩踏み込んでselectorを指定して取得する
async function access(url: string): Promise<void> {
  const browser = await launch(
    { 
        headless: false
    }
  );
  const page = await browser.newPage();
  await page.goto(url)
  
  # pageに対してjqueryっぽく指定して要素を取得することができる
  const headersCount = await page.$$eval('#h_nav .clearfix > li', headers => headers.length);
  console.log(headersCount)
  await browser.close();
}
  • もちろんgetElementByIdみたいな感じでJSから取得することも可能
  • ただし、selenideでできていたアサーションに関しては、puppeteerにはないので、jestあたりを使う必要がありそう(expect-pupperteerというのがあるらしい) github.com

まとめ

  • 元々はVSCodeで書ける言語ということで、Java/KotlinからTSへの移行を考えて始めた調査でした。
  • 実際にやってみると、selenideに慣れていたせいもあり、selenideから素のseleniumもしくはpuppeteerへの移行はちょっとハードルが高いなという印象が残った
  • 特に、selenideのアサーション周りで結構楽に実装できていた部分をpuppeteerを使うとしたら頑張る必要がありそう
  • ということで、最終的な結論として、個人的にはselenideに慣れてる人にとってはgaugeの為だけに言語を乗り換えるというのはあまりオススメできない

参考

Elixirで日付比較を不等号で行ってはいけない、絶対にだ!

はじめに

  • 釣りタイトルみたいな話ですが、実際に私がやってしまっていたことを包み隠さず話そうと思います
  • tipsみたいな記事なので大したことは書きません

実現したかったこと

  • 構造体の配列を日付の降順で並べ替えたいということを思っていた
  • ElixirにはEnum.sort/2があるから、その中で日付を不等号で比較したら終了でしょみたいな安易な発想をしました
iex(1)> value_hoge = %{date: ~D[2020-08-21], text: "hello"}
%{date: ~D[2020-08-21], text: "hello"}

iex(2)> value_fuga = %{date: ~D[2020-08-24], text: "hello"}
%{date: ~D[2020-08-24], text: "hello"}

iex(3)> [value_hoge, value_fuga] |> Enum.sort(&(&1.date > &2.date ))
[%{date: ~D[2020-08-24], text: "hello"}, %{date: ~D[2020-08-21], text: "hello"}]
  • ここまでは問題なく動いたが、試しに違う月のものを入れてみると
iex(4)> value_piyo = %{date: ~D[2020-06-30], text: "hello"}
%{date: ~D[2020-06-30], text: "hello"}
iex(5)> [value_hoge, value_fuga, value_piyo] |> Enum.sort(&(&1.date > &2.date ))
[
  %{date: ~D[2020-06-30], text: "hello"},
  %{date: ~D[2020-08-24], text: "hello"},
  %{date: ~D[2020-08-21], text: "hello"}
]
  • なぜか、6月30日が一番上に来てしまう!?
  • ではもっと昔のものを入れたらどうなるか???
iex(7)> [value_hoge, value_fuga, value_piyo, value_foo] |> Enum.sort(&(&1.date > &2.date ))
[
  %{date: ~D[1900-07-31], text: "hello"},
  %{date: ~D[2020-06-30], text: "hello"},
  %{date: ~D[2020-08-24], text: "hello"},
  %{date: ~D[2020-08-21], text: "hello"}
]

(o_ _)oバタッ

ちょっと調査してみる

iex(8)> inspect value_hoge
"%{date: ~D[2020-08-21], text: \"hello\"}"
iex(9)> inspect value_hoge, structs: false
"%{date: %{__struct__: Date, calendar: Calendar.ISO, day: 21, month: 8, year: 2020}, text: \"hello\"}"
iex(10)> inspect value_fuga, structs: false
"%{date: %{__struct__: Date, calendar: Calendar.ISO, day: 24, month: 8, year: 2020}, text: \"hello\"}"
  • Date型のデータを見た時に、daymonthyearの順番でキーが登録されていることがわかる
  • ということは、今までの挙動+構造体の中身的にmapを不等号で比較した時に、先に出てくるキーに対応するvalue同士で比較をしてしまっているのでは?という仮説がここで生まれる

ということで、不等号に関する公式参照してみる Operators — Elixir v1.10.4

The collection types are compared using the following rules: Tuples are compared by size, then element by element. Maps are compared by size, then by keys in ascending term order, then by values in key order. In the specific case of maps' key ordering, integers are always considered to be less than floats. Lists are compared element by element. Bitstrings are compared byte by byte, incomplete bytes are compared bit by bit.

書いてあった。どうやら、マップの比較では「マップのサイズ→キーの降順→キーの順番に対応する値」の順番で評価されていくようなので、今回はdateが最初に出てくるためこのようなことになってしまった模様

対策

  • そもそも、Date型の比較を不等号でやろうとしたのが悪いというのはあるとして、どうするのが良かったのか?という話で、結論としてはDate.compare/2を使えば良かったと思います
iex(1)> Date.compare(~D[2020-08-21], ~D[2020-08-22])
:lt
iex(2)> Date.compare(~D[2020-07-31], ~D[2020-08-22])
:lt
iex(3)> Date.compare(~D[1900-07-31], ~D[2020-08-22])
:lt
  • Date.compareは引数を2つ受け取って、1つ目の引数が2つ目の引数と比べてどうなのかという評価をしてくれます
  • 戻り値はatomで、:lt(less than),:gt(greater than) ,eq (equal)のいずれかなので、あとはパターンマッチさせればいいと思います
iex(10)> [value_hoge, value_fuga, value_piyo, value_foo] |> Enum.sort(&(Date.compare(&1.date, &2.date)) == :gt)
[
  %{date: ~D[2020-08-24], text: "hello"},
  %{date: ~D[2020-08-21], text: "hello"},
  %{date: ~D[2020-06-30], text: "hello"},
  %{date: ~D[1900-07-31], text: "hello"}
]
  • これで期待通りにソートされた。めでたしめでたし

まとめ

  • Elixirで日付を比較したい時は、横着して不等号で比較するなどしてはいけない絶対にだ。

ElixirのEnumをそれなりに使いこなしたい

はじめに

  • 今回のブログはElixirのEnum.XXを使いこなせるようになりたいという願望です
  • きっかけはプロダクションコードで、配列操作をするときにEnum.mapで力でねじ伏せることが多くなり、もっとスマートにもっとElixirらしくかけたらいいのにという思いが生まれたことです。 例えば、Enum.filter()Enum.find()の違いを知っておくことでコードの仕上がりもだいぶ変わってきます
  • なので、今回は知っていれば個人的に役立ったEnum以下の物を記して行きたいと思います

Enum

  • そもそも、ElixirでいうEnumとはenumerables (数えられる物たち)を操作するためのアルゴリズムの集合体みたいな位置付けです
  • 一般的に使うenumerables としては、ListRangeMapが挙げられます

Enum.以下にあるもの

  • 雑にiexを起動して、Enum以下にあるものを取得してみます
Interactive Elixir (1.10.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Enum.
EmptyError           OutOfBoundsError     all?/1
all?/2               any?/1               any?/2
at/2                 at/3                 chunk_by/2
chunk_every/2        chunk_every/3        chunk_every/4
chunk_while/4        concat/1             concat/2
count/1              count/2              dedup/1
dedup_by/2           drop/2               drop_every/2
drop_while/2         each/2               empty?/1
fetch!/2             fetch/2              filter/2
find/2               find/3               find_index/2
find_value/2         find_value/3         flat_map/2
flat_map_reduce/3    frequencies/1        frequencies_by/2
group_by/2           group_by/3           intersperse/2
into/2               into/3               join/1
join/2               map/2                map_every/3
map_intersperse/3    map_join/2           map_join/3
map_reduce/3         max/1                max/3
max_by/2             max_by/4             member?/2
min/1                min/3                min_by/2
min_by/4             min_max/1            min_max/2
min_max_by/2         min_max_by/3         random/1
reduce/2             reduce/3             reduce_while/3
reject/2             reverse/1            reverse/2
reverse_slice/3      scan/2               scan/3
shuffle/1            slice/2              slice/3
sort/1               sort/2               sort_by/2
sort_by/3            split/2              split_while/2
split_with/2         sum/1                take/2
take_every/2         take_random/2        take_while/2
to_list/1            uniq/1               uniq_by/2
unzip/1              with_index/1         with_index/2
zip/1                zip/2
  • するとこれだけ、取得できました。これを全部見ていくと時間が無限に溶けていくと思うので、Enum — Elixir v1.10.4 ここをみてもらうようにして、今回は個人的によく使うなと思った物のみにフォーカスを当てていきます。

Enum.flat_map

  • 基本的には、他の言語にもあると思うのですが配列の中に配列があるようなケースで、最終的にflattenしたいような場合にこれを使うといいと思います。
  • 元々は、Enum.map -> Enum.concatの流れでやっていたのですが、Enum.flat_mapすれば一発で終了します
# 配列の中に配列があるような物に対して処理をしたいとき
iex(4)> enumerable = [[1,2,3] ,[4,5,6]]
[[1, 2, 3], [4, 5, 6]]
# こんな感じでアクセスして、最後にconcatしてフラットにしてました
iex(5)> Enum.map(enumerable, fn array -> Enum.filter(array,fn item -> item != 0 end ) end) |> Enum.concat
[1, 2, 3, 4, 5, 6]
# それもflat_mapを使うことですっきりします
iex(6)> Enum.flat_map(enumerable, fn array -> Enum.filter(array,fn item -> item != 0 end ) end)
[1, 2, 3, 4, 5, 6]

Enum.find

  • Enum.filterとの使い分けをうまくできていなかったのですが、Enum.filter()は中のfunctionでtrueの物を全て返す、Enum.find()中のfunctionでtrueの物のうち1つ目のものを返し、見つからなければdafaultを返します
# 先ほどの配列の中から、それぞれの配列の中で0以外のものをとるときにfindの場合下記のようになります
iex(13)> Enum.flat_map(enumerable, fn array -> Enum.filter(array,fn item -> item != 0 end ) |> Enum.take(1) end)
[1, 4]
# ちょっと冗長な感じがしますが、これをEnum.findを使えばすっきりさせられます
iex(14)> Enum.map(enumerable, fn enumerable -> Enum.find(enumerable, fn item -> item != 0 end) end)
[1, 4]
# Enum.findの方が戻り値が1件と明確なので、配列の中から取り出されて帰ってくるのでflattenする必要もなくなります。ただ、該当するものがない場合はnilを返すのでそこは要注意です
iex(15)> Enum.map(enumerable, fn enumerable -> Enum.find(enumerable, fn item -> item == 0 end) end)
[nil, nil]
# この場合はfindの第2引数にdefault値を詰めればよさそうです
iex(16)> Enum.map(enumerable, fn enumerable -> Enum.find(enumerable,0, fn item -> item == 0 end) end)
[0, 0]

Enum.reduce

  • どの言語にもありますが、さらっとreduceについても触れておきます
  • 例えば、配列の中身を掛け合わせたい場合とかに使えます
iex(6)> Enum.reduce([1, 2, 3, 4], fn x, acc -> x * acc end)
24
# accを用意しておけば、それに加算していくみたいな処理もできる
iex(7)>Enum.reduce([1, 2, 3], 0, fn x, acc -> x + acc end)
6
# strtingでも可能ですが、1件目から処理して先頭に追加されていくのでreverseする必要はあります
iex(8)>Enum.reduce(["h","e","l","l","o"], "", fn x, acc -> x <> acc end)
"olleh"
iex(9)> Enum.reduce(Enum.reverse(["h","e","l","l","o"]), "", fn x, acc -> x <> acc end)
"hello"

Enum.zip

  • これは結構便利なのですが、配列の中で複数の配列を持っているときにそれぞれの対応性を持たせたいときに使う関数です
  • それぞれの配列の要素の頭からtuppleにしていって組み合わせてくれます
  • 戻り値はtuppleの配列です。(配列がネストされていても勝手にflattenしてくれます)
iex(10)>Enum.zip([[1, 2, 3], [:a, :b, :c]])
[{1, :a}, {2, :b}, {3, :c}]
# 仮に要素数が一致してない配列の場合は、少ない方の要素数までをzipしてtuppleの配列にしてくれます
iex(11)> Enum.zip([[1, 2, 3, 4], [:a, :b, :c]])
[{1, :a}, {2, :b}, {3, :c}]
# また、1つの配列にしなくても2つの配列であれば渡すことは可能です
iex(12)> Enum.zip([1, 2, 3], [:a, :b, :c])
[{1, :a}, {2, :b}, {3, :c}]
# この場合はアリティが多いので怒られます
iex(13)> Enum.zip([1, 2, 3], [:a, :b, :c], ["foo", "bar", "baz"])
** (UndefinedFunctionError) function Enum.zip/3 is undefined or private. Did you mean one of:

      * zip/1
      * zip/2

    (elixir 1.10.3) Enum.zip([1, 2, 3], [:a, :b, :c], ["foo", "bar", "baz"])
# 配列でwrapしてあげればOK
iex(13)> Enum.zip([[1, 2, 3], [:a, :b, :c], ["foo", "bar", "baz"]])
[{1, :a, "foo"}, {2, :b, "bar"}, {3, :c, "baz"}]
# ちなみにこれは通るけど
iex(14)> Enum.zip([[1, 2, 3]])
[{1}, {2}, {3}]
# こっちは怒られる。アリティが1つの場合、配列であり、中身も配列であることを期待しているので
iex(15)> Enum.zip([1, 2, 3])
** (Protocol.UndefinedError) protocol Enumerable not implemented for 1 of type Integer
    (elixir 1.10.3) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir 1.10.3) lib/enum.ex:141: Enumerable.reduce/3
    (stdlib 3.13) lists.erl:1338: :lists.foreach/2
    (elixir 1.10.3) lib/stream.ex:1175: Stream.do_zip/3
    (elixir 1.10.3) lib/enum.ex:3134: Enum.zip/1

まとめ

  • Enum.XXは他にもたくさんありますが、Enum.map, Enum.flat_map, Enum.zip, Enum.reduce, Enum.find あたりを知っておけば困らないと個人的には思います
  • あとは実際に書いてみて、こんなやつないのかなと探してみるといいかなと思いました