女子高生になりたい

はるみちゃんのブログだよ。えへへ。

モナドトランスフォーマーを使ってモナドを合成する

ネストしたモナドの取り扱い

モナドがネストして帰ってくることがあります。
例えば、DBに接続してユーザレコードを取り出す時、DBに接続できたかどうかを表すEither、該当のユーザがいたかどうかを表すOptionのモナドがネストして帰ってくるケースを考えてみます。

val fetchResult: Either[Error, Option[User]] = //DBからUserをFetch

このfetchResultから、ユーザ情報にアクセスするためには、以下のようなコードを書く事になると思います。

for {
  maybeUser <- fetchResult
} yield  {
  for {
    user <- maybeUser
  } yield user
}
fetchResult.map{maybeUser =>
  maybeUser.flatMap{ user =>
    ???
  }
}

どっちもネストして嫌なコードになってしまいました。

モナドAとモナドBを合成できれば、1つのflatMapだけで操作ができるようになるのですが、モナドは通常合成できません。
というのも、flatMapの実装ができないからです。

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] = ???
  }
}

ただし、M2モナドが確定すれば、合成することができます。
例えば、M2がOptionだとしたら、以下のように合成できます。

type Composed[A] = M[Option[A]]

def flatMap[A, B](fa: Composed[A])(f: A => Composed[B]): Composed[B] = 
    Monad[M].flatMap(fa) {
      case None => Option.empty[B].pure[M]
      case Some(a) => f(a)
    }

と、いうわけで、内部のモナドを確定できればモナドの合成ができるらしいです。

catsでは、EitherやOptionなどを内部のモナドとして合成できる便利なトランスフォーマーが定義されています。

モナドトランスフォーマーを使ってモナドを合成、操作する

catsでは、OptionTやEitherT等の「T」をつけた型でもなどトランスフォーマーが提供されています。

これを使ってListモナドとOptionモナドを合成する例を見てみます。

val listOption: OptionT[List, A] = OptionT(List(Option(10)))
val listOption2: OptionT[List, A] = OptionT(List(Option(10)))
val listOption3 = for {
    x <- listOption
    y <- listOption2
} yield x + y

簡単に合成したモナドを取り扱う事ができるようになりました。

次に、EitherとOptionの合成を見てみます。先の例で出てきた、Either[Error, Option[User]]です。

内部のモナドはOptionなので、OptionTを使えばよいのですが、Eitherの型が確定しません。

val fetchResult: OptionT[Either, User] = //Eitherは2つ型パラメータを取るのに1つしか渡せない

そこで、タイプエイリアスを使用してEItherの片方の型を確定させます。

type ErrorOr[A] = Either[Error, A]
val fetchResult: OptionT[ErrorOr, User]

このようにモナドを合成することができます。 また、3つ以上のモナドを合成する時も同様、タイプエイリアスを使って型パラメータを補完しながら合成していきます。

//Future[Either[String, Option[Int]]]の合成
type FutureEither[A] = EitherT[Future, String, A]
type FutureEitherOption[A] = OptionT[FutureEither, A]
10.pure[FutureEitherOption] 

また、モナドトランスフォーマはvalueメソッドを持っており、これを利用して合成されたモナドをアンパックすることができます。

valueメソッドを呼び出す度、内部から順番に1つずつアンパックされていきます。

val first: FutureEitherOption[Int] = 10.pure[FutureEitherOption]
val second: FutureEither[Option[Int]] = first.value
val third: Future[Either[String, Option[Int]]] = second.value

モナドトランスフォーマーの利用は局所的な関数内でとどめておき、値をかえす時はvalueによりアンパックした状態にしておくのが良いかもしれません。

gormでRelationを組む方法とn+1の回避

昨日から色々あってGo langで開発をはじめました。
超にわかですが、気づいたことやハマったことなどあれば備忘録を残しておこうと思います。

今回は、gormを使ってBelongToの関係にあるFishモデルとWaterAreeモデルのリレーションを組んでみます。

github.com

また、Fishは1匹ではなく、複数匹Selectする必要があります。

(1)Relationメソッドを使う方法

type Fish struct {
    ID             uint       `json:"id" gorm:"column:id"`
    Name           string     `json:"name" gorm:"column:name"`
    WaterAreaID    uint      `json:"-" gorm:"column:waterarea_id"`
    WaterArea      WaterArea `json:"water_area"`
}

