RailsのStrong Parameterで配列を許可したい

最近、Ruby on Railsで開発をすることがあり、メンバーが書いたコードを参考させてもらいながら書いたりしています。

今回、Strong Parameterで配列を扱いたかったときに上手く値を取得できなくて、少し詰まったのでログを残して置きます。

やりたかったこと

やりたかったことは、クライアントから以下のようなJSONを受け取り、複数のIDを配列で受け取りたかった。

{
    "xxx_id": "xxx",
    "yyy_ids": [
        "yyyy_1",
        "yyyy_2"
    ]
}
def demo_params
  params.permit(:xxx_id, :yyy_ids)
end

起きた現象

実際に、やりたかったことで書いたdemo_params のようなコードを書いて、複数IDを受け取れるかと思ったが、ログに出力してみると何も表示されませんでした。

解決方法

どうも配列を受け取りたい場合は、以下のようにキーに空配列を指定してあげる必要がありました。

def demo_params
  params.permit(:xxx_id, yyy_ids: [])
end

参考

railsguides.jp

2023年を振り返る

みんな書いてるので書いてみようと思う。

といっても、軽くだけど。

TL;DR

  • 転職した
  • 副業始めた
  • ライブによく行った

仕事

今年の一番大きいイベントなのが、2023年3月にスタフェスに転職したこと。

blog.ryskit.com

最初の方は結構手を早く動かしてタスクを潰していけた気がする。特にDB周りのインテグレーション環境を整えたり、リファクタしたりと多少気合のいるものもパワープレイでどうにかできた。

ただ、後半は少し手を動かす速さは失速気味になってきていた。昔からあるアプリケーションやDB、業務知識などが求められるタスクも増えてきて、仕様を確認するためにコードを読んだり相談したりすることも多くなっていた。

なぜ今そうなっているのか、今の業務はアプリケーションを使ってどのように進めているのかを理解するのにとても時間が掛かってしまっていた。

また、自分のモデリング・リモデリングの実力不足、かつ上手くチームに貢献できている感覚がない期間もあり、自信を失くしていたときもあった。

設計・モデリングに関しては来年の課題としてインプット・アウトプットの量を意識していこう!

来年はロードマップに沿って機能開発を進めつつ、チームメンバーの数が少ないので少人数で開発しやすい形にアプリケーションを改善したり、その他利用者目線での改善などもどんどんしていきたい!

副業

今年からがっつり副業を始めた。

本業でフロントエンドを書き始めてできるようになってきたので、よりフロントエンド・バックエンド・インフラ領域を垣根なく動ける仕事も他の時間でやりたいなと思い始めた。

あと、自分がどれぐらい稼げるのか試してみたかったというのもある。

結果としてはやってみて良かったし、もっと早く挑戦するべきだったなと思う。

ありがたいことにフロントエンドの経験はあまりないけどフロントエンドのタスクもやらせてもらえていて、かつエディタやReact Flowを使ったUIなど結構挑戦的なものも多いのでやっていて楽しい!

生活

音楽

コロナの影響が少なくなり、ライブも行きやすくなってきたので今年は好きなアーティストのワンマンライブに何回か行った!

すでに来年行くライブもいくつか決まってるし、来年こそはCDJにも参加したい!

ゲーム

今年はApexにハマった!

スタフェスの人たちがやっているので、自分も軽い気持ちで始めたらドハマりしてしまった。

本当に時間が溶けてなくなるのでやり過ぎ注意。気をつけよう(気をつけない)

最後に

皆さん、良いお年を!

どうして入社してすぐにチームに馴染めたのかを振り返ってみる

こんにちは!ryskitです。

この記事はスターフェスティバル Advent Calendar 2023の1日目の記事です。

qiita.com

はじめに

スターフェスティバル株式会社(通称、スタフェス)に2023年の3月に入社してから早くも10ヶ月目になりました。早すぎる...

入社してKitchen Successチームに参加してからすぐ開発に参加して機能開発を始めたのですが、チームメンバーから「馴染むのが早い」「キャッチアップが早い」「WOW(スタフェスのバリュー)」とフィードバックをいただけることがありました!

