Scala関西Summit2017
Non-Functional Programming in Scala
資料
- Scala導入において、関数型は大きな壁になっている。
- しかし、Scalaは基本的には手続き的な言語であり、関数型の要素をうまくmixしているに過ぎない。
- チームの状況、スキルに応じて関数型を取り入れていくのが良い。
- 色々な書き方ができるのがScalaの利点である。
メモ
竹添 直樹 さん (@takezoen)
ビズリーチ
- Scalaがもっと普及して欲しい → どうすればいい?
- Scalaの本を書く、翻訳する、SIで業務システム開発の事例を作った...etc.
- 関数型プログラミングという巨大な壁
- 関数型プログラミング in Scala
- 基本的には副作用を使った手続き型プログラミング言語
- 関数型言語由来の様々な昨日をうまくmixしている
- さまざまな関数型ライブラリ
- どこまで関数型に寄せるか? という選択肢を迫られる
- プログラミングだけでなく、ライブラリの選定なども
- 業務でのチーム開発で起きたこと
- 最初はみんな初心者
- 関数型プログラミングが取り入れられてくる
- Scala力が上がるにつれてあとから入ってくる人のハードルが高くなる
- 振り返り
- Scalaはプログラマの成長に合わせてスタイルを変えられる
- が、人が入れ替わるようなプロジェクトだとつらいよね
- OSS開発では…
- 手続き型バリバリ、var、while…
- スキルが低い? → Scalaを使っているモチベーションの違い
- 関数型プログラミングのモチベーションが低い状態でどうScalaを使うか
- Scalaは副作用のある手続き型プログラミング言語であり
- モナドを意識せずに使うことができる
- 使うべきか?使わないべきか?
- var,while ○
- mutableコレクション ○
- メソッド内のりようであれば許容する
- 戻り値を返す際にはimmutableなコレクションに変換する
- なるべくimmutableなコレクションを使うように啓蒙していく
- return ○
- 戻り値の型を明記しないといけなくなる
- 場合によっては、例外としてコンパイルされる
- 実際に問題になることは少ない
- そもそも例外をThrowableでキャッチしない
- 例外 ○
- エラーを戻り値で返すか、例外で返すか?
- Either … 例外の発生は防げないので変換が必要になる → 複雑になる
- 非同期処理は別
- null ×
- 使う理由がまるでない
- Option使おう
- Javaのライブラリは別
- Option.get △
- opt.getOrElse("") などとする人が出現
- getしてエラーになる方がまし
- mapやforeachなどを使うように啓蒙していく
- for式 △
- モナモナするときに使う
- 無理して使わない
- コレクション操作やFututeのネストをシンプルに書きたい時に
- あくまでmapとflatMapのシンタックスシュガー
- 型クラス ○
- フレームワークやライブラリで使っているケースが多い
- 使うだけであれば意識しなくてよい
- 自分で実装しないと行けないケースはあるが、そこまで躓かない印象
- ただし「型クラス」と言わないほうがいい
- あくまでも判断の一例
- 今後のプロジェクトの展開に沿って決める
- Scalaを使う人がもっと増えて欲しい
- 最初はみんな初心者であることを忘れない
- Scalaで楽しくプログラミングをしてほしい
- 関数型プログラミングに興味のある人だけでなく、それ以外の人たちにもScalaを使って欲しい
- Akka in Action の日本語版が出るよ!
- 今年中には出るよ
Scala and Akka apps on Kubernates in ChatWork
資料
- Kubernatesでインフラの負担がへり、アプリエンジニアもインフラを管理していけるように。
- メトリクスを収集・視覚化することで、運用管理やチューニングが楽になった。
- Akkaのメトリクス収集はKamonがよい
- Scalaにおいて、GCの回収は特に目を向けること
メモ
林 大介 さん (@hayasshi_)
チャットワーク
- Falcon
- ChatWorkのバックエンド
- Scala
- ChatWorkでのKubernaetesの利用について
- Kuberetesとは
- コンテナのオーケストレーションツール
- クラスタ化、コンテナの運用管理
- アプリはPodという単位で管理される
- いくつかのアプリと
- Deploymentという仕組みで同一のPodを冗長化できる
- なぜ導入した?
- インフラ面での課題
- 開発環境やデプロイにおけるインフラチームのコストが高い
- 責務が多く、スピーディに対応できなかった
- DevOpsの推進
- デプロイイメージ
- コンテナイメージの作成は sbt-native-packager
- ConcourseCIで一連のタスクをパイプライン化
- アプリ開発者はデプロイの定義を作成しパイプラインに設定するだけ
- 導入のメリット
- アプリ開発者が自分たちでアプリの運用管理を行えるようになった
- スケールアップ、スケールダウンが楽になった
- ChatWorkでのアプリケーションのメトリクス
- メトリクスをとる必要性
- 異常検知
- 障害時の原因調査
- アプリケーションのチューニング
- Kamon
- Datadog
- PagerDuty
- アプリケーションの外から見て意味のあるメトリクス
- 処理時間
- 単位時間あたりのスループット
- エラーレート
- しきい値を設けて、それを超えた時にアラート
- Kamon
- アプリケーション内部のメトリクス(Scala)
- 基本的なものが中心
- CPU使用率
- ヒープ使用料
- GCタイム・GCカウント
- 特にScalaは短命オブジェクトが多くなりやすいのでGCの回収は特に確認
- モニタリングも大切だが、従来のJVM管理ツールでの確認も必要
- Akka
- Akkaは基本的に並列処理
- 並行処理をささえる仕組みであるDispatcherまわりのメトリクスをおさえる
- スレッドプール
- メールボックス
- Kamonが収集してくれる
- まとめ
- Kubernetesを導入したことで
- DevOpsの文化が少しづつできてきた
- アプリケーションの運用管理が楽になった
- メトリクスは
- 目的によって収集するメトリクスを見極める
- 運用管理やアプリチューニングが楽になった
実践ScalaでDDD
資料
- DDDは、仕様書の言葉をそのままコードに落としこむようなこと目指した設計手法
- ScalaとDDDの相性はかなり良い。
- case class で大量のクラスを作成
- ローカル関数とfor式で処理のフローを明確化 など
- DDDに正解はない。自分たちでスタイルを作り上げる。
メモ
辻 陽平 さん (@crossroad0201)
- DDDとは
- オブジェクト指向で変更な容易なソフトウェアを開発するための体系化された原則集
- 顧客も開発者も、「ユビキタス言語」で会話
- 業務の関心事で「コンテキスト」を分割する
- ドメインの知識をドメインの言葉でコードに落としこむ
- お客さんの行っていることをそのままコードに書いているかのようなものを目指す
- 参考書
- エリックエヴァンスのドメイン駆動設計
- 実践ドメイン駆動設計
- DDDの登場人物
- 集約
- 整合性を保証しなければならないまとまり
- エンティティ
- 集約の中で一意な識別子で識別される概念
- バリューオブジェクト
- CQRS
- 更新と検索をの実装を分けよう
- ScalaとDDD
- ScalaとDDDは相性が良い!
- ドメインの仕様を自然に表現できる
val user = userRepository.get(userId) ifNotExists NotFoundError
- オブジェクト指向と関数型
- ドメインの静的側面はオブジェクト指向で
- 動的な側面は関数型で
- case classで簡単に型が作れる
- 変数と関数の区別がない
- 関数をネストできる
- その関数を使う場所(スコープ)が明確
- 処理ステップに細かく名前をつけやすい
- Scala実装スタイル
- イミュータブルに作る
- 値を書き換えるのではなく別インスタンスを返す
- case classだとcopy()メソッドで簡単
- 副作用を起こさない(局所化する)
- 戻り地として帰らないものはすべて副作用
- ソフトウェアは何らかの永続化処理を伴うので、完全になくすことはできないが局所化できる
- ドメインロジックから副作用を分離する
- Userを生成→保存
- 同時に行うと副作用になる
- 別の処理に切り出す
- for内包表記を使いこなす
- ローカル関数を定義して処理ステップに名前をつける
- for内包表記でステップをつなげて処理フローを作る
- implicit parameterを活用する
- ドメインの関心事でないパラメタ(DBセッションなど)は、implicit parameterで暗黙的に渡す
- ドメインロジックからノイズを排除できる
- case classのcopy()は外から使わない
- コードの意図がわからなくなる
def rename()
などにする
- 変数名はむやみに略さない
- Option型の変数はmaybeをつける
- 型推論で型を省略することが多いのでOptionlaどうかが変数名からぱっと見て分かると便利
- 自動コードフォーマッタを利用
- Scalafmt, Scalareform
- for内包表記とmatch式は揃えるとグッと読みやすくなる
- アーキテクチャ
- レイヤ構成
- ドメインレイヤ
- アプリケーションレイヤ
- インフラストラクチャレイヤ
- インターフェースレイヤ
- レイヤ化アーキテクチャ
- ドメイン中心アーキテクチャ
- オニオンアーキテクチャ
- エラー処理
- Scalaでは例外をスローしない
- Option
- あったりなかったりする値を表現する
- Either → 予測できるエラー
- 異常時に任意のエラー値を返すことができる
- Stringのエラーメッセージを返すなどせず、独自にエラーオブジェクトをつくる
- スタックトレースを持たせておくと便利
- Try → 予測不可能なエラー
- 例外をラップして戻り地として返せるようにする
- 各レイヤで発生するエラーの特性
- ドメインレイヤ
- アプリケーションレイヤ
- インフラストラクチャレイヤ
- インターフェースレイヤ
- エラーの変換
- 利用者にはユーザフレンドリーなエラーを返したいが、ドメインやインフラストラクチャでユーザーインターフェースを意識すべきでない
- エラーを変換するヘルパ関数を使っておくと便利
- コンポーネントの実装
- アプリケーションサービスのコードが仕様書になることを目指す
- DDDの実践
- さらにその先へ
- DDDに正解はない
- 実践して、学び、改良して自分たちのスタイルを作っていく
ストリーム処理ことはじめ Akka Streams
- StreamAPIを使うことで、自然と処理が別れたコードになり、メンテしやすい、テストしやすいコードができが上がる
- Akka Streamsを使うことで、無限長といった大きなデータも効率よく処理ができる
- Akka Streamsは通常のコレクション操作とほぼ変わりなく扱うことができる
メモ
にしかわささき さん (@nishikawasasaki)
- Java Stream API のおさらい
- データの変更・絞り込み・処理がひとつのforで実行されている
- →StreamAPIはそれぞれ分けられている。見通しが良い。
- 仕様変更があった際に、forの中に処理を詰め込んでいくのはしんどい
- 無名関数を関数に切り出す → テストしやすい
- 待ちを減らして効率よく処理を終わらせるには?
- メモリに載り切らないほど大きなデータを扱うには?
- Akka Streams
- 1ブロックずつ処理が進む → データが揃うまで待ちが発生している
- 処理が重たい時は? ブロックに乗り切らない時は?
- ブロック単位ではなくデータ単位で進む
- ブロック内で並列処理
- Akka-Streamsを使う
- 通常の操作とほとんど変わりなく使える
- Akka-Streamsを導入することで処理の並列処理が可能となる
- Akka-Streamsのasync(とsの他)を使うと処理間の非同期も可能に
- 効率的
- まとめ
- StreamAPIを使うと自然と処理の内容と流れがわかれたコードになる
- 処理の内容と流れがわかれたコードは仕様変更や追加に強い
- 「普段やっていないことは複雑な問題に適用できない」
- 並列処理、非同期処理を助けてくれる
- 処理の流れが複雑になっても用意された多くの機能や表現力が助けてくれる
グラフを知って理解するAkka Streams
@kamijin_fanta
さくらインターネット
- AkkaStreamsを使う上での基礎知識
- ストリーム処理
- バックプレッシャー
- 型安全
- グラフ
- 用語
- Element
- Producer
- Consumer
- Back-pressure
- Graph
- Source []-
- Flow -[]-
- Sink -[]
val runnableGraph = source via flow to sink
runnableGraph.run()
- よく使われるSource/Sinkは提供されている
- Default
- alpakka
- Akka Stream Kafka
- Reactie Streams
- まとめ
- バックプレッシャーによるフロー制御
- 小さな部品を組み合わせてグラフを作る
- Source/Flow/Sink の部品を1から作るのも難しくない
プレースホルダ構文完全解説
資料
- 言語仕様、BNFを用いた解説で正直かなり難しかった…
- プレースホルダ構文を使うところ
- レシーバの位置に置く
- 中置式のオペランドの位置に置く
- メソッド呼び出しの引数の位置に置く
- ネストした式の中には使わない!
メモ
水島 宏太 (@kmizu) さん
- プレースホルダ構文 (Placeholder Syntax for Anonymous Functions)
以下のコードの転回形を答えよ
list.map(_.size)
list.map{(x) => x.size}
list.map(_.size + 1)
list.map{x => x.size + 1}
list.map((_.size) + 1)
list.map{(x => x.size) + 1}
- コンパイルエラー
list.map(_ + 1 + 2)
list.map{x => x + 1 + 2}
list.map(((_).+(1)).+(2))
list.map{((x) => (x + 1)) + 2}
- コンパイルエラー
- 構文的に同型なのに結果が異なる!
def add(x: Int, y: Int) = x + y
list.map(add(1, add(2, _)))
list.map(add(1, x => add(2, x)))
コンパイルエラー
- なぜこのような展開形になるのか
- プレースホルダのコアとなる概念
- 構文カテゴリ
- 還元
勘違いされやすい
obj.method _
obj.method(_)
- 前者は型チェック時に解釈(Eta Expansion)
- 後者は構文解析時に解釈
プレースホルダ構文の難しさ
list.map(_ + 1 + 2)
list.map((_.+(1)).+(2))
SLS 6.32.1
構文kてゴリExprの式内に、識別子が有効な箇所に_(アンダースコア)を埋め込むことができる。 そのような式は無名関数を表現する。 2回以上出現した場合は、複数の引数を表現する。
Expr ::= (Bindings | id | `_") `=>" Expr
| Expr1
構文カテゴリにおける「還元」 構文カテゴリExpr1の式をExprに還元可能
アンダースコア節
- 2つの条件が満たされた時、式eはアンダースコア節uを束縛する
- e は u を真に含む
- Exprであるような他の式xが存在しない
-
プレースホルダ構文、どう使うべきか
- レシーバの位置に置く
- 中置式のオペランドの位置に置く
- メソッド呼び出しの引数の位置に置く
- ネストした式の中には使わない!
Property Based Testing でドメインロジックをテストする
資料
- Property Based Testing で変更に強いテストを書くことができる。
- テストは、「普遍的な性質」より「特定条件の時に満たす性質」を書く。
- ドメインの制約は型で表現するとよい。
メモ
中村 学 (@gakuzzzz) さん
- テスト書いてますか?
- 型 > テスト
- 「テストで示せるのはバグの存在であって、バグの不在は証明できない」
- そうはいっても、値に関するテストは必要
case class Message(id: Long, read: ReadStatus)
Message(1, ReadStatus.Unread)
Message(2, ReadStatus.Unread)
Message(3, ReadStatus.Read)
Message(4, ReadStatus.Read)
↓
case class Message(id: Long, read: ReadStatus, category: Category)
レコードが増えてテストが落ちる!
全てのテストでテストデータを共通化してるからだめ
テーブルにカラムが追加 → 全てのテストを書き直し…
テスト毎に専用のデータを用意できればおk
↓
そこで Property Based Testing
- テストデータをランダムに半自動生成して、その生成された全ての値について、みたすべき性質を満たしているかテストする
- scalaprops, ScalaCheck
テストデータを半自動生成とは
``` case class User(name: String, age: Int)
val userGen: Gen[User] = for { name <- Gen.alphaNumStr age <- Gen.choose(1, 150) } yield User(name, age) ```
データの生成方法だけを定義すればいいので、仕様変更や改修でフィールドが増減してもその生成方法だけ変更すればOK
ドメインロジックで「普遍的な性質」を探そうとすると難しい 「特定条件の時に満たす性質」であれば見つけやすい
特定条件を表し方
- 性質を定義する際にfilterする
- whenever
- 条件を満たした時だけassertionする
- 専用のGenを用意する
- 特定条件のインスタンスのみ生成させる
- 複数のテストで繰り返し使われる条件の時便利
- しかし、あまりに多くなるとテスト毎にデータを用意しているのと変わりない(本末転倒)
- 生成された値に手を付ける
- 手軽
- テスト失敗時に出力される値と実際にロジックに渡した値が異なる
- (大抵は手を加えた値以外が原因のため問題にならない)
- 複雑なオブジェクトの生成は煩雑になる…
- → Lensを使う (Monocle, Shapeless, Scalaz ...)
その他
- 制約を型で表現するとGenの定義が楽
case class Group(members: List[User])
case class Group(leader: User, member: List[User])
- 暗黙的に期待している制約に注意
- 自動生成だと値が重複する! (同じIDを持つオブジェクトが複数含まれる…etc.)
まとめ
- テストデータの管理は大変…
- Property Based Testing でデータを半自動生成
- 「特定条件時に満たす性質」は見つけやすい
- 制約を型で表現すると捗る
- 暗黙的に期待している制約に注意
- ドメインロジックのテストにもProperty Based Testingは使える!