type WaterArea struct {
    ID   uint   `json:"id" gorm:"primary_key"`
    Name string `json:"name" gorm:"column:name"`
}
var fishes []models.Fish
db.Find(&fishes)
for i := range fishes {
    db.Model(fishes[i]).Related(&fishes[i].WaterArea, "WaterArea")
}

多分ドキュメント読んでたらこの方法でリレーションを組むのが先に思いつくんじゃないでしょうか?(知らんけど)

2567レコード分のFIshを返すのにかかったレスポンスタイムは582.688351msでした。ふええ・・・遅すぎです・・><
(DBはlocalhostに立ってますので、外部ネットワーク通信によるレイテンシはありません)

これ、典型的なn+1問題です。
Go書いてると、Mapなんかもforでまわして結合したりするので、違和感なく上記のようなコードを書いてしまいがちな気がします?

あと、今回の問題とは関係ないですがRelatedの第二引数に”WaterArea”を渡さないと(invalid association )で怒られました。

ex) (invalid association )で怒られないために

WaterAreaID→WaterAreaIdにしたら大丈夫です。
ただ、Lintの警告が出でて味が悪いです。

type Fish struct {
    ID             uint       `json:"id" gorm:"column:id"`
    Name           string     `json:"name" gorm:"column:name"`
    WaterAreaId    uint      `json:"-" gorm:"column:waterarea_id"`
    WaterArea      WaterArea `json:"water_area"`
}

type WaterArea struct {
    ID   uint   `json:"id" gorm:"primary_key"`
    Name string `json:"name" gorm:"column:name"`
}

参考 gormでbelongs to にハマった話 · polidog lab++

(2)Preloadメソッドを使う方法

モデルの定義は先と同じです。

forで走査するのではなく、Preloadメソッドを使うようにしてみます。

db.Preload("WaterArea").Find(&fishes)

2567レコード分のFIshを返すのにかかったレスポンスタイムは30.843664msでした。 発行されるクエリが2クエリになり、かなりレスポンスが改善されました。

その他詳しい使い方は公式ドキュメントをご参照ください。

CRUD: Reading and Writing Data · GORM Guide

CatsのStateモナドを使ってFP in ScalaのEx6.11 自動販売機問題を解く (2)解いてみた

Functional Programming in ScalaのExercise6.11の問題をStateモナドを使用して解いてみます。

問題設定

スナックの自動販売機をモデリングする有限状態オートマトンを実装する。

この自動販売機は2種類の入力を受け付ける。
1つは硬貨の投入、もう1つはハンドルを回してスナックを取り出すこと。

自動販売機の状態はロックされたか、ロック解除された状態かのどちらかになる。
ロックが解除された状態でハンドルをまわすとスナックが出てくる。

・ロックされた状態の自動販売機に硬貨を入れると、スナックが残っている場合ロックが解除される
・ロックが解除された状態の自動販売機のハンドルをまわすと、スナックが出てきてロックがかかる
・ロックされた状態でハンドルをまわしたり、ロックが解除された状態で硬貨を投入しても何も起こらない
・スナックが売り切れたとき、自動販売機は入力をすべて無視する

最終的には、入力のリストを渡し、自動販売機の最終的な状態をかえすsimulateMachine関数を実装する。
例えば、自動販売機に10枚のコインと5つのスナックが入っており、4つスナックが購入されたときの自動販売機の最終状態は(14, 1)になる。

解答例

sealed trait Input
case object Coin extends Input
case object Turn extends Input

case class Machine(locked: Boolean, candies: Int, coins: Int)

def inputCoin = State[Machine, (Int, Int)] {
  case machine if machine.candies == 0 => (machine, (machine.coins, machine.candies))
  case machine if machine.locked => {
    (machine.copy(locked = false, coins = machine.coins + 1), (machine.coins + 1, machine.candies))
  }
  case machine => (machine, (machine.coins, machine.candies))
}

def turnHandle = State[Machine, (Int, Int)] {
  case machine if machine.candies == 0 => (machine, (machine.coins, machine.candies))
  case machine if !machine.locked =>  {
    (machine.copy(locked = true, candies = machine.candies - 1), (machine.coins, machine.candies - 1))
  }
  case machine => (machine, (machine.coins, machine.candies))
}