入社してから最初の約3ヶ月間ぐらいの振る舞いは、チームメンバーとして将来の仕事のやりやすさに影響する(つまり、チームメンバーに信頼してもらえるか)と考えていて、この期間に何を意識していたかを振り返ってみようと思います。

※ 言わずもがなですが、継続的に結果を出して信頼してもらうことも大切

まずは小さいタスクを倒していく

まずは大きいタスクを引き受けるのではなく、自分でも1~2日でできそうな小さなタスクを何回か倒していくことで成功体験を積み、チームでやっていけるかも...?という自信に繋げました。

  1. 入社2日目にローカル環境を構築するためのスクリプトのバグを修正
    • ikkitangさん にPRのRTAやりましょう!と言われて出したPR
  2. 複数のアプリケーションを跨いだ修正が必要が機能の開発
    • 最初の方は あひるさんペアプロをしてもらった

特に2に関してはすごく感謝していて、アプリケーションの仕様やドメインの理解が浅い状態で開発に臨むには多少不安が付きまといますが、ペアプロだとそこらへんの不安はなく開発を進めることができました。

入社してすぐはアプリケーションの仕様は理解していないし、ドメインの理解も浅い。

そんな中で一人で開発を進めるのは不安にはなるけど、ペアプロだと分からないところはすぐに聞けるしチームではどのようにコードを書いているかも学ぶこともできます。しかも、達成すると自信も付く!最高!ありがとう!

開発を進められる必要最低限を素早くキャッチアップする

ScalaJava等を使ったバックエンド開発とAWSのインフラ構築などをやっていたので、スタフェスで利用しているTypeScriptでのフロントエンドやバックエンドの開発経験はありませんでした。

そのため、業務を通してアプリケーションの仕様を理解しつつ、まずは機能開発を進められる最低限の技術を学ぶことを意識しました。

例えば、今回キャッチアップしたときのキーワードが以下で、よく使いそうな部分から学びました。

  • TypeScript
  • Kysely
  • Next.js
  • React.js
  • GraphQL

ただ、スタフェスに入社してから初めてReact.js / Next.jsに挑戦したため、未だに苦戦している部分ではあります。笑

不安でも積極的に突っ込んでいく

大体入社して3ヶ月間ぐらいは色んなことに慣れるまでに時間が掛かるので、もし積極的に突っ込んで開発に多少時間が掛かってしまっても大目に見てもらえるかなと思っています。

なので、自分のできる範囲で良いので、分からなくても不安でも積極的に突っ込んで行くのが良いと思います。突っ込んでいくことで分かるようになるし不安も減っていきます。

例えば、入社した当時のチームにはフロントエンドに精通している みやっちさん はいましたが大半のメンバーはバックエンドの方が得意でした。

そのため、フロントエンドの開発タスクを進める人数が少なかったのと僕が挑戦してみたかったということで、タスクを任せてもらえることになりました。

正直、フロントエンドなんて分からないし合っているか不安でしたが、運良く精通しているメンバーがいてレビューもしてもらえたので少しずつできるようになり、このおかげ(?)で少しはフロントエンドを任せても良さそうと思ってもらえているのかなと思います。

積極的にコミュニケーションを取る(2023/12/02 加筆)

エンジニアに関して、スタフェスではフルリモートで働くことが認められていて、僕も福岡の自宅から仕事をしています。

入社したときにフルリモートで何が大変かというと、コミュニケーションの接点が少ないことだと思います。

オフィスで働いていた頃はメンバーとランチに行ったり飲みに行ったりと仕事以外でのコミュニケーションの接点が意図せずとも発生するので、関係性を築いたりメンバーとの距離を縮めるのはそこまで意識しなくてもできたように感じます。

しかし、フルリモートだとそうもいかないため、意識的にSlackでリアクションをしたり雑談の輪に積極的に入って少しでも直接会話ができるようにしていました。

特に自分のチームだと夕方頃にSlackで雑談タイムが始まったりするので、自分が入っていくことで話す機会が得られる場を用意してくれていたのはすごく感謝しています!

まとめ

振り返ってみると脳筋的なアプローチな感じが否めないですが、いかがでしたでしょうか?

今回、チームに馴染むために個人が意識したことを書きましたが、チームメンバーが積極的に良かった部分を褒めてくれたり、挑戦に寛容で不安をあまり感じさせない姿勢であることも、チームにすぐ溶け込むことができた要因だと思います。

