Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

Scala 3ではGeneral Type ProjectionのかわりにMatch Typesを使いましょう / Type Lambdasも使える

Scala 2ではこういう表現がたまにあった:

trait Entity { type Key }
type KeyOfEntity[E <: Entity] = E#Key

ねぇよそんなもん、と思った方はここで終わりです。せっかくなので聞いていってください。

KeyOfEntityEntityを受け取って、その内部のKey型を取り出す。Scalaでは、A#Xと書くと型Aの中の型Xを表現できるのだ。ちょうどa.xと書くとオブジェクトaのプロパティxを表現するのと同じ感じだ。DDDとかやってるとこういう型操作がたまに出現する。Scalaはこういうふうに型をそのまま値みたいに扱えるのでおもろいですよね。

このように抽象的な型を取り出す操作をGeneral Type Projectionと言うようだ。

しかしScala 3からはGeneral Type Projectionは廃止されているので、このままではコンパイルエラーが出て動作しない。

Match Types / Auxパターン

Scala 3からはMatch TypesとAuxパターンを使ってこのような操作を表現するようになった。順に見ていこう。

Auxパターン

以下のコードを見てほしい。

object Entity:
  type Aux[K] = Entity { type Key = K }

Auxは、新たに型パラメータKを導入し、内部でKey = Kという束縛を行っている。こうすることで、Keyは型パラメータKに持ち上げられて取り出せるようになる。このように、型パラメータを導入して内部で束縛することで、内部の型を型パラメータとして取り出すというパターンはAuxパターンとよく呼ばれる。具体的な使い方としては、コンパニオンオブジェクトにAuxという型定義を用意することが多い。どこで使われるかというと例えばジェネリック操作ライブラリShapelessの内部で使われていたりする。

Match Types

Match Typesとは型レベルでmatch式が使えるというScala 3の大変便利な機能である。

type Even
type Odd
type EvenOrNot[B] = B match
  case true => Even
  case false => Odd

// この型はOddになるはず
type X = EvenOrNot[false]

// summonを使って型が合致するかを確認する。=:=は左右の型が等しいときにのみsummonが通る
summon[X =:= Odd] // コンパイルが通る
summon[X =:= Even] // コンパイルが通らない

こうして見るとパラメータ付きの型、Java風に言うとジェネリクスは、型レベルの関数でしかないのだな、という気付きを得られる。

合体

で、Match TypesをどうAuxと組み合わせるかというと、次のようにする:

type EntityKey[T <: Entity] = T match
  case Entity.Aux[k] => k

こうすると、EntityKey[_]Entityを入れたとき、Auxパターンで中身のKeyが取り出され、それが返される、という型計算ができる。

trait Foo extends Entity { type Key = Int }
type KeyOfFoo = EntityKey[Foo] // => Int

よかったですね。型が取り出せないときはAuxとmatch、覚えておくと一生に一度くらいは役立つかもしれません。

経緯

Scala2で書かれたコードを手直ししていて、以下のようなコンパイルエラーが出るようになり困って調べていたところ次のリンクを見付けたのでメモしたという次第。

Foobar is not a legal path
since it is not a concrete type

stackoverflow.com

追記(2024-09-10)

ところで、Scala 3にはType Lambdasという機能が生えていて、その場で型引数を取るような型を定義して使うことができる。ようするに無名関数の型版だ。

type F[A] = (A, A)
type F = [A] =>> (A, A)

例えば上掲の2つの定義は等価だ。これを値レベルで見ると、functionを使って関数を定義するか、const ... = (x) => ...の形で関数を定義するか、という、TypeScriptとかでおなじみの形になる。

この機能を利用すると、Auxは以下のように書くこともできる:

object Entity:
  type Aux = [K] =>> Entity { type Key = K }

このAuxの定義はKEntityだけに依存しているから、EntityKeyの定義の中にそのまま持ち込める:

type EntityKey[T <: Entity] = T match
  case [k] =>> Entity { type Key = k } => k

しかしこのままではうまく型引数の導入ができないようで、いったん括弧で括ってやる必要があるみたいだ:

type EntityKey[T <: Entity] = T match
  case ([k] =>> Entity { type Key = k })[k] => k

うーん、ちょっと見た目が微妙になってしまったけれど、定義を削りたいときのためにこういう使い道があることは覚えておくと便利だ。

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?