def simulateMachine(inputs: List[Input]): State[Machine, (Int, Int)] = {
  def action(input: Input) = input match {
    case Coin => inputCoin
    case Turn => turnHandle
  }
  inputs.traverse(action).map(_.last)
}

挙動の確認

val initMachine = Machine(
  locked = true,
  candies = 3,
  coins = 0
)

//コインは0->2枚になり、スナックは2個減る
simulateMachine(List(Coin, Turn, Coin, Turn)).run(initMachine).value //(Machine(true,1,2),(2,1))

//コインは1枚目のみ入り、後は無視される
simulateMachine(List(Coin, Coin, Coin)).run(initMachine).value //(Machine(false,3,1),(1,3))

//何もおきない
simulateMachine(List(Turn, Turn, Turn)).run(initMachine).value //(Machine(true,3,0),(0,3))

//最後のCoin->Turnの段階で、スナックが0個なので以降の入力は無視される
simulateMachine(List(Coin, Turn, Coin, Turn, Coin, Turn, Coin, Turn)).run(initMachine).value //(Machine(true,0,3),(3,0))

スナックが0のときと、ロックされた状態でハンドルをまわしたり、ロックが解除された状態で硬貨を投入したときのコードがinputCoinとturnHandleで冗長になってしまっている、愚直な実装です。

コイン投入の時の挙動と、ハンドルをまわした時の挙動を独立して考える方が、はじめて実装する段階ではわかりやすいと思います。

また、traverseでstateモナドを合成するとvalueが蓄積されてしまうので、うまくスタックを積み上げないような実装が出来ればよかったです。

本家の解答はよりスマートなので、そちらも参照ください。 fpinscala/State.scala at master · fpinscala/fpinscala · GitHub

CatsのStateモナドを使ってFP in ScalaのEx6.11 自動販売機問題を解く (1)Stateモナドについて

Stateモナドについてざっくりと。

状態の変化を伴うミュータブルな計算をFunctional Programmingのコンテキストで表現可能にしたもの。

catsにおけるStateモナドのapplyメソッドのシグネチャは以下のようになっています

def apply[S, A](f: S => (S, A)): State[S, A]

見たとおり、状態Sを受け取って、(S, A)を返す関数を渡します。

これだけではさっぱりなので、使用例を考えていきます。。
例に示すのは乱数を生成する関数です。

乱数生成のためのオブジェクトは状態を持つため、以下は典型的な参照透過性のない関数です。

def createRandomInt = {
  val random = new Random()
  random.nextInt()
}

これを少し改良してみましょう。

def createRandomInt(random: Random) = random.nextInt()

これで同じRandomオブジェクトに対しては同じ出力結果が帰るようになったので、先程の例よりかはかなりテストしやすくなりました。

ただ、これも非常に辛い点が幾つかあります。
というのは、同じ乱数ジェネレータを再現するには、Randomオブジェクトを同じシードで生成した上に、同じ回数この関数の呼び出しをしないといけません。(副作用により状態を作り出す必要がある)

以前の状態はこの関数を呼び出すと完全に失われてしまいます。

これを、Stateモナドを使って参照透過性のある実装にしてみます。
状態を副作用として更新するのではなく、生成された値と共に新しい状態を返すようにします。

また、scala.util.randomはシングルトンオブジェクトになっており、nextIntを呼ぶと必ず副作用が生じるので、線形合同法により乱数を生成するようにします。

実装はFP in Scalaのリスト6-3を参考にしました。

type SEED = Long
def createRandomInt = State[SEED, Int] { state =>
  val nextState = (state * 0x5DEECE66DL + 0xBL) & 0xFFFFFFFFFFFFL
  val n = (nextState >> 16).toInt
  (nextState, n)
}

これは何度呼び出しても元のstateが変わる事がない、つまり副作用がない関数になっています。

createRandomInt.run(1).value._2 //384748
createRandomInt.run(1).value._2 //384748
createRandomInt.run(1).value._2 //384748

また、帰ってくる次のstateを使用して、新たな乱数を生成することができます。

val s = for {
  s1 <- createRandomInt
  s2 <- createRandomInt
  s3 <- createRandomInt
} yield (s1, s2, s3)
s.run(1).value //(245470556921330,(384748,-1151252339,-549383847))
val s = for {
  s1 <- createRandomInt
  s2 <- createRandomInt
  s3 <- createRandomInt
} yield s3
s.run(1).value //(245470556921330,-549383847)