良いチームで良い仕事をしていけるようにしていきたいですね!頑張るぞ!

次は?

スターフェスティバル Advent Calendar 2023の2日目は yoshifujiT さんが担当です!

qiita.com

スターフェスティバルに転職してました

お久しぶりです、木田です。

定期的に書かないと、ブログ書くのが億劫になってしまって前回の記事から半年以上経ってしまいました。

転職しました🎉

実は、2023年3月に スターフェスティバル株式会社 に転職していて、すでに5ヶ月ぐらい経ってしまいました。

stafes.notion.site

入社してSlackに入ったら、自分のオンボーディング用のslackチャンネルでWelcomeメッセージをたくさんもらってすごく嬉しかったのを今でも鮮明に覚えてます。

転職の軸

今回の転職活動の中で、自分が気になった・興味がある企業の方々とカジュアル面談・面接をさせてもらいました。

その中で、軸の一つとして意識していたのは、企業文化です。

特に、エンジニア組織の文化や開発の進め方などが自分とマッチするか、というのは非常に重要視していて、ここがマッチしてないと長期的に結果を出したり働いていてもお互いに辛い状況が発生するからです。

もう一つの軸が、一緒に働く仲間、です。

全員と話せるわけではないのですべては分からないですが、一緒に働くであろう仲間はどんな人たちなのか、というのは自分にとってはすごく大切でした。

カジュアル面談やテックブログ、イベントを通して、どんな人たちがいるのかはすごく知ろうとしました。

特に、仲良くできることはもちろんのこと、やっていき・のっていきがあるか、みたいなのは、ノリが合うかぐらい大切だと思ってます。笑

※ やっていき、のっていきはペパボさんのスライドがすごく分かりやすいです!

tech.pepabo.com

最後の軸が、職種に縛られずに幅広く業務を行えるか、です。

自分が得意なのはバックエンドの開発ですが、AWSなどのクラウドインフラ周りを整備したりするのも好きですし、あまり実践経験はないけれどフロントエンドもどんどんやっていきたいと思っていました。

とにかく必要なことは全部やりたい主義で、そういうことを許してくれそうな会社があると嬉しいなと思いながら転職先を探していました。

スタフェスに入った経緯

転職活動前にスタフェス meetup#1 に参加したことがあり、技術的におもしろいことをしてるし、働いているエンジニアの方も素敵な人たちだなと思っていました。

zenn.dev

転職活動を始めて1ヶ月ぐらいしてから、今所属しているチームのTechPMである ikkitang さんからスカウトメッセージをいただいて、びっくりしましたがすぐに返信しました。

というのも、スカウトメッセージの内容(ぼかしてます)が

  • 自分の軸とこういう風にマッチしてるよ!
  • 活躍してもらえそうなポイントはこれだけあるよ!
  • スタフェスではこういうところで活躍できる機会を提供できるよ!
    • 自分の場合はこういうことしたよ!

と丁寧なメッセージをいただいたので、かなり嬉しかったんですね。

複数の企業様とのカジュアル面談や面接を通して、転職の軸などを照らし合わせて、最終的に自分とマッチしそうなのは スタフェス だなと思って入社を決めました。

今どんな感じ?

まだ入社して5ヶ月ぐらいですが、すでに馴染みすぎてるねw と言われるぐらい馴染めているなと自分でも思います。笑

最初は簡単なAPIの修正から始まり、気づいたらバックエンドの開発に加えて、フロントエンドの開発もさせてもらったり、かなり楽しく仕事をさせてもらってます。

フロントエンドに関しては、めちゃめちゃ詳しい方がいらっしゃるので、分からないことは気軽に聞けるし、優しく教えてくれるので本当にありがたい。

あと、スタフェスのエンジニアは褒めるのが上手というか、色々仕事をすればちゃんと褒めてくれるので、もっと頑張ろー!!ってなるんですよね。本当に仕事が楽しい!

自分が何より楽しく働けるのは、周りにいる人たちのおかげなので、本当に恵まれているなと思います。

最後に

