プログラマブル深海魚

目立たないけど華やかに

ScalaのZIOを使ってみる

目次

ZIOとは

ZIOは、Scalaでエフェクト(副作用)を扱い、非同期や並列のプログラミングをするためのライブラリ。

ZIO[R, E, A]という型を中心に利用して、エフェクトを記述する。

  • A: エフェクトの成功時に返す値の型
  • E: エフェクトの失敗時に投げる可能性のあるエラーの型
  • R: エフェクトが依存する環境(サービス等)の型

例えば、以下のようなZIOを返す組み込みのサービスがある。

  • Console.readLine(prompt: String): ZIO[Any, IOException, String] → 標準入力から文字列を読み取る。成功時は入力された文字列(String)を返し、IOExceptionを投げる可能性がある
  • Console.printLine(line: => Any): ZIO[Any, IOException, Unit] → 標準出力に文字列を書き出す。IOExceptionを投げる可能性がある
  • Random.nextInt: ZIO[Any, Nothing, Int] → Int型の乱数を発生させて返す。エラーを投げることはない(EがNothing)

ZIOを利用する準備

build.sbtにZIOのライブラリの依存関係を追加する。

libraryDependencies += "dev.zio" %% "zio" % "2.1.21"

Hello, World!!

ZIOでアプリケーションを作成する場合、ZIOAppDefaultを継承してrunメソッドをオーバーライドすれば良い。
runメソッドがエントリーポイントになる。

import zio._

object MyApp extends ZIOAppDefault {
  def run: ZIO[Any, IOException, Unit] = Console.printLine("Hello, World!!")
}

変換(Transforming)と連鎖(Chaining)

ZIOをmapで変換したり、flatMapで連鎖させたりすることができる。以下のプログラムは、2を出力する。

def run: ZIO[Any, IOException, Unit] = ZIO.succeed(1)
  .map(_ * 2)
  .flatMap(Console.printLine(_))

特に、flatMapでZIOを連鎖させる場合、for内包表記(for-yield)を利用して記述するのがよい。

def run: ZIO[Any, IOException, Unit] = for {
  input <- Console.readLine("Please input something: ")
  _ <- Console.printLine(s"Input value: ${input}")
} yield ()

結合(Combining)

ZIOをzip(または<*>演算子)結合して並列で実行し、結果を組として取得することもできる。この場合、左右どちらかがエラーになると全体がエラーになる。

def run: ZIO[Any, IOException, Unit] = for {
  random <- Random.nextInt <*> Random.nextInt
  (randomLeft, randomRight) = random
  _ <- Console.printLine(s"Left-value: ${randomLeft}, Right-value: ${randomRight}")
} yield ()

結合するエフェクトの片方がUnitを返す等、値が不要である場合はzipLeft(または<*演算子)またはzipRight(または*>演算子)を使って、どちらかの値のみを取得することもできる。

def run: ZIO[Any, IOException, Unit] = for {
  input <- Console.printLine("Please input something: ") *> Console.readLine
  _ <- Console.printLine(s"Input value: ${input}")
} yield ()

参考文献

Getting Started with ZIO | ZIO
https://zio.dev/overview/getting-started

Introduction to ZIO | Baeldung on Scala
https://www.baeldung.com/scala/zio-intro

ZIO小史 - k.dev
https://kdotdev.com/kdotdev/zio-history

retrieve a tuple directly in a for comprehension with ZIO? · Issue #2761 · zio/zio
https://github.com/zio/zio/issues/2761

GraphQL APIについて

目次

GraphQLの概要

GraphQLはAPI用のクエリ言語であり、Facebookによって2012年に開発された後2015年にオープンソース化され、2018年にはGraphQL Foundationが設立されてGraphQLはGraphQL Foundationにより管理されるようになりました。
GraphQLでは、APIでクエリ可能な型やフィールドをスキーマとして定義し、またAPIを利用するクライアントは取得するフィールドを指定することができます。そのため、複雑で階層的なデータ構造を持つAPIの設計に適しています。

リクエスト操作の種類

GraphQL APIにリクエストする際の操作は、以下の3種類があります。

  • クエリ: データ取得操作
  • ミューテーション: データ更新操作
  • サブスクリプション: リアルタイム更新通知