やや解釈が難しいコードかもしれないですが、これでnexrStateを使用して関数を合成できています。 帰ってきたタプルの1要素が最後に帰ってきたstateで、2要素が生成された乱数です。

また、catsのStateモナドには便利メソッドとして、get,set,inspect,modify等があります。

get[S]はState[S, S]を生成します

val state = State.get[Int]
state.run(1).value //(10, 10)

set(s: S)はState[S, Unit]を生成します。 つまり、一度ためておいたvalueをすべて破棄して、状態だけ保持することができます。

val state = State.set(1)
state.run(10).value //(10, ())

inspectは次の状態から値を生成することができます。

val state1 = createRandomInt
state1.run(1).value #(25214903928,384748)

val state2 = createRandomInt.inspect(i => s"hello: $i")
state2.run(1).value #(25214903928,hello: 25214903928)

state2を見ると、nextStateであるSEED「25214903928」からvalueを生成していることがわかります。

modifyは状態を変化させることができます。

val state1 = createRandomInt
state1.run(1).value #(25214903928,384748)

val state2 = createRandomInt.modify(_ + 1)
state2.run(1).value #(25214903929,384748)

state2をみると、modifyを使用していないstate1に比べて、stateであるSEEDの数が + 1されていることがわかります。

ここまで、Stateモナドについてざっくり書きました。
ここから、本題である自動販売機のモデルをStateモナドを用いて実装していくのですが、、長くなったので次の記事にまわします・・。

UdacityのDeep Learning Nanodegree Programを修了しました

Nanodegreeとは

MOOCプラットフォームであるUdacityの提供する有料コースです。

基本的には無料の講座と同様、動画を見て学ぶ形になります。

無料コースと比較して以下のような特徴があります。

  1. 修了には指定された幾つかのプロジェクトを期限以内に提出しないといけない

  2. プロジェクトは丁寧にレビューしてもらえる

  3. メンターが付いてチャットで学習をサポートしてくれる

  4. 受講生と講師のみが入れるSlackチームに招待され、質問や雑談ができる

  5. 修了後、関連する他のNanodegreeProgramを審査なしで受講できる

  6. 修了後、キャリアセンター経由で就職の支援をしてもられる(自分は利用した事がないです)

  7. 修了後、同窓会のようなLinkedinのチームに招待してもらえる

  8. 修了後、希望と適性があればメンターになれる

  9. 修了後、修了証明書を貰える

無料と最も違うところはやはり、プロジェクトを完遂しないといけない、ということです。
しっかりとしたレビューを受けられ、要求水準を満たしてなければ容赦なく再提出をくらうので、確実に自分の力になります。

ただ、わからなくてもSlackで聞けたり、メンターに聞けたりするので、完全に詰んでしまうことはなさそうです。

Deep Learning Nanodegreeプログラムについて

このプログラムの開講期間は4ヶ月で、1年に4回受講生の募集があります。
自分は2018年の2月-6月のタームで受講しました。

料金は599$。日本円で6.5万円ほどです。

また、内容、料金は定期的にアップデートされるようです。
他の方の記事を見ていると、今年から?(いつからか詳細は不明)は「Deep Reinforcement Learning(強化学習)」の章が追加されたようです。

1章を除く残りの章で、プロジェクトの提出が求められます。

具体的な構成は以下です。

1. Intoroduction to DeepLearning

Deeplearningの体験やAnaconda、JupyterNotebook環境を整えたりなどするチュートリアル

2. Neural Netoworks

ニューラルネットワークの基礎。プロジェクトではバイクシェアリングサービスを題材に、日に何台バイクが借りられるかを自分でニューラルネットワークを実装して予測します。

3.Convolutional Networks

畳み込みニューラルネットワークの章。他にはオートエンコーダや転移学習などの重要な概念も取り扱います。
プロジェクトではCNNを組んで犬種を予測するのが目的です。

4. Reccurent Networks

リカレントニューラルネットワークの章。単純なRNNに加え、LSTMも取り扱います。また、Word2Vecもここで取り扱われています。
プロジェクトではRNNを組んでシンプソンズのTVスクリプトを自動生成する事が目的です。