スタフェスに入社したからには、エンジニアとしてもっと強くなりたいし、仕事をWOW!なスピードで進めたいし、頼られる存在にもなりたいわけです。

自分が見習うべき人たちが社内にはたくさんいるので、追いつけ・追い越せで、より良い仕事をしていきたいなと思います!

Github Actionsを使ってプルリクエストを自動でマージしたい

Scalaを利用したプロジェクトだとScala Stewardを導入しているところも多いのではないでしょうか?

Stewardは便利なんですがPRをいちいち手動でマージするのが面倒なので、それを自動化するのが本ブログの趣旨です。

Scala Stewardとは?

github.com

Scala Stewardはプロジェクトで使われているScalaのライブラリの依存関係、sbtプラグイン、Scalaとsbtのバージョンを最新に保つためのボットです。

簡単に言うと、ライブラリのバージョンアップがあったら自動でPR作ってくれる便利なやつ。

似たようなボットでいうとDependabotとかですかね。

github.com

前提

プロジェクトによってGithubのリポジトリの設定は違うと思いますが、ある程度前提を揃えておきましょう。

ブランチ保護設定

  • Protect matching branches
    • ✅ Require a pull request before merging
      • ✅ Require approvals: 1
  • ✅ Require status checks to pass before merging
    • 他のワークフローでUnitTestやIntegrationTestが実行されているイメージ

やりたいこと

StewardがPRを作成したらユニットテストやインテグレーションテストがCIで実行され、それらが完了して成功していれば自動でマージを行いたい。

対応方法

以下のようなGithub Actionsのワークフローを記載します。

name: Steward Auto Merge
on:
  pull_request:
    branches:
      - main
permissions:
  pull-requests: write
  contents: write
jobs:
  auto-merge:
    runs-on: ubuntu-latest
    if: startsWith(github.head_ref, 'update/')
    env:
      PR_URL: ${{github.event.pull_request.html_url}}
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - name: Approve PR
        run: gh pr review "$PR_URL" --approve
      - name: Enable auto-merge for Steward PRs
        run: gh pr merge --merge --auto "$PR_URL"

解説

on.pull_request.branches

まず、on.pull_request.branches には main を指定しています。

ここでよく間違えるのが、botが作成するブランチ名を指定してしまう点です。

branchesにはPRが作成されてマージされるブランチ対象の名前を指定する必要があります。

自分は認識を間違えていてワークフローが起動されずに時間を溶かしてしまいました。

Github Actions Workflow Trigger#Pull Request

zenn.dev

jobs.*.if

job.*.if を使うことでジョブの実行条件を書くことができます。

ここでは、PRのブランチ名のプレフィックスがupdate/であればジョブを実行するように定義しています。

もし特定のアクターがPRを作成しているのであれば、以下のように定義しても良いかもしれません。

if: ${{ github.actor == 'my-dependabot' }}

条件を使用してジョブの実行を制御する

gh pr review --approve

Githubのgh pr review --apprveコマンドを使用して、PRをapproveしています。

これでブランチ保護の1人以上のApprove必須は突破できます。

cli.github.com

gh pr merge --merge --auto

Githubのgh pr merge --autoコマンドを使用して、PRをマージします。

正確にいうと、上記のコマンドを実行することで一旦マージキューに入れて、自動マージを有効にします。

ブランチ保護の設定内容は有効なので、その他のワークフローのchecksがすべて成功していれば自動でマージされますし、もしユニットテスト等のワークフローが失敗した場合は自動マージされないので安心です。

ここで注意点なのが、リポジトリのAllow auto-merge設定は必ず有効にしておきましょう。

この設定が有効でないと、自動マージを有効にしようとするとワークフローでエラーが発生してしまいます。

プルリクエストを自動的にマージする

余談

Github Appを作成してadmin権限で gh pr merge --merge --admin を実行するとPRのapprovalの必須設定を避けてマージすることはできます。

しかし、他のワークフローのchecksの成功を待たず、マージキューにも追加せずに対象ブランチに直接マージするため、ユニットテスト等が失敗していてもマージされてしまいます。

そのため、もしadmin権限でマージしたい場合は、マージを実行するステップの前に他のワークフローがすべて完了して成功しているかをチェックするステップを挟んだほうが良いかもしれません。