RESTful APIとの比較

  • GraphQL APIでは、エンドポイントが1つだけになります。
  • GraphQL APIは、必要なデータのみを指定して取得することができるため、その点において効率的です。
  • RESTful APIではデータ構造を変更する場合はバージョンを変更したりエンドポイントを追加したりする必要がありますが、GraphQL APIではフィールド追加等で対応できる場合が多くなります。そのため、バージョン管理の必要性が少なくなります。
  • GraphQL APIはRESTful APIに比べて、一般的に実装が複雑になります。
  • GraphQL APIでは、キャッシュの利用が複雑になります。

スキーマの定義

GraphQLのスキーマは、スキーマ定義言語によって定義されます。

例えば、以下のように型を定義します。

type User {
  id: Int!
  name: String!
  age: Int!
  address: String
}

また、クエリやミューテーションを定義できます。

type Query {
  user(id: Int!): User
}

type Mutation {
  createUser(user: User): User
}

クエリ・ミューテーションの利用

クエリでは、取得するフィールドを指定してリクエストすることができます。

例えば、以下のリクエストを送信すると

query {
  user(id: 1) {
    id
    name
  }
}

以下のように指定したフィールドだけがレスポンスされます。

{
  "id": 1,
  "name": "Mano Sakuragi"
}

ミューテーションでは、データを登録・更新することができます。ここでも、戻り値のフィールドを指定することができます。

例えば、以下のリクエストを送信すると

mutation {
  createUser(user: {
    name: "Hiori Kazano",
    age: 15
  }) {
    id
    name
  }
}

以下のようにレスポンスが返ってきます。

{
  "id": 2,
  "name": "Hiori Kazano"
}

ScalaでGraphQLを実装する場合に利用できるライブラリ

以下のようなライブラリを利用できそうです。

  • Sangria
  • Caliban

参考文献

Introduction to GraphQL | GraphQL
https://graphql.org/learn/

初心者向けガイド:GraphQLの基本情報
https://apidog.com/jp/blog/basis-of-graphql/

GraphQL とは?をわかりやすく解説
https://www.redhat.com/ja/topics/api/what-is-graphql

GraphQL vs REST: What's the Difference? | IBM
https://www.ibm.com/think/topics/graphql-vs-rest-api

GraphQLスキーマの解説|NestJS でGraphQL開発を試す
https://zenn.dev/keisuke333/books/ce5e3208a9d393/viewer/f5bd3f

[Scala]Sangriaを使ってGraphQL APIを実装する #sangria - Qiita
https://qiita.com/petitviolet/items/e3e87c3f3e740b3c57ba

Scala, Akka HTTP, CalibanでGraphQL APIを実装する
https://zenn.dev/kowaremonoid/articles/4faaec0356877a

AWSで2つのVPC間をピアリング接続する

AWSで、2つのVPC間をピアリング接続して通信できるようにしてみました。

目次

作業の概要

VPC間で通信できるようにするため、以下のような流れで作業をします。

  1. VPC間のピアリング接続を作成する
  2. VPC(サブネット)のルートテーブルに相手側へのルートを追加する
  3. セキュリティグループに接続を許可するルールを追加する

事前準備

VPCを作成する

ピアリング接続を試すために、VPCを2つ作成します。
今回はピアリング接続を試すだけなので、プライベートサブネット1つ・AZ1つで作成します。

VPCを2つ作成した

VPCにEC2インスタンスを作成する

通信するWebサーバ(EC2インスタンス)を各VPC上に作成する。

EC2インスタンスを作成した

VPCのピアリング接続を作成する

VPCのメニューの[仮想プライベートクラウド]>[ピアリング接続]から、「ピアリング接続を作成」をクリックしてピアリング接続を作成します。

リクエスタ、アクセプタに事前準備で作成したVPCを設定し、「ピアリング接続を作成」をクリックします。

ピアリング接続の設定

ピアリング接続を作成すると「承諾の保留中」となるため、[アクション]>[リクエストの承諾]をクリックして、ポップアップの[リクエストを承諾]をクリックして承諾します。

