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

こんにちは!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

Monads

Scala with Catsを読んでいく

What is a Monad?

ざっくり言うと、MonadはコンストラクタとflatMapメソッドを持つもの。

Option[A] flatMap (A => Option[B]) => Option[B]

すべてのMonadFunctorでもある。

flatMapmapメソッドを持っていれば、for-comprehension(For式)を使うことができる。

Monad Laws

  • Left identity
    • pure(a).flatMap(func) == func(a)
  • Right identity
    • m.flatMap(pure) == m
  • Associativity
    • m.flatMap(f).flatMap(g) == m.flatMap(x => f(x).flatMap(g))

ここらへんの圏論用語がいまいち分からない。

  • identity ... 恒等射
  • associativity ... 結合律

Monads in Cats

cats.Monadは2つの型クラスを継承している。

  • FlatMap type class
    • flatMapメソッドを提供している
  • Applicative
    • pureメソッドを提供している
    • ApplicativeはFunctorを継承しているため、すべてのMonadでmapメソッドを使うことができる

Error Handling

プログラムで発生する可能性のあるエラーを表す代数的データ型を用いたアプローチ

sealed trait LoginError extends Product with Serializeable

final case class UserNotFound(username: String) extends LoginError

final case class PasswordIncorrect(username: String) extends LoginError

case object UnexpectedError extends LoginError

case class User(username: String, password: String)

type LoginResult = Either[LoginError, User]

def handleError(error: LoginError): Unit =
  error match {
    case UserNotFound(u) => println(s"User not found: $u")
    case PasswordIncorrect(u) => println(s"Password Incorrect: $u")
    case UnexpectedError => println(s"Unexpected error")
  }

Aside: Error Handling and MonadError

CatsはMonadErrorと呼ばれるエラーハンドリングのために使われるEitherのような抽象化したデータ型を提供する。

package cats

trait MonadError[F[_], E] extends ApplicativeError[F, E] with Monad[F] {

   def raiseError[A](e: E): F[A]

  def handleErrorWith[A](fa: F[A])(f: E => F[A]): F[A]

  def handleError[A](fa: F[A])(f: E => A): F[A]

  def ensure[A](fa: F[A])(f: A => Boolean): F[A]
}
  • F[_] はMonad型
  • EはFに含まれるエラー型

MonadErrorの重要なメソッドは以下の2つ

  • raiseError
  • handleError

raiseErrorはMonadでいうpureメソッドに近い。

handleErrorWithraiseErrorを補足する。

The Eval Monad

cats.Eval は、評価の様々なモデルを抽象化するためのMonadである。

典型的な2つはeagerlazyでそれぞれcall-by-valuecall-by-nameとも呼ばれる。

Evalはまた結果をメモすることができ、call-by-needで評価もできる。

ja.wikipedia.org

また、Evalはスタックセーフなのでとても深い再帰処理でもスタックを開放することなく使用できる

評価時の挙動

call-by-value evaluation

val x = {
  println("Computing X")
  math.random
}
// Computing X
// val x: Double = 0.0056134621561709785

x // first access
// val res0: Double = 0.0056134621561709785 // first access

x // second access
// val res1: Double = 0.0056134621561709785
  • 定義した時点(eager)で計算が評価される
  • 計算は一度だけ評価されメモ化される

call-by-name evaluation

def y = {
  println("Computation Y")
  math.random
}
// def y: Double

y // first access
// Computation Y
// val res0: Double = 0.6768399768604834

y // second access
// Computation Y
// val res1: Double = 0.1400800525943975
  • 計算は使用した時点(lazy)で評価される
  • 計算は毎回使用されるたびに評価される(メモ化されない)

call-by-need evaluation

lazy val z = {
  println("Computation Z")
  math.random
 }
// lazy val z: Double

z // first access
// Computation Z
// val res0: Double = 0.5971338589578261

z // second access
val res5: Double = 0.5971338589578261
  • 定義時点では評価されず(not eager)、使用した時点で評価される(lazy)
  • 一度評価されると結果はキャッシュされる(memoized)

Eval's Models of Evaluation

Evalには3つのサブタイプがあり、上記で記載した評価時の挙動と一致している。

  • Now(call-by-value)
  • Always(call-by-name)
  • Later(call-by-need)
Scala Cats Properties
val Now eager, memoized
def Always lazy, not memoized
lazy Later lazy, memoized

The Writer Monad

cats.data.Writerは計算と一緒にログを運ぶことができるMonadである。

Writer[W, A]は2つの型を運ぶことができる。

Wはログの型でAが結果の型を表す。

The Reader Monad

cats.data.Readerは入力に依存する操作を連続させることができるMonadである。

Readerのインスタンスは引数一つの関数をラップし、それらを合成するための便利なメソッドを提供する。

import cats.data.Reader

final case class Cat(name: String, favoriteFood: String)

val catName: Reader[Cat, String] = Reader(cat => cat.name)

catName.run(Cat("Steve", "tuna"))
// res1: cats.ppackage.Id[String] = "Steve"

The State Monad

cats.data.Stateは計算の一部として追加のステートを渡すことができる。

アトミックな状態操作を表すStateを定義し、mapやflatMapを使ってそれらを繋ぎ合わせる。

State[S, A]のインスタンスは S => (S, A)の型を持つ関数を表す。