ただ、その場合は待っている間はスリープしたりする必要があると思うのでその間余計なGithub Actionsの使用料金が掛かってしまうため難しいところです。

最後に

この記事が誰かの役に立てば幸いです。

以上。

DMM英会話を始めて総英会話時間が1,000分になりました

どうも、お久しぶりです。

気がつくともう11月に突入し、2022年も残りわずかとなってきましたね。早すぎる!

ほとんどブログも書かずに何に熱中してたか・時間を使っていたかというと、最近は"英語力"を伸ばそうと毎日頑張っている次第です。

というのも、Duolingo English Testのスコア投稿にも書いたように、会社でプロダクト開発を行うにあたり、ニュージーランドチームや日本のビジネス・モバイルアプリチームと要求・仕様を整理・検討し開発までの流れのコミュニケーションを英語・日本語を使って行っています。

この仕事の中で、Github上のIssueでの議論や毎週行われる会議ももちろん英語です。

日本語でも難しい仕様に関する議論を英語で行うには、あまりにもスピーキング能力が足りないので、正確に伝わらない・会議を開いてもらっているがその場ですぐに意見を発言したり反論ができず、ただただ時間が無駄になっている感覚がありました。

今すぐ英語学習をしてもすぐには効果が出ないのは理解しつつ、やらないよりはやって少しでも話せるようになろうと思いました。

ということで、とにかくインプットの時間を増やして、それをアウトプットしようとDMM英会話を9/22から始めてみました。

感想

DMM英会話を始めて良かったなと思います。

今まではDuolingoやスピークバディ、英文法の勉強のみしていただけなので、少しのインプットはあっても書く・話すというアウトプットする機会が少なかったのですが、それをDMM英会話を使うことで改善できたのは良かったです。

また、レッスンで何回も英語を話すことで間違うことを恐れずに話せるようになったのも良かったです。

英会話のスケジュールに関してですが、25分~/1日を週6で回しています。

契約当初は25分 * 3回/1日できるプランを契約していたのですが、平日仕事終わってから3回やるのはさすがに続かなかったので今のようなスケジュールになっています。

2022/11/05時点で1,000分(回数でいうと40回)に到達したので、ひとまず継続できていることが嬉しい!

課題

英会話を始めて実際に仕事の会議でスッと言葉が出てきやすくなったものの、まだまだ正確に伝えたり複雑だったり難しい文になると途端に話せません。

DMM英会話のレッスンを受けていても同じで、教材の中にディスカッションをするExerciseがあったりするのですが、言いたいことがあるんだけど伝えられないことがあったりします。

そういうのをメモしておいて、あとでどう伝えるべきだったかを思い出してライティングするなどして練習しています。

また、英文法の学び直しをしつつ、「日本語(思考) → 英語(変換) → 発声」ではなく「英語(思考)→ 発声」ができるように、とにかく声を出して読んで口に英語を覚えさせようとしています。

この英語学習で合っているのか分からないですが、とにかく先生には英文を読めと言われたので、言われた通りにやります!

今後

ニュージーランドのメンバーと技術的な議論をスラスラとできることが目標なのですが、道のりが長いのでマイルストーンをいくつか切る必要があると思っています。

なので、DMM英会話でいうと次のゴールドランク(総英会話時間が3,000分/120レッスン)を最短で目指して頑張ろうと思います!

では。

紹介コード

もしよかったら、紹介コードを使ってください!

この紹介コードを使うことで、お互いが「プラスレッスンチケット」が3枚付与されるのでお得です!

eikaiwa.dmm.com

2回目のDuolingo English Testを受けてきました

どうも、お久しぶりです。

早くも9月末になり、会社では評価面談の時期で個人的には憂鬱な気分になります。

それはさておき、日々Duolingoアプリを使って勉強したり、スピークバディというアプリを使って話す練習をしたり、英文読解をしたり少しずつではありますが英語を勉強していました。

なので、定期的に結果を計測しておこうということで数日前に2回目のDuolingo English Testを受けてきました。

Duolingo English Testがどんなものかは以下の記事に記載してるので、興味があれば呼んでもらえると嬉しいです。

blog.ryskit.com

やっぱり受けてみて、いちいち会場までテストを受けに行かなくて済むのが本当に最高ですね!