以上で、2つのVPC間にピアリング接続が作成されました。

ルートテーブルを編集する

ピアリング接続を作成しただけではVPC間の通信がルーティングされないため、ルートテーブルを編集します。
VPCのサブネットが関連付けられているルートテーブルに、もう一方のVPCへのルートを追加します。

まず、VPC1のサブネットに関連付けられているルートテーブルを編集します。

VPC1のサブネットを表示して関連付けられているルートテーブルを確認する

ルートテーブルにチェックを入れて詳細を表示して、[ルート]>[ルートを編集]からルートを追加します。

ルートテーブルを表示する

送信先がVPC2のCIDRブロック、ターゲットが作成済みのピアリング接続のルートを追加します。[変更を保存]をクリックして、ルートテーブルを更新します。

VPC1にルートを追加する

VPC2のサブネットに関連付けられているルートテーブルも、同様に編集します。

VPC2にルートを追加する

以上で、2つのVPC間のルートがルートテーブルに追加されました。

セキュリティグループを編集する

ルーティングまでは実施されるようになりましたが、このままだとファイアウォール(セキュリティグループ)で通信が拒否されてしまうため、セキュリティグループを編集します。
今回は、VPC1のEC2→VPC2のEC2 のpingが通るように設定してみます。

VPC2にあるEC2に設定されているセキュリティグループを編集します。

VPC2にあるEC2を表示して設定されているセキュリティグループを確認する

[インバウンドルール]>[インバウンドのルールを編集]からインバウンドルールを追加します。

セキュリティグループを表示する

VPC1からの「すべてのICMP - IPv4」を許可するルールを追加します。[ルールを保存]をクリックして、セキュリティグループを更新します。

インバウンドルールを追加する

以上で、VPC1のEC2→VPC2のEC2 のpingが許可されるようになりました。

動作確認

VPC1のEC2に接続して、実際にVPC2のEC2にpingをしてみます。
EC2 Instance Connectで接続しました。

pingを実行する

pingが通りました!

参考文献

VPC ピア機能とは - Amazon Virtual Private Cloud
https://docs.aws.amazon.com/ja_jp/vpc/latest/peering/what-is-vpc-peering.html

AWSVPCVPCピアリング接続で相互に通信できるようにする
https://zenn.dev/rescuenow/articles/8e21a751c828c6

【神アプデ】え!? プライベートなEC2にもAWSマネコンから簡単SSHできるように?? #ハンズオン - Qiita
https://qiita.com/minorun365/items/696ba8bf683a403550d0

アバター改変でやってしまったアニメータ関連のミス事例

VRC向けのアバター改変をしている際に、アニメータ関連でミスしてしまった事例を備忘録としてまとめます。

目次

事例

アニメーションレイヤーのWeightが0になっている

初歩的なミスにして、結構やりがち(だと思う)なミス。Weightが0になっているので、当然アニメーションが適用されない。
これをやりがちなのには理由があって、アニメーションレイヤーを新規作成したときのWeightのデフォルト値が0だからです。(デフォルトは1でもいい気がするんだけどね……)

新規作成したレイヤーはWeightが0となっている

VRC Expression Parametersにアニメーションパラメータを追加していない

VRC特有の問題です。アバターを他人から見たときに、アニメーションが同期されない問題。
アニメーションパラメータはVRC Expression Parametersに設定して"Synced"にチェックを入れると他人にパラメータ値が同期されますが、設定していなかったためにパラメータが同期されず、他人視点ではアニメーションが発動しませんでした。
もともと私は少し勘違いをしていて、VRC Expression ParametersはVRC Expression Menuでパラメータを変更できるようにするための設定だと思っていたのですが、それが理解として正しくありませんでした。

アニメーションパラメータを新規追加した (New Bool)

VRC Expression Parametersにもパラメータを追加しないと他人に同期されない

なお、ContactReceiver等に"LocalOnly"というパラメータがありますが、こちらのチェックを外してもパラメータが同期されるわけではありません。詳細は割愛します。

Interruption Sourceの設定が適切でない