SがStateの型で、Aが結果の型。

import cats.data.State

val a = State[Int, String] { state =>
  (state, s"The state is $state")
}

val (state, result) = a.run(10).value
// state: Int = 10
// result: String = "The state is 10"

val justTheState = a.runS(10).value
// justTheState: Int = 10

val justTheResult = a.runA(10).value
// justTheResult: String = "The state is 10"

Stateの特徴は2つ

  • inputの状態をoutputの状態に変化させる
  • 結果を計算する

Stateは3つのメソッドを提供する

  • run
  • runS
  • runA

Define Custom Monads

以下の3つのメソッドを実装すればカスタムタイプのMonadを定義できる。

  • flatMap
  • pure
  • tailRecM

tailRecMメソッドはCatsで使われる最適化で、flatMapをネストして呼び出したことで消費されるスタックのスペースを制限するために使われる。

Monadに関する参考情報

MonadTransformer とは何か · GitHub

合成できるモナド、モナドが合成できる時 - Milestones to EVERPEACE 〜alius via〜

Functors

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

Functors

Functorとはざっくり言うと、map メソッドを持ったものと言える。

通常、Listsの反復処理でmapメソッドに出会うが、Functorを理解するには別の視点で考える必要がある。

Listsを走査する」と考えるのではなく、「すべての値を一度に変換する」と考えるべきである。

ある関数を適用するとすべての値に適用され値は変化するがListsの構造は保ったままである。

List(1, 2, 3).map(n => n + 1)
// List(2, 3, 4)

Future

Futureは非同期計算をキューイングしてシーケンス化し、前の計算が完了した時点で適用するFunctorである。

Futureは内部の状態については保証せず、Futureでラップされた計算は、ongoing(進行中), complete(完了), rejected(拒否)で表される。

もし、Futureが完了した場合は、マッピングされたカンスは即座に呼ばれる。もしそうでない場合は、下位のスレッドプールが関数の呼び出しをキューに入れて後で呼び出される。

Futureにおいて、「関数がいつ呼び出されるかは分からない」が、「関数がどの順序で呼び出される」かは分かる。

Note: Futureはreferentially transparent(参照透過性)でない 。

Futureは常に計算結果をキャッシュするため、これらの動作を調整することはできない。

Definition of a Functor

Functorの型は F[A]で、map操作は (A => B) => F[B]

Functor Laws

Functorは、多くの小さな操作を一つずつ順番に行う場合でも、それらを組み合わせて大きな関数にしてからマッピングする場合でも同じセマンティクスを保証する。

Identity: identity関数でmapを呼び出しても、何もしていない場合と同じである

fa.map(a => a) == fa

Composition: fgという2つの関数をマッピングすることはfという関数をマッピングしてからgという関数をマッピングするのと同じである

fa.map(g(f(_))) == fa.map(f).map(g)

Functors in Cats

Functorの型クラスは cats.Functorである。

Functorはliftというメソッドを提供していて、これは A => B という型の関数をFunctor上で操作し、F[A] => F[B]という型に変換する関数。

val func = (x: Int) => x + 1
val liftedFunc = Functor[Option].lift(func)
liftedFunc(Option(1))
// Some(2)

Exercise: Branching out with Functors

2分木型のFunctorを書け、という問題。

以下は、2分木の定義。

sealed trait Tree[+A]
final case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]
final case class Leaf[A](value: A) extends Tree[A]

Functorの実装はすぐ書けた。

  
  implicit def binaryTreeFunctor: Functor[Tree] = new Functor[Tree] {
    override def map[A, B](fa: Tree[A])(f: A => B): Tree[B] =
      fa match {
        case Branch(left, right) => Branch(map(left)(f), map(right)(f))
        case Leaf(value) => Leaf(f(value))
      }
  }

これを実行してみようと以下のように書いてみると、Cannot resolve symbol map だと怒られた。

  println(Branch(Leaf(1), Branch(Leaf(2), Leaf(3))).map(_ * 2))

Cats.Functorの定義を見てみると以下のようになっており、Invariant[F]を継承している。

trait Functor[F[_]] extends Invariant[F] { self =>
  def map[A, B](fa: F[A])(f: A => B): F[B]
  ...
}

ということは、変位指定は不変なので、Tree型を継承したBranch型やLeaf型は渡せない。

なので、Treeオブジェクトを作成して、Branch型とLeaf型のコンストラクターを作成する。

object Tree {
  def branch[A](left: Tree[A], right: Tree[A]): Tree[A] =
    Branch(left, right)

  def leaf[A](value: A): Tree[A] = Leaf(value)
}

これらのコンストラクターの返り値は Tree[A] なので、怒られずに実行できるはず。

println(Tree.branch(Tree.leaf(1), Tree.branch(Tree.leaf(2), Tree.leaf(3))).map(_ * 2))
// Branch(Leaf(2),Branch(Leaf(4),Leaf(6)))

実行できた。

Contravariant Functors and the contramap Method

Contravariant Functorcontramapというメソッドを提供する。

このcontramapメソッドは変換を表すデータ型にのみ意味がある。

F[B] contramap (A => B) => F[A]

Invariant functors and the imap method

Invariant Functorimapメソッドを実装しており、これはざっくり言うとmapcontramapの組み合わせと同じ。

F[A] imap (A => B, B => A) => F[B]