思い立ったらすぐに試験料払って受験できる体験が毎回最高すぎます!!

結果

certs.duolingo.com

前回が総合スコアが90で、今回の総合スコアは85でした!

いや、スコア落ちとるやないかーーーーい!!!

正直、このスコア見て心の中で自分に突っ込んでしまいました。

ただ、細かく見ると以下のような感じでした。

  • Literacy(受験者の読んで書く能力)は前回より5だけ下がった
  • Comprehension(受験者の読んで聞く能力)は前回と同じ
  • Conversation(受験者の聞いて話す能力)は前回より5だけ上がった
  • Production(受験者の書いて話す能力)は前回より20上がった

ConversationやProductionは前回低すぎて悔しかった領域だったので、少しだけですが上がっていてよかったです。

ただ、Literacyは前回よりも下がっていたのでちゃんと読解練習できてないんだなというのは分かりました。(やり方の見直しが必要)

感想

話す・書く能力がもともと低く、その部分を鍛えるための勉強はしていたつもりだったのでそこは少しですが結果が出ていて良かったです。

ただ、会社の会議でNZチームのメンバーと英語で会話をする必要があるのですが、正直毎週準備をしていてもイレギュラーなことを聞かれてしまうとアワアワして上手く話せない状態です。

これでいかんと今週からDMM英会話も始めてみました。これが数カ月後のDuoligo English Testに効果があるのか個人的にも楽しみです!

とにかく、結果は受け止めて全体的に英語力の伸ばせるように日々学習を継続しようと思います。以上!

Foldable and Traverse

今日も今日とてScala with Catsを読む。

この章ではコレクションに対するイテレーションの2つの型クラスについて見ていく。

  • Foldable
    • foldLeftfoldRightの操作を抽象化したもの
  • Traverse
    • Applicativeを使ってfoldingよりも少ない手間でイテレートを行う高度な抽象化をしたもの

Foldable

CatsのFoldablefoldLeftfoldRightを型クラスに抽象化したもの。

Foldableのインスタンスはこの2つのメソッドが定義し、多くの派生メソッドを継承している。

import cats.Foldable

object Playground1 extends App {

  val ints = List(1, 2, 3)
  println(Foldable[List].foldLeft(ints, 0)(_ + _))
}

Folding Right

FoldablefoldRightの定義はfoldLeftと違い、Evalモナドが使われている。

def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]: Eval[B]

Evalを使うということは、常にスタックセーフにfoldingできるということ。

Folding with Monoids

FoldablefoldLeftの上に定義された多くの便利メソッドを提供してくれる。

それらの多くは標準ライブラリにある馴染みのあるメソッドを模倣したもの:find, exists, forall, toList, isEmpty, nonEmpty ...

これらに加えて、CatsはMonoidを使った2つのメソッドを提供する。

  • combineAlll(alias: fold)
    • Monoidを使って連続したすべての要素を結合する
  • foldMap
    • ユーザーが与えた関数をシーケンスにマッピングし、その結果をMonoidを使って結合する
import cats.instances.int._
import cats.instances.string._

Foldable[List].combineAll(List(1, 2, 3))
// 6

Foldable[List].foldMap(List(1, 2, 3))(_.toString)
// "123"

Traverse

Traverse型クラスはApplicativeを活用して、より便利で法則性のある反復処理のパターンを提供する上位のツールです。

CatsのTraverseの定義

trait Traverse[F[_]] extends Functor[F] with Foldable[F] with UnorderedTraverse[F] { self =>
  
  def traverse[G[_]: Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]]

  def sequence[G[_]: Applicative, A](fga: F[G[A]]): G[F[A]] =
    traverse(fga)(ga => ga)
}

CatsはList,Vector,Stream,Option,Either, その他の型のためのTraverseのインスタンスを提供する。

import cats.Traverse
import cats.instances.future._
import cats.instances.list._

import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.DurationInt

object Playground2 extends App {

  val hostnames = List("google.com", "facebook.com", "instagram.com")

  def getUptime(hostname: String): Future[Int] =
    Future(hostname.length * 60)

  val totalUptime: Future[List[Int]] =
    Traverse[List].traverse(hostnames)(getUptime)