この問題は、おそらく私が使用しているアバターの「びしょぬれのしずくさん」の表情アニメータコントローラがやや特殊な作りであるため発生したものだと思います。(他のアバターについてはあまり知識がないので、もし一般的な作りだったらすみません)
しずくさんの表情アニメータコントローラは、以下のように表情が切り替わるときにデフォルト表情を経由するようになっています。本来、ステートのTransision設定でInterruption SourceがNext Stateになっているため、途中のステート(デフォルト表情)がスキップされて変更先の表情に直接変わるような動きになっています。それが、Interruption SourceがNoneになっていたことで、表情の切替時に一瞬だけデフォルト表情が表示されるようになってしまっていました。

しずくさんの表情アニメータコントローラを簡略化して、Face 1からFace 2の切替を図示したもの

おわり

以上、アニメータ関連のミス実例3つでした。ただの備忘録ですが、誰かのお役に立てば幸いです。

参考文献

知識0だけどExメニューでアクセとか武器とかをオンオフしたい!!(初心者向けExpression解説1)[VRChat]|Ran_kotonoha
https://note.com/ran_kotonoha/n/nd1fc7f1d84ea#9fe187ed-1547-45ba-b9d3-f3b695817597

【VRChat】Contactsで物を出し入れする【Avatar Dynamics】|風庭ゆい
https://note.com/yui0471/n/n54beffc6df97

VRChat上のアバター間データ同期の概念|おにく/Oniku@VRC
https://note.com/onikuvrc/n/n4fc95cf23dab

【初心者Unity】アニメーションの遷移を検証③【遷移の割り込み】 | TECH PROjin
https://tech.pjin.jp/blog/2021/08/31/unity-transition_3/

ScalaTestのウォークスルー

ScalaTestでどのようにテストを記述するのかを学習したため、メモ的に記事に内容を残します。

目次

テストの実行(sbt)

testタスク

プロジェクトに含まれるすべてのテストを実施するには、sbtのtestタスクを実行します。

testOnlyタスク

特定のテストクラスのみテストを実施するには、testOnlyタスクを実行します。
"testOnly {クラス名}"の形式で実行すると、指定されたクラスのテストのみが実行されます。また、クラス名にはワイルドカードも使用できます。

テストの記法

ScalaTestでは、いくつかの記法でテストを記述することができます。
記法はテストの宣言の見た目にのみ影響を与え、テストの内容には影響を与えません。そのため、その場で最適な記法を選ぶことができます。

自分で記法を選択するのが面倒な場合、ScalaTestの公式DocではFlatSpecが推奨されています。

以下では一部の記法を紹介し、それ以降ではWordSpecスタイルを採用します。

FunSuiteスタイル

FunSuiteスタイルは非常にシンプルな記法で、xUnit(例えばJUnit)に近い記法です。

import org.scalatest.funsuite.AnyFunSuite

class FunSuiteTest extends AnyFunSuite {
  test("A negative number should be less than 0") {
    assert(-1 < 0)
  }
}

FlatSpecスタイル

FlatSpecスタイルはFunSuiteに近い記法ですが、"X should Y"や"X must Y"といった形式で記述します。
FunSuiteスタイルを、やや自然言語のように記述できるようにしたといった感じでしょうか。

import org.scalatest.flatspec.AnyFlatSpec

class FlatSpecTest extends AnyFlatSpec {
  "A negative number" should "be less than 0" in {
    assert(-1 < 0)
  }
}

FunSpecスタイル

FunSuiteスタイルを入れ子構造にできるようにしたような記法です。
RubyRSpecと似た記法のようで、describeとitによりテストを入れ子で記述します。

import org.scalatest.funspec.AnyFunSpec

class FunSpecTest extends AnyFunSpec {
  describe("A number") {
    describe("when negative") {
      it("should be less than 0") {
        assert(-1 < 0)
      }
    }
  }
}

WordSpecスタイル

こちらはFlatSpecスタイルを入れ子構造にしたような記法です。
Scalaの別のテストフレームワークであるspecs/specs2と似た記法です。

import org.scalatest.wordspec.AnyWordSpec

class WordSpecTest extends AnyWordSpec {
  "A number" when {
    "negative" should {
      "be less than 0" in {
        assert(-1 < 0)
      }
    }
  }
}

