1 of 34

Rustユーザーから見た

Scalaの型クラス

GitHub:yubrot

LT at #scalabase_scala 2022/03/04

2 of 34

yubrot(ゆーぶろ)

https://yubrot.github.io/profile/

  • プログラミング言語がわりと好き
    • 2021年はRustで自作言語処理系のセルフホスティングとか
  • 新しい言語を学ぶ時は簡単なLisp処理系を実装することが多い
    • 2022年にScala 3移行したのがこのスライドの発端
  • 仕事は最近はバックエンドエンジニア (RustもScalaも書いてない)

「Rust/Haskellユーザーから見た」としたかったが、GitHub見返したら年単位でHaskell書いてなかった...のとHaskellも含めると完全にLTに収まらない

3 of 34

導入: 型クラス in Scala 3

以下、糖衣構文については省略します

4 of 34

型クラス in Scala 3: 定義

(例: 文字列から変換できる、という特性を表現した型クラス: FromString)

Scalaにおいて、型クラスは単にtraitで定義される

5 of 34

型クラス in Scala 3: インスタンスの実装

(例: 整数は文字列から変換できる: IntFromString)

  • 型クラスのインスタンスはtraitの実装として与える

6 of 34

型クラス in Scala 3: インスタンスの実装

(例: 整数は文字列から変換できる: IntFromString)

  • 型クラスのインスタンスはtraitの実装として与える
  • givenによってそれを標準的(canonical)な定義とする

7 of 34

型クラス in Scala 3: 型クラスを満たすことを要求

(例: 「Aが文字列から変換できる」ことを要求する関数: fromReverseString)

型クラスを満たすことを要求するときは、using句を用いてコンテキスト引数を要求する形で表現する

8 of 34

型クラス in Scala 3: 呼び出し

コンテキスト引数が要求される関数を呼び出すコード

→ Scalaコンパイラはterm inferenceによってコンテキスト引数にgivenによる標準的なインスタンスを暗黙に与える

9 of 34

型クラス in Scala 3: 呼び出し

コンテキスト引数が要求される関数を呼び出すコード

→ Scalaコンパイラはterm inferenceによってコンテキスト引数にgivenによる標準的なインスタンスを暗黙に与える

※Scala 2の頃はこの暗黙(implicit)という機構にフォーカスした言語デザインだった

10 of 34

導入: 型クラス in Rust

右下に比較用にScala 3のコード

11 of 34

型クラス in Rust: 定義

型クラスは専用の trait 構文を用いて定義する

※Rustのtraitは必ず暗黙にSelfという型パラメタを含む�(Scalaの例の型パラメタA相当、FromString[A]を実装するオブジェクトとは無関係)

12 of 34

型クラス in Rust: インスタンスの実装

型クラスのインスタンスは専用の構文 impl .. for .. で与える

※i32は整数型�※impl .. for ..自体がgiven相当

13 of 34

型クラス in Rust: 型クラスを満たすことを要求

型クラスを満たすことを要求するときは、where句で制約を明示する

14 of 34

型クラス in Rust: 呼び出し

※Scalaと異なり、明示的に型クラスのインスタンスを与える構文は存在しない (理由は後述)

15 of 34

二つの言語の「型クラス」

構文は色々異なるが (特にRustはSelfを優遇した構文となっている)、�共に「型クラス」として以下のようなことを実現している:

  • 型 (の組) が満たす特性を型クラスとして定義でき
  • ある型 (の組) について、その特性を満たす実装...型クラスのインスタンスを、型を拡張せずに与えることができ
  • これを用いるとアドホック多相を実現できる

16 of 34

二つの言語の「型クラス」の差異

ある言語機能の呼称やその実態は、言語によって様々

  • 名称や予約語が違う (interface vs. trait vs. protocol …)
  • できることが違う (デフォルト実装を持てる, mixinできる, …)

RustとScalaの型クラスは何が異なるのか

17 of 34

二つの言語の「型クラス」の差異