  println(Await.result(totalUptime, 1.second))

  val numbers = List(Future(1), Future(2), Future(3))

  val numbers2: Future[List[Int]] =
    Traverse[List].sequence(numbers)

  println(Await.result(numbers2, 1.second))
}

Semigroupal and Applicative

今日も今日とてScala with Catsを読んでいく。

Semigroupal and Applicative

  • Semigroupal
    • コンテキストのペアを構成する概念を内包する
    • CatsはSemigroupalFunctorを利用して、複数の引数を持つ関数のシーケンスを可能にするcats.syntax.applyモジュールを提供する
  • Parallel
    • Parallelはモナドインスタンスを持つ型をSemigroupalインスタンスと関連する型に変換する
  • Applicative
    • ApplicativeSemigroupalFunctorを継承する
    • コンテキスト内のパラメータに関数を適用する方法を提供する

Semigroupal

cats.Semigroupalはコンテキストを組み合わせることができる型クラスである。

もし、F[A]F[B]という2つの型のオブジェクトがあったら、Semigroupa[F]F[(A, B)]のような形に組み合わせることできる。

trait Semigroupal[F[_]] {
  def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]
}

Optionを例として以下の実行結果を見てみると分かるように、両方の値がSomeの場合はタプルで値を返すが、一方がNoneの場合は結果はNoneとなる。

println(Semigroupal[Option].product(Some("abc"), Some(123)))
// Some((abc,123))
println(Semigroupal[Option].product(Some("abc"), None))
// None

Semigroupal Laws

Semigroupalの法則は一つで、productメソッドが結合法則を満たしていることである。