アサーション(Assertions)

アサーションを使用してテストを記述することができます。
以下で、一部のアサーションを紹介します。

assert

assertは記述した条件がtrueの場合に成功し、falseの場合に失敗します。

"A Set" when {
  "empty" should {
    "have size 0" in {
      assert(Set.empty.size == 0)
    }
  }
}

assertResult

あるコードの結果が特定の値になることを確認したい場合、assertResultを利用できます。

"A Set" when {
  "singleton" should {
    "have size 1" in {
      assertResult(1) {
        Set("test").size
      }
    }
  }
}

assertThrows

あるコードが特定の例外をスローすることを確認したい場合、assertThrowsを利用できます。

"A Set" when {
  "empty" should {
    "produce NoSuchElementException when head is invoked" in {
      assertThrows[NoSuchElementException] {
        Set.empty.head
      }
    }
  }
}

intercept

assertThrowsと同様に例外をスローすることを確認できますが、テストを中断せず例外の内容をさらにテストできます。

"A Set" when {
  "empty" should {
    "produce NoSuchElementException including 'empty' in message when head is invoked" in {
      val caught = intercept[NoSuchElementException] {
        Set.empty.head
      }
      assert(caught.getMessage.contains("empty"))
    }
  }
}

succeed/fail

テストを強制的に成功/失敗させます。

"A test" should {
  "succeed" in {
    succeed
  }

  "fail" in {
    fail("Always fails.")
  }
}

マッチャー(Matchers)

ScalaTestには、アサーションを"should"や"must"といった単語を使用して記述するためのDSLドメイン固有言語)が定義されています。 これをマッチャー(Matchers)と言います。
これらは、should.Matchersまたはmust.Matchersをミックスインすることで使用できます。

以下では、一部のマッチャーを紹介します(Shouldマッチャーを使用します)

等価性

equal, not equalを用いると、等価性をチェックできます。
また、be, not beも用いることができますが、こちらは等価性のカスタマイズができません(デフォルトの等価性チェック)

"A Set" when {
  "empty" should {
    "have size 0" in {
      Set.empty.size should equal (0)
    }
    "have not size 1" in {
      Set.empty.size should not equal (1)
    }
  }
}

より大きい・小さい

<, > , <=, >= を利用すると、より大きい・小さいや以上・以下の比較ができます。

"A number" when {
  "negative number" should {
    "less than 0" in {
      -1 should be < 0
    }
  }
  "positive number" should {
    "greater than 0" in {
      1 should be > 0
    }
  }
  "0" should {
    "0 or less" in {
      0 should be <= 0
    }
  }
}

ある数から一定の範囲内

ある数nから一定の範囲dの中に数値が入っているかを、"+-"を利用してチェックできます。小数も可能。

"A number" when {
  "5" should {
    "in range 6 +- 2" in {
      5 should equal (6 +- 2)
    }
  }
}

サイズ・長さ

have size, have lengthを用いると、サイズや長さのチェックができます。

"A Set" when {
  "empty" should {
    "have size 0" in {
      Set.empty[Number] should have size 0
    }
  }
}
"'ABC'" should {
  "have length 3" in {
    "ABC" should have length 3
  }
}

Booleanプロパティ

shouldBeのあとにプロパティ名のシンボルを記述すると、Booleanプロパティの真偽値をチェックできます。

"A Set" when {
  "empty" should {
    "be empty" in {
      Set.empty[Number] shouldBe Symbol("isEmpty")
    }
  }
}

参考文献

ScalaTest User Guide
https://www.scalatest.org/user_guide

ScalaTest入門 - Qiita
https://qiita.com/verdoyant/items/e8d81e80268b714fdfbc

Scalaユニットテスト入門 - seratch's weblog in Japanese
https://seratch.hatenablog.jp/entry/20110807/1312726957

【STEP4】テスト : Scalaでコードを書く - Qiita
https://qiita.com/yukinagae/items/038f75e9a1bf17978886

Scalatest: 特定のテストケースだけ実行したい - Qiita
https://qiita.com/suin/items/0294a53d6babd69f29a9