5. Generative Adversarial Networks

GANの章。GANとDeep Convolutional GAN、半教師付き学習を取り扱います。
プロジェクトでは、CelebAのデータを元に、GANにより顔を自動生成するこ事が目的です。

6. Deep Reinforcement Learning

強化学習の章。様々な強化学習アルゴリズムを取り扱っています。Value Iteration、Policy Iteration、MC Prediction、GLIE Monte-Carlo、Sarsa、Deep Q-Learning、Actor Critic、、などなど。

この章だけやたら難しいです。

数式→実装の流れを繰り返す感じで、理解するには動画だけでは厳しかったです。
日本語の副読本として自分は「これからの強化学習( https://amzn.to/2sJy9cy )」を使用しました。

また、取り扱うアルゴリズムも非常に多く、ボリュームもあります。

取り扱う内容は素晴らしいので、今後この辺りの動画が整備されてよりわかりやすい講座になれば良いですね。。

プロジェクトでは、シミュレータ内のドローンを強化学習により目的のタスク、に合わせて飛行させるのが目的です。

修了してみて

DNNの世界を一通り体験&実装できるので、初学者にはとてもオススメです。

もちろん数式は出てきますがそんなに難しいものではないですし、グラフィカルな説明があるのでわかりやすいです。(強化学習の章を除く)

あと何より、6.5万円も払うと勉強しようという気になります><

ただ、修了したからといって、何かキャリアが大きく変わるようなものではないです。。 (キャリアセンターを使用したら何か変わるのかもしれないですが)

自分は相変わらずScalaばかり書いてて、仕事でDNNを扱うことはないです。。誰か雇って><><

今後のキャリアに活かすため、Kaggleを通してより力をつけていきたい所存です。。
登録してから3年経つんですが、先日やっとタイタニックを提出しました><

f:id:sakata_harumi:20180610024315p:plain

ExpectedSarsaでOpenAI GymのTaxi問題を解く

OpenAI Gym

gym.openai.com

強化学習アルゴリズムを開発して比較するためのツールキット。 シンプルなものからAtariのゲームのような複雑なものまで、様々なシチュエーションが用意されています。

今回は、そこから「Taxi-v2」環境を使って、強化学習によるAIを作成してみました。

(OpenAI Gymはpip経由で簡単に入れることができます。詳細は本家ドキュメントをご確認下さい)

Taxi-v2

概要

Taxi-V2の目的は、5x5のフィールドでタクシーを運転し、乗客を乗せ目的地で下ろす一連の流れをなるべく素早く行う事です。

タクシーは1マス進むたびに、-1の報酬を受け取りますが、乗客を目的地まで運ぶと+20の報酬を受け取る事ができます。
また、乗客のいない所で客を拾ったり、乗客がいないのに客を下ろす動作をしたりすると、-10の報酬を受け取ってしまいます。

f:id:sakata_harumi:20180608155205p:plain

黄色い長方形がタクシーになります。開始位置はランダムです。

青色の文字が乗客の位置で、赤色の文字が目的地を表しています。
乗客の位置と目的地の位置はR,G,B,Yのどこか、ランダムに決まります。

「:」は通れる通路、「|」は壁になっており、タクシーは通れません。

状態

観測できる状態はただ1つの整数値のみです。
この数値で「現在の位置」「乗客の位置」「目的地の位置」を表現しています。

具体的にはこちら gym/taxi.py at master · openai/gym · GitHub の実装をご覧下さい。

行動

取れる行動は「東/西/南/北に移動」「乗客を拾う」「乗客を下ろす」の6種類になります。
0~5の数値が割り当てられており、順に「"South", "North", "East", "West", "Pickup", "Dropoff"」になります。

ExpectedSarsa

学習は ExpectedSarsaという手法を使用して行いました。詳細は割愛します。

ExpectedSarsaでは、以下の更新式によりQ関数を更新します。

 Q(S_t, A_t) \leftarrow Q(S_t, A_t) + \alpha(R_{t+1} + \gamma \sum_a \pi(a|S_{t+1})Q(S_{t+1},a) - Q(S_t, A_t))

ちなみに、Q学習の更新式は以下でした。

 Q(S_t, A_t) \leftarrow Q(S_t, A_t) + \alpha(R_{t+1} + \gamma max_a Q(S_{t+1}, a) - Q(S_t, A_t))

γがかかっている部分を見ると、Q学習ではQ関数が最大となるようなactionを選択しますが、ExpectedSarsaは文字通り、期待値を取っている事がわかります。

実装

Agentの実装です。

行動選択はε-greedyを用い、活用と探索のバランスをとっています。

最も期待値の高いactionを1-ε + ε/nAで選択し、それ以外のactionをそれぞれ確率ε/nAで選択します。

εは学習を進めるごとに小さくしていき、活用の割合を増やしていきます。

class Agent:
    def __init__(self, nA=6):
        self.nA = nA
        self.Q = defaultdict(lambda: np.zeros(self.nA))
        self.sum_step = 1
        
    def epsilon_greedy_probs(self, state):
        epsilon = 1.0 / self.sum_step
        policy_s = np.ones(self.nA) * epsilon /self.nA
        policy_s[np.argmax(state)] = 1 - epsilon + (epsilon / self.nA)
        return policy_s
        
    def select_action(self, state):
        policy_s = self.epsilon_greedy_probs(self.Q[state])
        return np.random.choice(self.nA, p = policy_s)

    def step(self, state, action, reward, next_state, done):
        self.sum_step += 1
        alpha = 0.1
        gamma = 0.999
        policy_s = self.epsilon_greedy_probs(self.Q[state])
        self.Q[state][action] += self.Q[state][action] + (alpha * (reward + (gamma * np.dot(self.Q[next_state], policy_s)) - self.Q[state][action]))

次に、学習を走らせる部分の実装です。

特に言及するところはありませんが、後に報酬の推移をグラフ化するために、キューを使って報酬の平均値をサンプリングしています。

def interact(env, agent, num_episodes=20000):
    avg_rewards = deque(maxlen=num_episodes)
    sampling_rewards = deque(maxlen=100)

    for i_episode in range(1, num_episodes+1):
        state = env.reset()
        total_reward = 0
        while True:
            action = agent.select_action(state)
            next_state, reward, done, _ = env.step(action)
            agent.step(state, action, reward, next_state, done)
            total_reward += reward
            state = next_state

            if done: 
                sampling_rewards.append(total_reward)
                if (i_episode >= 100):
                    avg_reward = np.mean(sampling_rewards)
                    avg_rewards.append(avg_reward)
                break
    return avg_rewards

結果

2万エピソードまでの報酬の推移のグラフです。

f:id:sakata_harumi:20180608170245p:plain

2500エピソードあたりで収束したように見えますが、15000エピソードあたりで更に一気に得られる報酬が増えていることがわかります。
大体平均報酬9~10程度になったら最短ルートを辿っていると言えそうです。

次は、実際に動かしてみてランダム(未学習)のタクシーの挙動と、学習後タクシーの挙動を比べてみます。

未学習

学習済み

学習済みモデルの方はほぼ最短ルートで送迎できています。よかったー。

次はDQNでインベーダーとかブロック崩しとかさせたいんだけど、全然ちゃんと学習してくれなくて詰んでます。。
インベーダーはUFOに飛び付くし、ブロック崩しは左右端で待機しちゃう・・・。学習量の問題なんだろうか・・・。
いい感じになったら公開したいです。

SwiftのOptionをEither(Result)にするExtension

SwiftのOptionalをResultにするExtension書いたのでメモ。

Resultはこのライブラリを入れてます

github.com

実装

import Result

extension Optional {
    func toResult<E: Error>(error: E) -> Result<Wrapped, E> {
        guard let value = self else {
            return .failure(error)
        }
        return .success(value)
    }
}

テストコード

class OptionalExtensionTests: XCTestCase {
    func testToResult() {
        enum MockError: Error {
            case mock
        }
        let someString: String? = "hoge"
        let expectRight = someString.toResult(error: MockError.mock)
        XCTAssertEqual(expectRight, .success("hoge"))
        
        let noneString: String? = nil
        let expectLeft = noneString.toResult(error: MockError.mock)
        XCTAssertEqual(expectLeft, .failure(MockError.mock))
    }
}

SwiftのOptionは色々足りてない感あるけど、この方(↓)の書いたExtensionと↑を入れとけば大分使いやすくなる、、と思う。

qiita.com

Eitherがないの違和感しかないんだけど、いつか入るんだろうか