product(a, product(b, c) == product(product(a, b), c)

Apply Syntax

CatsはSemigroupalのメソッドショートハンドであるtupledmapNなどの便利なapply syntaxを提供している。

Semigroupal Applied to Different Types

Semigroupalはいつも期待するような振る舞いはしない。特にMonadインスタンスも持つ型だ。

Future

import cats.syntax.apply._

import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.DurationInt


case class Cat(name: String, yearOfBirth: Int, favoriteFoods: List[String])

val futureCat =  (Future("Garfield"), Future(1978), Future(List("Lasagne"))).mapN(Cat)

println(Await.result(futureCat, 1.second))
// Cat(Garfield,1978,List(Lasagne))

List

SemigroupalのListを組み合わせた場合、いくつか期待しない結果になる。

println(Semigroupal[List].product(List(1, 2), List(3, 4)))
// List((1,3), (1,4), (2,3), (2,4))

Either

Eitherにproductメソッドを適用するとエラーが累積されることを期待するが、結果を見てみると分かるように最初のエラーのみが結果として返されている。

type ErrorOr[A] = Either[Vector[String], A]

println(
  Semigroupal[ErrorOr]
    .product(Left(Vector("Error 1")), Left(Vector("Error 2")))
)
// Left(Vector(Error 1))

Semigroupal Applied to Monads

ListやEitherに対するproductメソッドの結果が期待したものと違っている理由は、それらがモナドであるから。

モナドの場合は、productメソッドの実装は次のようになる。

import cats.Monad
import cats.syntax.functor._
import cats.syntax.flatMap._

def product[F[_]: MOnad, A, B](fa: F[A], fb: F[B]): F[(A, B)] =
  fa.flatMap(a => fb.map(b => (a, b)))

productメソッドの実装によって異なるセマンティクスをもつのはとても奇妙なことだ。

そこで、Catsのモナド(Semigroupalを拡張したもの)は上記のような標準実装を提供している。

Parallel

Paralell型とそれらに関連する構文によって、ある種のモナドの代替的なセマンティクスにアクセスすることができる。

import cats.Semigroupal
import cats.syntax.parallel._
import cats.syntax.apply._

type ErrorOr[A] = Either[Vector[String], A]

val error1: ErrorOr[Int] = Left(Vector("Error 1"))
val error2: ErrorOr[Int] = Left(Vector("Error 2"))

println(Semigroupal[ErrorOr].product(error1, error2))
// Left(Vector(Error 1))

println((error1, error2).tupled)
// Left(Vector(Error 1))

println((error1, error2).parTupled)
// Left(Vector(Error 1, Error 2))

parTupledを使うと両方のエラーが返却されるのが分かる。

なぜこのような振る舞いになるのかParallelの定義を見てみる。

trait Parallel[M[_]] {
  type F[_]

  def applicative: Applicative[F]
  def monad: Monad[M]
  def parallel: ~>[M, F]
  • M型のモナドインスタンスがある
  • 関連するFの型コンストラクタがあり、それはApplicativeインスタンスを持っている
  • MからFに変換できる

※ 型コンストラクタ(type-constructor)は、型を引数に取って新しい型を作るもの

~>というのはFunctionKのエイリアスで、~>[M, F]はMからFに変換するということを表す。

FuntionK M ~> F は、M[A] 型の値から F[A] 型の値への関数である。

import cats.arrow.FunctionK

object optionToList extends FunctionK[Option, List] {
  def apply[A](fa: Option[A]): List[A] =
    fa match {
      case None => List.empty[A]
      case Some(a) => List(a)
   }
}

optionToList(Some(1))
// List(1)

optionToList(None)
// List()

Apply and Applicative

Catsは2つの型クラスを用いてApplicativeをモデル化する。

1つ目はcats.Applyで、これはSemigroupalFunctorを継承しており、コンテキスト内の関数にパラメータを適用するapメソッドを追加したもの。

2つ目は、cats.Applicativeで、これはApplyを継承し、pureメソッドを追加したもの。

trait Apply[F[_]] extends Semigroupal[F] with Functor[F] {
  def ap[A, B](ff: F[A => B](fa: F[A]): F[B]

  def product[A, B](fa: F[F], fb: F[B]): F[(A, B)] =
    ap(map(fa)(a => (b: B) => (a, b)))(fb)
}

trait Applicative[F[_]] extends Apply[F] {
  def pure[A](a: A): F[A]
}

モナドの型クラスの階層

Monda Transformers

引き続き、Scala with Catsを読んでいく。

Monad Transformers

Monadはネスト化されたfor-comprehensionsによってコードを肥大化させる可能性がある。

Exercise: Composing Monads

M1, M2というモナドを合成したComposed[A]という型のflatMapを実装できるかという問題。

import cats.Monad
import cats.syntax.applicative._

object Exercise5_1 extends App {

  def compose[M1[_]: Monad, M2[_]: Monad] = {
    type Composed[A] = M1[M2[A]]

    new Monad[Composed] {
      def pure[A](a: A): Composed[A] =
        a.pure[M2].pure[M1]

      def flatMap[A, B](fa: Composed[A])(f: A => Composed[B]): Composed[B] = ???
    }
  }
}

M1, M2について知らずにflatMapの一般的な定義を書くことは不可能である。

しかし、どちらかのモナドについて知っていれば、上記のコードを書くことができる。

たとえば、M2がOptionだとすると、flatMapの定義はこうなる。

def flatMap[A, B](fa: Composed[A])(f: A => Composed[B]): Composed[B] =
  fa.flatMap(_.fold[Composed[B]](None.pure[M1])(f))

上記の定義で出てくるNoneというのはOption固有の概念で、一般的なモナドには出てこない。

Optionを他のモナドと合成するためにより詳細な情報が必要になる。

他のモナドについても同様で、flatMapメソッドを書くのに役に立つ。これはモナドトランスフォーマーの背景にあるアイデアである。

Catsは様々なモナドのためにトランスフォーマーを定義し、それぞれが他のモナドと合成するために必要な追加の情報を提供している。

A Transformative Example

Catsではたくさんのモナドトランスフォーマーを提供しており、それらの名前のサフィックスにはTという名前がつけられている。(e.g. EitherT)

EitherTはEitherと他のモナドを合成し、OptionTはOptionと他のモナドを合成する。

Monad Tranformers in Cats

各モナドトランスフォーマーはデータ型でcats.dataに定義されており、モナドのスタックをラップして新しいモナドを生成することが可能。

Summary

モナドトランスフォーマーの型シグネチャは内から外に向かって書かれている。

例えば、EitherT[Option, String, A]というシグネチャの場合はOption[Either[String, A]のラッパーとなる。

参考資料

MonadTransformer とは何か · GitHub

everpeace.hatenadiary.org