ある言語機能の呼称やその実態は、言語によって様々

  • 名称や予約語が違う (interface vs. trait vs. protocol …)
  • できることが違う (デフォルト実装を持てる, mixinできる, …)

RustとScalaの型クラスは何が異なるのか�→ 色々異なる (それはそう)�  色々異なるが、注目すべき言語デザインに根差した違いがある

18 of 34

違い(1): インスタンスのユニーク性

19 of 34

違い (1): インスタンスのユニーク性

Scalaのgivenインスタンスはスコープにローカルなもの

  • あるgivenインスタンス定義のスコープの外で必要ならimportする
  • 利用側でコンテキスト引数が解決できない場合のエラーはnot found

20 of 34

違い (1): インスタンスのユニーク性

Scalaのgivenインスタンスはスコープにローカルなもの

  • あるgivenインスタンス定義のスコープの外で必要ならimportする
  • 利用側でコンテキスト引数が解決できない場合のエラーはnot found

Rustのtrait implはグローバルにユニークなもの

  • ある型 (の組) に対するtrait implはプログラム全体で常に高々一つ
    • これを満たすためにtrait implができる場所に関する制約がある
  • trait implが見つからない場合のエラーはnot implemented
    • 実装があるが今のスコープで見つからないということが起きない

21 of 34

Rustはインスタンスがユニークである →

ある型 (の組) が特性を満たすとき、それがグローバルに一貫しているという保証�marker trait

FromStringの実装はAから一意

i32: FromStringの実装は高々一つ

ユニークであることを前提とした構文

22 of 34

Scalaはときにインスタンスを選べる

「整数」の「順序付け」の方法は複数ある

23 of 34

Scalaはときにインスタンスを選べる

常に明示的に渡す

24 of 34

Scalaはときにインスタンスを選べる

常に明示的に渡す

必要に応じて明示的に渡す

25 of 34

Scalaはときにインスタンスを選べる

常に明示的に渡す

必要に応じて明示的に渡す

Rustの場合は振る舞いを切り替えるには新しい型が必要

26 of 34

違い (2): ランタイムの戦略

27 of 34

違い (2): ランタイムの戦略

Scalaの型クラスのインスタンスはオブジェクト

  • インスタンスは、単に引数としてランタイムに渡る
  • Scalaは「オブジェクト指向言語」

28 of 34

違い (2): ランタイムの戦略

Scalaの型クラスのインスタンスはオブジェクト

  • インスタンスは、単に引数としてランタイムに渡る
  • Scalaは「オブジェクト指向言語」

Rustの型クラスのインスタンスを示す値や型は基本的に無い

  • 静的型付けで、型 (の組) からインスタンスが一意に定まる
  • メソッドの呼び出しは静的に解決される

29 of 34

違い (2): ランタイムの戦略

型クラスのインスタンスのメソッド呼び出しはデフォルトで動的ディスパッチの形であり、そのオーバーヘッドについては処理系の最適化に期待する

30 of 34

違い (2): ランタイムの戦略

型クラスのインスタンスのメソッド呼び出しはデフォルトで動的ディスパッチの形であり、そのオーバーヘッドについては処理系の最適化に期待する

Aに対する型クラスのインスタンスのメソッドはコンパイル時に一意に定まり、そのメソッドの呼び出しに静的に展開される

Rustはランタイムのコストがかかり得る場所が明示的であることを好む:�動的ディスパッチを伴う言語機能としてTrait Objectが提供されている

31 of 34

まとめ

ともに「型クラス」と呼ばれる言語機能でも、言語の目的・指向の違いからデザインや実装戦略に差異があり面白い

共に型システムが強力とされる言語だが、それぞれの特性を理解してコードに反映していきたい / 使い分けていきたい

32 of 34

以下おまけ

33 of 34

おまけ: Scalaへの直訳が少し難しい例

34 of 34

おまけ: Scalaへの直訳が少し難しい例

この辺うまい書き方があれば知りたい