ShapelessとPhantomTypeで値のコンテキストを型で表現する

scala ネタ

## 背景
ある型 `A` に置いて特定の処理を通ったこと(値が加工されたことを)を型としてラベル付けしたいときってありますよね
コードにすると

```scala
case class UserList(
  underlying: List[User]
) {
  def filterByEnabled: UserList = UserList(
    underlying.filter(_.isEnabled)
  )
}

object UI {
  def view(values: UserList): Unit = ???
}

object UserRepository {
  def getLists(): UserList = ???
}

object Main {
  def run(): Unit = {
    val xs = UserRepository.getLists()

    val filtered = xs.filterByEnabled

    // ここには必ずFilter後の値を渡したい
    UI.view(filtered)
  }
}
```

このとき `UI.view` には必ずFilter処理を行った値を渡したいとします
そうすると間違って未処理の `xs` を渡す可能性を無くしたいですよね

## 目的
背景を踏まえた上で
`UI.view` には必ず `filterByEnabled` を行った `UserList` のみを渡すよう型を構成します
つまり、型チェックによって未処理の値を渡してしまうのを防ぎます

以下段階的に対応を書いていきますが
タイトルにも書いてある `shapeless` を使用しているのは [解決案3](#解決案3) のみです

## 解決案1
「処理後」を表す型を用意し、引数側でそれを受け取る
(パターンとして名前があるかは存じないですが割と一般的に使われていると思っています)

```scala
case class UserList(
  underlying: List[User]
) {
  def filterByEnabled: UserList = UserList(
    underlying.filter(_.isEnabled)
  )
}

case class EnabledUserList(
  underlying: List[User]
)

object UI {
  def view(values: EnabledUserList): Unit = ???
}
```

### pros
- 一般的なclass機能のみで完結している
- 型の名前でどういう処理が行われたのあとなのかが一眼でわかる

### cons
- 値がもつコンテキスト以外は同じ型を複数回定義することになる
- 型チェックしたい処理が増えれば増えるほどクラスが増える
- 複数の状態を定義したとき、掛け合わせ状態を表現するのが辛い

## 解決案2
PhantomType
いわゆる実行時には何も影響を与えない、型チェック時にのみ影響を与える型パラメタのことです

```scala
case class UserList[S <: UserList.State](
  underlying: List[User]
) {
  def filterByEnabled(implicit ev: S =:= UserList.Init): UserList[UserList.OnlyEnabled] = UserList(
    underlying.filter(_.isEnabled)
  )
}

object UserList {
  sealed trait State
  trait Init extends State
  trait OnlyEnabled extends State

  def init(values: List[User]): UserList[UserList.Init] = UserList(values)
}
object UI {
  def view(values: UserList[UserList.OnlyEnabled]): Unit = ???
}
```

### pros
- ひとつのクラスでまとまっている

## cons
- 複数の状態を定義したとき、掛け合わせ状態を表現するのが辛い

## 解決案3
PhantomType & Coproduct
`Coproduct` は直和の英訳、および直和を表現する[Shapelessのclass](https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/coproduct.scala)です
前2案で解決できなかった「複数の状態を定義したとき、掛け合わせ状態を表現するのが辛い」という短所を解決するために導入します

### 型の集合として扱うCoproduct
(本来の使い方ではありません、ただ用途に対して必要な要素が揃っているためCoproductを使います)
ShapelessにCoproduct型は `type X = A :+: B :+: CNil` のように構築できます
これは `X = A | B` という直和型を生成しています(CNilは実装の都合上付随するものなので無視します)
今回は型の集合として扱いたいのでこれを `X` は `A` と `B` の要素で構成された集合、とします
集合でありながら `A :+: B :+: A :+: CNil` など同じ型が複数回出現する集合もかけてしまいますが、用途の上
で支障はないので黙認することにします

この型に対する値は `A` or `B` のどちらか、なのですがPhantomTypeとして扱うので値は気にしなくもよくなります

### 集合 `X` に任意型 `A` が含まれていることを型チェックする
型が含まれているかを調べるには [Selector](https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/ops/coproduct.scala#L49) 型をimplicitパラメタとして取得することでチェックできます

```scala
import shapeless._
import shapeless.ops.coproduct._
import shapeless.syntax._

object SelectorTest {

  type X = Int :+: String :+: CNil

  def test(): Unit = {
     implicitly[Selector[X, Int]]
     implicitly[Selector[X, String]]
     // implicitly[Selector[X, Boolean]] => compile error!
  }
}
```

扱いやすくするために以下のtype aliasを定義しましょう

```scala
type ∈[A, P <: Coproduct] = Selector[P, A]
```

### 集合 `X` に任意型 `B` が含まれて "いない" ことを型チェックする
型が含まれていないことを調べるには [NotIn](https://github.com/milessabin/shapeless/blob/2e79921b1ac8d9246889e92bebfc64219b821023/core/src/main/scala/shapeless/ops/hlists.scala#L657) 型をimplicitパラメタとして取得することでチェックできます

```scala
object NotInTest {
  import shapeless.ops.hlist.ToCoproductTraversable.NotIn

  type X = Int :+: String :+: CNil

  def test(): Unit = {
    implicitly[NotIn[X, Boolean]]
    implicitly[NotIn[X, Double]]
    // implicitly[NotIn[X, String]] => compile error!
  }
}
```

扱いやすくするために以下のtype aliasを定義しましょう

```scala
type ∉[A, P <: Coproduct] = NotIn[P, A]
```

### 実装
`Coproduct` による型の集合表現については説明しましたのであとはこれを用いて当初の目的を実装していきましょう
解決案2 で提示したState PhantomTypeには処理によって付与されたStateの集合である `Coproduct` を使います
値を必要とする関数の引数では `Selector` や `NotIn` をimplicitパラメタとして指定することで値に必要なコンテキストを指定することができます
以下が実装コードとなります(複数の組み合わせによるState表現を示したいので当初よりメソッドの数を増やしています)

```scala
import shapeless._
import shapeless.ops.coproduct._
import shapeless.syntax._
import shapeless.ops.hlist.ToCoproductTraversable.NotIn

object Helper {
  type ∉[A, P <: Coproduct] = NotIn[P, A]
  type ∈[A, P <: Coproduct] = Selector[P, A]
}

import Helper._

case class User(
  /* user attributes */
) {
  def isEnabled: Boolean = true
  def isViewable: Boolean = true
}
case class UserList[StateList <: Coproduct](
  underlying: List[User]
) {
  import UserList.State._

  def filterByEnabled(implicit notIn: FilteredByEnabled ∉ StateList): UserList[FilteredByEnabled :+: StateList] = UserList(
    underlying.filter(_.isEnabled)
  )
  def filterByViewable(implicit notIn: FilteredByViewable ∉ StateList): UserList[FilteredByViewable :+: StateList] = UserList(
    underlying.filter(_.isViewable)
  )
}

object UserList {
  def init(values: List[User]): UserList[State.Init] = UserList(values)

  sealed trait State
  case object State {
    type Init = CNil
    trait FilteredByEnabled extends State
    trait FilteredByViewable extends State
  }
}

object UI {
  import UserList.State._

  def view[StateList <: Coproduct](xs: UserList[StateList])(
    implicit s1: FilteredByEnabled ∈ StateList,
             s2: FilteredByViewable ∈ StateList
  ): Unit = println(xs)
}

object UserRepository {
  def getLists(): UserList[UserList.State.Init] = UserList.init(Nil)
}

object Test {
  def run(): Unit = {
    val init = UserRepository.getLists()
    val filtered = init.filterByEnabled
    // filtered.filterByEnabled => compile error
    val viewable = filtered.filterByViewable
    // UI.view(filtered) => compile error
    UI.view(viewable)
  }
}
```

Stateの初期値としては `CNil` を利用します
`Ui.view` 関数はStateの集合に `FilteredByEnabled` と `FilteredByViewable` が存在していることを期待します
つまり、 `filterByEnabled` `filterByViewable` のふたつの関数をクリアしていない値を渡すとコンパイルエラーになります、掛け合わせ状態も表現できていますね
今回は書きませんでしたが `Selector` と `NotIn` 両方受け取ることで `A` を処理していて `B` を未処理、みたいな複雑なコンテキストで制限をかけることも可能です

### pros
- 掛け合わせの状態も対処できる

### cons
- 実装が複雑

---
実際productionコードとして使うならコンパイルエラー時のメッセージの改善やimplicit周りをもう少し完結に書きたい気持ちはありますね
何かのご参考までに