ravineport blog

日々の学びをつらつらと

Goでつまづいたところを仕様を見ながら理解してみる

Go言語デビューしました!🎉 シンプルなのもあってすぐに馴染むことができています。Goよいですね!

さてさて今回はGoを書いていて「これできるんだ」「これはコンパイルエラーなんだ」となったところをGoの仕様などを見ながら(できればそのwhyまで)理解したいと思います。 Go 1.18時点での記事です。

つまづき1:constをポインタとして扱えない

例えばこんなのがあったとして

const Ten = 10

type User struct {
    age *int
}

以下のコンパイルが通りません。

func main() {
    user := User{
        &Ten, // コンパイル通らない🤔
    }
    fmt.Println(user)
}

エラーメッセージを見てみると

invalid operation: cannot take address of Ten (constant 10 of type Age)

とのこと。constのアドレスを取ることはできないらしい。

Goの仕様を見てみる

&オペレーターに関する仕様を見てみます。どうやらここが該当していそうです。

The Go Programming Language Specification - The Go Programming Language

The operand must be addressable , that is, either a variable, pointer indirection, or slice indexing operation; or a field selector of an addressable struct operand; or an array indexing operation of an addressable array.

たしかに&オペレーターを取ることのできるオペランドにconstantsはいないですね。constantは変数(variable)ではないことに注意ですね。

なんでconstのポインタとして扱えないの?

ここに答えがありそうです。

Taking address of string literal

リンク先はstringの例ですがintリテラルという意味では同じ、かつconstはリテラルで表現されるものなので同様のことが言えそうです。

以下のような関数があるとして

func changeIntLiteral(s *int) {
   *s = 1
}
// これはできない
changeIntLiteral(&10)

// もちろんこれもできない
const Ten = 10
changeIntLiteral(&Ten)

はできないということですね。

これを許してしまうとリテラル(変数の値ではなく)が直接書き換えられてしまうというよくわからないことが起きてしまいます。また、定数であるはずのconstの値を書き換えることが可能になってしまいます。なのでconstのアドレスを取ることはできないし、ポインタとして扱うこともできない、ということみたいです。

じゃあポインタとして扱いたい場合はどうするの?

リテラルを直接書き換えるようなことをしなければいいので以下の方法で解決できそうです。

  • 一度変数に入れてそのポインタを渡す
  • 関数を経由する

先程の例なら

const Ten = 10

type User struct {
    age *int
}

一度変数に取る場合はこんな感じ。

ten := Ten
user := User{
    &ten, // コンパイル通る🤗
}

const Tenと同じ値の変数を作りそのポインタを渡しています。

関数を経由する場合はこんな感じ。

func ToPtrInt(x int) *int {
    return &x
}

user := User{
    ToPtrInt(Ten)
}

これも変数のときと原理は同じで、xという変数がconst Tenと同じ値を持ちそのポインタを返しています。

ちなみにGo 1.18以降ならジェネリクスを使ってより汎用的に書けます。

func ToPtr[T any](x T) *T {
    return &x
}

参考資料

つまづき2:[]inttype X []int に代入できる

以下はコンパイルエラーになります。

type MyInt int

func returnMyInt(myInt MyInt) MyInt {
    return myInt
}

func main() {
    three := 3
    v := returnMyInt(three) // ここでコンパイルエラー
    fmt.Println(v)
}

エラーメッセージは

cannot use three (variable of type int) as type MyInt in argument to returnMyInt

で、「int型の変数をMyInt型として使うことはできないよ」と言っています。これは納得です。

では次の例です。

type MyIntSlice []int

func first(myIntSlice MyIntSlice) int {
    return myIntSlice[0]
}

func main() {
    arr := []int{1, 2, 3}
    v := first(arr) // コンパイルエラーにならない!?
    fmt.Println(v)  // 1
}

先ほどの例と異なるのはint型ではなく[]int型を扱っているところです。先ほどの例と同様にコンパイルエラーになるのかなと思いきや、こちらは通ります。

この謎を追ってみたいと思います。

代入可能性

intの例ではMyInt型の変数にint型の変数を代入しようとしてコンパイルエラーになっています。一方、[]int型の例ではMyIntSlice型の変数に[]int型の変数を代入しようとしてコンパイルエラーにはならずに通りました。

つまり「代入」について知る必要がありそうです。Goの仕様書に「Assignability(代入可能性)」というまさにドンピシャの記述があります。
The Go Programming Language Specification - The Go Programming Language

引用すると(番号は勝手につけました)

A value x of type V is assignable to a variable of type T ("x is assignable to T") if one of the following conditions applies:

  1. V and T are identical.
  2. V and T have identical underlying types but are not type parameters and at least one of V or T is not a named type.
  3. V and T are channel types with identical element types, V is a bidirectional channel, and at least one of V or T is not a named type.
  4. T is an interface type, but not a type parameter, and x implements T.
  5. x is the predeclared identifier nil and T is a pointer, function, slice, map, channel, or interface type, but not a type parameter.
  6. x is an untyped constant representable by a value of type T.

6つある条件のうち1つを満たせば代入可能ということみたいですね。つまり[]int, MyIntSliceの例はこれらのどれか1つを満たすはずです。

1つずつ見ていきたいところですが、3, 4, 5については今回の例とは明らかに関係なさそうなので割愛します。

1. V and T are identical.

では代入可能性の1つめの条件について考えてみます。

identicalってなに?

条件の中にidenticalというワードが出てきました。日本語だと「同一の」という形容詞です。Go言語における「同一」とはなんでしょうか?

こちらにその記述があります。
The Go Programming Language Specification - The Go Programming Language

named type  is always different from any other type.

named typeというものは常に他の型とは異なる(つまりidenticalじゃない)そうです。2つの型がnamed typeでない場合の条件もありますが今回は問題とは関係ないので割愛します。詳しくはぜひ仕様書をご覧ください!

named typeってなに?

named typeがなにかというとこちらに記述があります。
The Go Programming Language Specification - The Go Programming Language

Predeclared types, defined types, and type parameters are called named types . An alias denotes a named type if the type given in the alias declaration is a named type.

defined typeはnamed typeの1つみたいです。defined typeというのは雑に言うと

type 名前 型

みたいに定義された型です(より正確な定義はこちら https://go.dev/ref/spec#Type_declarations)。ということはMyIntSlice型はdefined typeですね!

では[]int型はnamed typeなのでしょうか?defined typeでも型パラメータでもないのでPredeclared typeかどうかが判断できればよさそうです。

Predeclared typeの一覧はこちらです。
The Go Programming Language Specification - The Go Programming Language

[]int型はPredeclared typeではないようですね。なのでnamed typeではありません。

ちなみに[]int型はtype literal(型リテラル)と呼ぶそうです(https://go.dev/ref/spec#Type)。

MyIntSlice型と[]int型は条件1を満たすか?

MyIntSlice型はdefined type、つまりnamed typeのひとつであり、常に他の型とは異なるので[]intとはidenticalではないですね。
よってこの2つの型は条件を満たしません。

2. V and T have identical underlying types but are not type parameters and at least one of V or T is not a named type.

続いて代入可能性の2つめの条件について見てみます。

underlying typesという見慣れないワードが出てきました。これについては後から見るとして後半部分を見てみます。

but are not type parameters

まず今回の例はどちらも型パラメータがからんでくる話ではないのでこれは満たしています。
では次の

at least one of V or T is not a named type.

を見てみます。MyIntSlice型はdefined typeなのでnamed typeのひとつです。一方[]int型は型リテラルでnamed typeではありません。

よって

but are not type parameters and at least one of V or T is not a named type

の部分はMyIntSlice型と[]int型については満たしていることがわかります。

では条件の前半部分に戻って

V and T have identical underlying types

について考えてみます。

MyIntSlice型と[]int型のunderlying type?あとそれがidentical?

underlying typeについて述べている部分はこちらです。
The Go Programming Language Specification - The Go Programming Language

Each type T has an underlying type: If T is one of the predeclared boolean, numeric, or string types, or a type literal, the corresponding underlying type is T itself. Otherwise, T's underlying type is the underlying type of the type to which T refers in its declaration.

T型というのはunderlying typeを持っているようです。型リテラルである[]int型はそれ自身がunderlying typeなので[]int型です。
一方、MyIntSlice型の定義は

type MyIntSlice []int

で、underlying typeは[]int型です。

ということでMyIntSlice型と[]int型のunderlying typeはどちらも[]int型です。そしてこれは明らかにidenticalです。

MyIntSlice型と[]int型は条件2を満たすか?

ここまでみてきた通り条件2の

V and T have identical underlying types but are not type parameters and at least one of V or T is not a named type.

MyIntSlice型と[]int型は満たします。

よって代入可能性の条件を満たすので[]int型の変数をMyIntSlice型の変数に代入することができます!

6. x is an untyped constant representable by a value of type T.

代入可能性を満たすことはわかりましたが、せっかくなのでこの条件についても触れておこうと思います。

xはuntyped constantであると。ただのuntyped constantではなくてrepresentable by a value of type T なuntyped constantであると。

MyIntSlice型と[]int型の例ではconstを扱っていないのでこの条件は満たさないですがせっかくなので見てみます。

untyped constantってなに?

仕様書のconstantに記述があります。
The Go Programming Language Specification - The Go Programming Language

定義を正確に書くとちょっと大変そうですが、仕様書にある例を見るとどんなものがtypedもしくはuntypedなのかわかりそうです。 一部を抜粋してみます。

const a = 2 + 3.0          // a == 5.0   (untyped floating-point constant)
const b = 15 / 4           // b == 3     (untyped integer constant)
const c = 15 / 4.0         // c == 3.75  (untyped floating-point constant)
const Θ float64 = 3/2      // Θ == 1.0   (type float64, 3/2 is integer division)
const Π float64 = 3/2.     // Π == 1.5   (type float64, 3/2. is float division)
const k = 'w' + 1          // k == 'x'   (untyped rune constant)
const m = string(k)        // m == "x"   (type string)

型を明記するかstring関数のようなものを使わなければuntypedになりそうというざっくりイメージです。

representableってなに?

こちらにその仕様があります。
The Go Programming Language Specification - The Go Programming Language

日本語だと「表現可能な」みたいな感じでしょうか。

constant x is representable by a value of type T, where T is not a type parameter, if one of the following conditions applies:

  • x is in the set of values determined by T.
  • T is a floating-point type and x can be rounded to T's precision without overflow. Rounding uses IEEE 754 round-to-even rules but with an IEEE negative zero further simplified to an unsigned zero. Note that constant values never result in an IEEE negative zero, NaN, or infinity.
  • T is a complex type, and x's components real(x) and imag(x) are representable by values of T's component type (float32 or float64).

日本語に雑に訳すと以下のようなかんじでしょうか。

定数xが型Tの値によってrepresentableとは、Tが型パラメータでなく以下の条件のうち1つを満たすもの

  • Tによって決定される値の集合にxが含まれる
  • Tがfloating-point型で、xをオーバーフローせずにTの精度で丸めることができる
  • Tがcomplex型で、xの要素がTの要素の型の値で表現できる

おまけ感ありますが条件6についてもざっくり眺めてみました。

なんでこういう仕様なの?

なぜMyIntSlice型に[]int型を代入できるようになっているのかについてのソースを見つけられなかったのでこれで終わってもいいのですが、せっかくなので考察してみます。

MyInt型にint型を代入できないように、MyIntSlice型に[]int型を代入できない方がよくない?と感覚的には思ったりします。

ですがGo言語でそのようにするのは無理なのでは?という考えを書いてみます。以下は正しいかどうかの自信はなく誤っている可能性も大いにあることにご留意ください!間違い訂正大歓迎です!

MyInt型とint型の代入可能性とMyIntSlice型と[]int型の代入可能性、差分はどこか

MyIntSlice型と[]int型では代入可能性の以下の条件を満たしたため代入可能でした。

  1. V and T have identical underlying types but are not type parameters and at least one of V or T is not a named type.

この条件をMyInt型とint型は満たさないわけですが、差分はどこかというと

at least one of V or T is not a named type.

の部分です。MyInt型とint型はともにnamed typedです。named typeの条件は

Predeclared types, defined types, and type parameters are called named types . An alias denotes a named type if the type given in the alias declaration is a named type.

で、int型はpredeclared typeのためです。一方[]int型は型リテラルなのでnamed typeではありません。

リテラルをdefined typeにできればMyIntSlice型と[]int型も代入不可能にできそう → 無理では?

リテラルとはmapやスライスのように他の型によって定義される型です。なのでdefined typeにするのに他の型が必要です。型は無限に作れるので各々を定義するのは不可能です。もちろん、ジェネリクスを使えばできそうな気はしますがGoでは1.18から入った機能なのでやっぱり定義できません。

一方、int型はGo言語の中にdefined typeで定義されています。
go/builtin.go at master · golang/go · GitHub

ということでMyInt型にint型を代入できないように、MyIntSlice型に[]int型を代入できないようにしたくてもできないのでは?という考察でした。

考察おわり!(繰り返しになりますが大いに間違えている可能性があることをご留意ください。)

参考資料

つまづき3:整数リテラルをtype X int型に代入できる

つまづき2で以下はコンパイルエラーになることを示しました。

type MyInt int

func returnMyInt(myInt MyInt) MyInt {
    return myInt
}

func main() {
    three := 3
    v := returnMyInt(three) // ここでコンパイルエラー
    fmt.Println(v)
}

これはMyInt型にint型を代入しようとしていますが代入可能性を満たさないためコンパイルエラーになっているのでした。

しかし以下はコンパイルエラーになりません。

type MyInt int

func returnMyInt(myInt MyInt) MyInt {
    return myInt
}

func main() {
    // 変数に入れないで3を直接渡す
    v := returnMyInt(3) // コンパイルエラーにならない
    fmt.Println(v)
}

なぜこれが通るのか

この例が示しているのはMyInt型の変数に「3」が代入可能ということですね。まず「3」が何者なのかを見てみます。

整数リテラルとuntyped

「3」というのはGo言語においては整数リテラルと呼びます。整数リテラルについての仕様を見てみます。
The Go Programming Language Specification - The Go Programming Language

An integer literal is a sequence of digits representing an integer constant

整数リテラルとはinteger constantを表現している数字の列のこと、ということみたいですね。そしてconstantといえばtypedだったりuntypedだったりするのでした(https://go.dev/ref/spec#Constants)。今回の場合はuntypedです。

代入可能性

この例は代入可能性のどの条件を満たすのかというと

x is an untyped constant representable by a value of type T.

を満たします。MyInt型が取る値はintなので「3」はMyInt型で表現可能です。先に述べたように「3」という整数リテラルはuntyped constantなのでこの条件を満たすというわけです。

今回は整数リテラルを代入してみましたがconstで定義した場合も同様の理由でコンパイルが通ります。

type MyInt int

func returnMyInt(myInt MyInt) MyInt {
    return myInt
}

const three = 3 // untyped constant

func main() {
    v := returnMyInt(three) // コンパイルエラーにならない
    fmt.Println(v)
}

参考資料

まとめ

Go言語を使っていて「あれ、これできるんだ」「これはできないんだ」と感じたものを仕様から追ってみました。プログラミング言語の仕様を読むということをしたことがなかったので大変興味深かったです(そして難しかった…先人の記事には大変助けてもらいました!)。Go言語における型がどのようなものなのか理解が深まりました。整数リテラル等のリテラルはその時点では型がなく、それが使われるときに型が確定するというのは驚きでした。

今回は型としてはシンプルな例で、ポインタやインターフェースを絡めた話はなかったのですが思っていたより話が深かったです(そして結構な文量になってしまった…)。

またつまづいたときには仕様も読んでみようかと思います!

Scalaが僕に教えてくれたこと

Scalaが僕に教えてくれたこと

Scalaを約3年くらい業務で書いてみて色々学びがあったのでまとめてみます。 Scala 2系を使います。

ちょっと自己紹介

Scalaを始める前にどんなことを知っていたかを軽く。

  • 初めてプログラミングに触れたのは大学生になってから。
  • 大学時代で触った言語はC, C++, Pythonで研究ではほとんどPythonだった。
    • 講義で少しJavaはやったけどCみたいな書き方しかしてなかった。
  • 関数型?なんか頭いい人がやるやつよね?
  • Scalaを始めたのは新卒で入社してから。

こんな人がScalaをやってみたらこんな世界を知れたよ!というのが伝われば幸いです。

1. 型という世界

  • 例外など副作用を型として扱う
  • collectメソッドにMapインスタンスが渡せる
  • 型クラス 〜List[Int].sumを例に〜

Scalaを始める前は型になんとなく苦手意識みたいなものがありました。Cを初めて触ってコンパイルが通らずなにか邪魔してくるものみたいな感覚があったからかもしれません。あとはPythonをよく使っていたこともあって型の世界にそんなに慣れ親しんでいなかったというのもある気がします。

そんな僕ですがScalaを始めて型っていいものだなと感じるようになったので例を挙げながら書いていきます。

型で文脈を表現する

永続化されたユーザー情報をとってくるfindByName(name: String)というメソッドを考えてみます。このメソッドはユーザー名で検索することができます。

class UserRepository() {
  // 存在しないユーザー名を指定されたら…?
  def findByName(name: String): User = ???
}

存在しないユーザー名を指定されたらどうでしょうか?例外を出すかもしれないし、nullになるかもしれません。しかしこのシグネチャからはそれを読み取ることはできません。ユーザーがいるかもしれないし、いないかもしれない。そんな文脈をScalaではOption型を使って表現できます。

class UserRepository() {
  // nameのユーザーがいるかもしれないし、いないかもしれない
  def findByName(name: String): Option[User] = ???
}

こんな感じです。存在すればSome(user: User)が、存在しなければNoneが返ってきます。

他にも非同期処理をしたい場合にはFuture[A]という型もあります。これはいつか型Aのインスタンスが返ってくるかもしれないし、返ってこないかもしれないという文脈を表現しています。成功すればSuccess(Aのインスタンス)、失敗すればFailure(exception: Throwable)が返ってきます。

失敗の可能性等も型で表現できるため、シグネチャを見ればどんなメソッドなのかがわかるというのは素敵です。

Map, Set, Seqが関数の性質を持っている

型の世界というよりScalaの世界の話かもしれませんが、面白いなーと感じたので紹介します。

val dictionary = Map("谷" -> "ravine", "口" -> "port")
val japaneseSeq = Seq("谷", "ゲーム", "口")

japaneseSeqの内容をdictionaryに登録されているものは英語に翻訳し、ないものは無視したいとします。例えばこんな風に書けます。

japaneseSeq.filter(dictionary.keySet).map(dictionary) // 結果: Seq("ravine", "port")

もう少し冗長に書くと

val keys = dictionary.keySet // Set("谷", "口")
val filteredSeq = japaneseSeq.filter(keys) // List(谷, 口)
filteredSeq.map(dictionary)

です。

ここでfilterシグネチャを見てみます。

def filter(pred: A => Boolean): C

filterにはA => Booleanという関数が要求されています。先ほどの例ではSet[String]を渡しました。 これが通るということはdictionary.keySetSet[String]ではなくA => Booleanとして扱われているようです。実際にdictionary.keySetを変数にバインドする際にA => Booleanという型をつけてみます。

val f: String => Boolean = dictionary.keySet // dictionary.keySetはSet[String]型だと思っていたがこれは通る
f("谷")    // true
f("ゲーム") // false

「え、Set[String]String => Boolean…?」という感じですが、実はapplyというメソッドが省略されています。

val f: String => Boolean = dictionary.keySet.apply

applyという名前のメソッドは特殊な扱いで省略可能です。dictionary.keySet("谷")dictionary.keySet.apply("谷")と同じです。シンタックスシュガーというやつです。

(余談:Scalaでは関数も型をもっていて、String ⇒ BooleanFunction1[String, Boolean]型ということを表しています。fはFunction1[String, Boolean]型のインスタンス(値)です。Scalaは全ての値がオブジェクトであるという意味での純粋オブジェクト指向言語ですね。また、すべての関数が値であるという意味でScala関数型言語ですね。(https://docs.scala-lang.org/ja/tour/tour-of-scala.html)

ということでfilter(keys)という書き方ができるわけです。map(dictionary)についても同様です。

// mapのシグネチャ
def map[B](f: A => B): CC[B]
val f2: String => String = dictionary.apply

dictionaryString => Stringの関数として扱うことができるのでmapに渡すことができます。

もしかしたらここで疑問に思う方もいるかもしれません。dictionaryに存在しない文字列を渡したらどうなるの?と。その場合は実行時エラーになります。

val dictionary = Map("谷" -> "ravine", "口" -> "port")
dictionary.apply("谷口") // 実行時エラー! java.util.NoSuchElementException: key not found: 谷口

今回はあらかじめfilterdictionaryに存在するものしか渡ってこないため大丈夫というわけです。

さて、先程はfiltermapを使って「japaneseSeqの内容をdictionaryに登録されているものは英語に翻訳し、ないものは無視する」ということを実現しましたが、collectメソッドを使えばもっとシンプルに書けます。

japaneseSeq.collect(dictionary)

こんな短く書けるとは・・・すごい。ちょっとcollectシグネチャを見てみます。

def collect[B](pf: PartialFunction[A, B]): CC[B]

dictionaryつまりMap[K, V]PartialFunction[A, B]として渡しているようです。PartialFunctionは部分関数と言って特定の入力のときだけ値を返す関数です。Mapはまさにその性質を持っています。今回の例だと"谷"と"口"しか入力として受け付けないですね。実際にMapの定義を追っていくとPartialFunctionがMix-inされています。

型クラス

Seqには要素の合計値を返すsumというメソッドがあります。

Seq(1, 2, 3).sum // 結果:6

これをSeq[String]でやろうとするとどうなるか。

Seq("ravine", "port").sum // コンパイルエラー!:could not find implicit value for parameter num: scala.math.Numeric[String]

同じSeq[A]sumメソッドなのにStringだとコンパイルエラーになるのすごいですね。確かに文字列の合計(結合じゃなくて)ってなんやねんという感じです。sumの定義を見てみます。

def sum[B >: A](implicit num: Numeric[B]): B = if (isEmpty) num.zero else reduce(num.plus)

[B >: A]はここでは触れないでおいて、implicit num: Numeric[B]というのが今回のミソです。引数の型が型パラメータを持っていて、Seq[A]Aに合わせて暗黙的に引数に渡されるインスタンスが切り替わってくれます。賢い。Seq(1, 2, 3).sumの例で言えばNumeric[Int]が渡されています。これはScalaにあらかじめ定義されています。対してSeq("ravine", "port").sumではNumeric[String]がどこにも定義されていないのでコンパイルエラーになりました。実行時ではなくコンパイル時にエラーになるのがすてきです。この仕組みを型クラスといいます。世界の広がりを感じますね。

さて、ここまでで型については終了です。はじめはなにかにつけてプログラムを実行させてくれないちょっと鬱陶しいものだった型でしたが、Scalaを学んでいくうちに型っていいものだなという気持ちとともになにか表現の広がりのようなものも感じるようになりました。

2. 関数型という世界

関数型のかの字もわからなかった僕ですが、Scalaをやっていると自然とその世界に触れています。

実は「Map, Set, Seqが関数の性質を持っている」では関数型言語の一端に触れていました。関数に関数を渡すのはまさに関数型のパラダイムです。気づかぬ間に関数型の世界に触れていたようです。

ここではもう少し関数型の世界を深掘ってみようと思います。

不変な世界

こちらのページの説明を引用させて頂くと

また、Scalaはオブジェクトの不変性(immutability)を意識している言語です。変数宣言は変更可能なvarと変更不可能なvalが分かれており、コレクションライブラリもmutableとimmutableでパッケージがわかれています。 case classもデフォルトではimmutableです。

で、基本的には不変な世界でプログラミングをしていきます。関数型の世界では不変なものを扱います。宣言したものを変えられないとかめちゃくちゃ不便じゃんとはじめは思っていましたが、慣れると基本は不変な世界でコードを書くほうがわかりやすい(個人の感想です)し、バグも生み出しにくい(個人の感想です)と感じるようになりました。Scalaの豊富なコレクションライブラリを駆使して不変な世界でプログラミングしていくうちに気がついたら関数型という概念に割と慣れ親しんでいたという感覚です。

↑の引用でも述べられているように必要であれば可変なものも使うこともできます。懐が広いですね。

ちょっと横道に逸れますが、Scalaを始める前はオブジェクト指向と関数型は反するものというイメージがありました(なぜかはわかりませんが)。ですが、その2つの概念は相反するものではなく、直交するものだということもScalaは教えてくれました。 これに関してはこちらの記事が参考になると思います。

高カインド型、そして圏論

高カインド型とはF[_]と表現されているやつです。なんだかとても抽象的ですね。ちょっと順番に見ていこうと思います。

こちらの図を御覧ください。

慣れ親しんでいる型たちです。MyClassは自分で定義した型です。ここに「1. 型という世界」の世界でも登場したOption[A]などの型パラメーターを取る型(first-order typeと言います)を加えてみます。

[A]というのは任意の型を表しており、縦線と横線の交差している点が具体的な型になります。例えばOption[A]Stringの交差点ならOption[String]です。

ところでOption[A]にはmapというメソッドがあります。そのシグネチャは以下のとおりです。

def map[B](f: A => B): Option[B]

これが意味していることはOption[A]Someなら中の値に関数fを適用してOption[B]を得て、Noneならなにもしないというメソッドです。 実はSeq[A]Future[A]にも同じ(Futureはちょっと違うけど許してください)メソッドがあります。

# Seq[A]
def map[B](f: A => B): Seq[B]

# Future[T]
def map[S](f: T => S)(implicit executor: ExecutionContext): Future[S]

Seqの方は各要素に関数fを適用していく、Futureの方はSuccessであればその値に関数fを適用してFailureであればなにもしないというメソッドです。

Futureにはimplicitパラメーターがありますがどれもなんだか似てませんか?ちなみにflatMapというメソッドもあってシグネチャは以下のとおりです。

# Option[A]
def flatMap[B](f: A => Option[B]): Option[B]

# Seq[A]
def flatMap[B](f: A => IterableOnce[B]): Seq[B]

# Future[T]
def flatMap[S](f: T => Future[S])(implicit executor: ExecutionContext): Future[S]

SeqのfはIterableOnce[B]だったり、Futureにはimplicitパラメーターがありますが目をつぶっていただいて…こちらもやはり似た形をしています。

ちょっと違くない?ということを握りつぶして図にするとこんな感じです。

だいぶ握りつぶしましたが、Option[A], Seq[A], Future[A]で似たような構造を持っているようです。つまりこれらを抽象化したF[A]という型(高カインド型と言います)を考えられそうです。F[A]mapflatMapを持っています。

// F[A]はこんなシグネチャのメソッドを持つ
def map[B](f: A => B): F[B]
def flatMap[B](f: A => F[B]): F[B]

こんなイメージです。

これをScalaのコードに落としてみるとこんな感じです。

trait HigherKind[F[_]] {
  def map[A, B](a: F[A])(f: A => B): F[B]
  def flatMap[A, B](a: F[A])(f: A => F[B]): F[B]
}

object HigherKind {
  implicit val option: HigherKind[Option] = new HigherKind[Option] {
    override def map[A, B](a: Option[A])(f: A => B): Option[B] = a.map(f)
    override def flatMap[A, B](a: Option[A])(f: A => Option[B]): Option[B] = a.flatMap(f)
  }

  implicit val seq: HigherKind[Seq] = new HigherKind[Seq] {
    override def map[A, B](a: Seq[A])(f: A => B): Seq[B] = a.map(f)
    override def flatMap[A, B](a: Seq[A])(f: A => Seq[B]): Seq[B] = a.flatMap(f)
  }

  implicit def future(implicit ec: ExecutionContext): HigherKind[Future] = new HigherKind[Future] {
    override def map[A, B](a: Future[A])(f: A => B): Future[B] = a.map(f)
    override def flatMap[A, B](a: Future[A])(f: A => Future[B]): Future[B] = a.flatMap(f)
  }
}

Option, Seq, FutureをHigherKindとしてまとめることができました。

この構造、聞いたことある方も多いであろうモナドと大いに関係しています。これ以上突っ込むのはやめておこうと思いますが、多少なりともF[A]のイメージがわけば幸いです。

さらにこのF[A]モナドという言葉からもわかるように圏論という分野につながっています(というより圏論由来の言葉です)。興味がわいたので自分なりに勉強してみてこんな発表させてもらったりしました。

https://speakerdeck.com/tanitk/scaladequan-lun-tiyotutoxue-bu

関数型についてはここまでです。関数型と概念だけでなく、圏論という数学のための数学とも言われている分野を知ることができたりで、プログラミングの領域を飛び越えてScalaが学びを与えてくれました。

3. ドメイン駆動設計と事業への関心

Scalaの話題からはちょっとそれるのですが、エンジニアとしての意識が変わったなーという瞬間だったので紹介します。

ドメイン駆動設計(略称:DDD)をご存知でしょうか?ソフトウェア開発の設計スタイルの1つです。DDDがなにかはたくさん記事があるのでここでは述べないことにして、それを通してどんな意識の変化が生まれたかを書いてみます。

DDDはScalaと相性がよいです(参考資料:https://speakerdeck.com/crossroad0201/scala-on-ddd)。 プラス開発現場でもDDDが共通知識としてあったためDDDを学ぶのは必然でした。

最初は設計パターンとしか見ていなかったのですが、ちゃんと本を読んでみるとどうやら「君たちドメインに向き合ってる?ビジネスの人と会話してる?」という主張をしていてそれが本当に言いたいことなのでは?と思うようになりました。 思い返してみると、開発アイテム1つ取ってみてもそれがなんで必要なのか、どんな効果が期待できるのかなどなどをよくわかっていなかったし、ビジネスの人がどういう仕事してるかもよく知らなかったです。DDDは「もしかして自分が携わっているプロダクトと人のことなにも理解していない…?」という気づきを与えてくれました。 ということで、積極的にビジネスの人とコミュニケーション取ってプロダクトの目指すところ、今はなにをしようとしているのかを意識するようになりました。事業部とエンジニアの1on1を企画したりしてビジネスの人たちがどんな人なのか、どんな気持ちで仕事をしているのかを知れたのはとてもよかったです。ビジネスの人と密に連携して目指す世界の解像度を上げ、それをソフトウェアとして表現していくのはとても楽しいです。

「とりあえずなんか作れればええねん」という気持ちから「ビジネスもエンジニアもみんなでプロダクトを作っていくぞ!」という気持ちに変わったのは、Scalaを通してDDDの世界を知れたからに他なりません。

まとめ

Scalaが僕に教えてくれたことをまとめてみました(最後はDDDの話題だったけど)。他にもfor式を使った書き方、Akkaを使ったStream処理、Actorなどなどまだまだ学びはあるのですがここらへんにしておこうと思います。

Scalaを使った開発現場からは遠ざかってしまったのですが今もScalaはとても好きな言語です。ぜひScalaをやってみてください!

Azure Cosmos DBでAtomic Counterを実現する

Atomic Counter

一意性を保ったカウンタのこと(そのまま)。

あるキーに対してカウントが保存されているDBを考える。このDBに保存されているデータをカウントアップすることを普通に考えるなら、キーでSELECT、そのカウントを+1してデータUPDATEするという実装になるはず。

しかし、2つのクライアントが同時にデータを読み込み、同時に書き込みを行うようなタイミングが想定されるときには不整合が起きえてしまう。例えば、testというキーに1が保存されているときに、2つのクライアントが同時にカウントアップしようとした場合は期待する結果は3となる。しかし、読み取り時にどちらのクライアントも1を読み取り、カウントアップして2を書き込むと最終結果が2になってしまう。これが一意性が保たれていない状態。Atomic Counterはこのような場合でも3が記録されることを保証する。

Azure Cosmos DB

とにかく速くて世界中にスケールできるDB(雑)。AWSDynamo DB、GCPのCloud Spannerに相当するサービス。 docs.microsoft.com

今回はDatabase id: AtomicCounterTest、Container id: Counter(Partition key: /key、Unique keys: /key)で作成した。ドキュメントはkeyとcountプロパティを持つ。

f:id:taniT91:20200517114702p:plain
今回作成したデータベースとコンテナ

Cosmos DBでAtomic Counterの実現

Stored Procedureを使ったトランザクション管理

Atomic性を実現するためにトランザクション管理をする必要がある。そのままではAtomic性が保証されないので、Stored Procedureという機能を使うことで実現する。 SDKCLIを使って登録する他、Azure Portalからも作ることもできる。

f:id:taniT91:20200517004152p:plain
Azure PortalからStored Procedureを作成

今回はspCountUpという名前でStored Procedureを作ってみた。Stored ProcedureはJavaScriptで記述する。

f:id:taniT91:20200517113248p:plain
Stored Procedureを実装してみた

実装は以下。js力に自信がないのでもっといい書き方あったら教えて下さい。

// keyのcountに+countUpNumStrして更新、なければ新規作成
function countUp(key, countUpNumStr) {
    const context = getContext();
    const container = context.getCollection();
    if (!key) throw "Error: key is null or empty. key=" + key;
    const countUpNum = Number(countUpNumStr);
    if (isNaN(countUpNum)) throw "Error: countUpNumStr is not Number. countUpNumNumStr" + countUpNumNumStr;

    // keyでSELECT
    const filterQuery =
    {
        'query' : 'SELECT * FROM Counter c where c.key = @key',
        'parameters' : [{'name':'@key', 'value':key}]
    };

    // Query実行
    const accept = container.queryDocuments(container.getSelfLink(), filterQuery, {},
        function (err, items, responseOptions) {
            if (err) throw "Failed to read document: key=" + key;

            // なければ新規作成して終了
            if (items.length != 1) {
                const document = {'key': key, 'count': countUpNum};
                const accept2 =  container.createDocument(container.getSelfLink(), document, {},
                    function (error, resource, options) {
                        if (error) throw "Error: " + error.message;
                    }
                );
                if (!accept2) throw "Failed to create document: key=" + key;
                return;
            }
            // unique key制約により1つしか存在しないことが保証されている
            let document = items[0];
            document.count += countUpNum;

            // 該当ドキュメントを更新
            const accept2 = container.replaceDocument(document._self, document,
                function (err2, replacedDocument) {
                    if (err2) throw "Error: " + err2.message;
                }
            );
            if (!accept2) throw "Failed to update document: key=" + key;
        });
    if (!accept) throw "Failed to read document: key=" + key;
    return;
}

このStored Procedureはkey(更新したいkey。String)とcountUpNumStr(いくつカウントアップするか。String)という2つの入力パラメータを受け取る。keyでドキュメントを検索して、なければcountがcountUpNumStrのドキュメントを新規作成、あればそのcountを+countUpNumStrするという実装になっている。各処理で失敗したときはエラーを返す。context.getResponse().setBody()を使えばレスポンスボディも変更できる。

入力パラメータは常にStringで渡ってくる。

Azure Portalから実行

作成したStored Procedure、spCountUpをAzure Portal上からテストすることができる。

f:id:taniT91:20200517120652p:plain
Azure Portalから実行

Stored Procedureを実行するときは、Stored Procedure内で定義した引数の他にPartition keyを指定する必要があるので注意。1つのStored Procedureで複数のPartition keyを参照することはできないらしい。図の例ではtestというkeyをもつドキュメントのcountを+3する。

Java SDKから実行

次にJava SDK経由で実行してみる。Scalaが好きなのでScalaで。

package cosmos_db

import com.microsoft.azure.documentdb._
import scala.concurrent.{ ExecutionContext, Future }

class CosmosDB {
  private val cosmosDBClient = new DocumentClient(
    "DATABASE_URI",
    "MASTER_KEY",
    connectionPolicy,
    ConsistencyLevel.Session
  )

  private val connectionPolicy: ConnectionPolicy = {
    val retryOptions = new RetryOptions
    retryOptions.setMaxRetryAttemptsOnThrottledRequests(5)
    retryOptions.setMaxRetryWaitTimeInSeconds(30)

    val policy = new ConnectionPolicy
    policy.setConnectionMode(ConnectionMode.DirectHttps)
    policy.setMaxPoolSize(100)
    policy.setIdleConnectionTimeout(60)
    policy.setRetryOptions(retryOptions)
    policy
  }

  def executeStoredProcedure(storedProcedureLink: String, partitionKeyValue: String, args: Seq[String])(
    implicit ec: ExecutionContext
  ): Future[Unit] = {
    val requestOptions = new RequestOptions
    requestOptions.setPartitionKey(new PartitionKey(partitionKeyValue))
    Future(
      cosmosDBClient.executeStoredProcedure(storedProcedureLink, requestOptions, args.toArray)
    ).map { _ =>
      ()
    }.recoverWith {
      case e: Throwable =>
        Future.failed(
          new RuntimeException(
            s"failed to execute stored procedure($partitionKeyValue). arg: ${args.toString()}. message: ${e.getMessage}"
          )
        )
    }
  }
}

マルチリージョンの場合は一貫性レベル(コードのConsistencyLevel)をStrongにする必要があるかも。

使うときはこんな感じ。

import scala.concurrent.ExecutionContext.Implicits.global

object CosmosDBTest {
  def main(args: Array[String]): Unit = {
    val cosmosDB = new CosmosDB
    val spPath = "dbs/AtomicCounterTest/colls/Counter/sprocs/spCountUp"
    cosmosDB.executeStoredProcedure(spPath, "testKey", Seq("testKey", "3"))
  }
}

まとめ

Azure Cosmos DBでAtomic Counterを実現するためにStored Procedureを使った。Cosmos DBでトランザクション管理をするときには是非。

参考

ストアドプロシージャ、トリガー、および UDF を Azure Cosmos DB に記述する | Microsoft Docs

Azure Cosmos DB の JavaScript クエリ API を使用してストアドプロシージャとトリガーを記述する | Microsoft Docs

Akkaを使ってScalaのFutureにタイムアウトメソッドを生やす(あと注意点)

ScalaのFutureにはタイムアウトを設定するメソッドはない。
Await.result で設定時間待つことができるが、失敗した場合は例外がスローされてしまうのでTryなりで扱う必要があるし、なによりブロッキングするのでスレッドを無駄遣いしてしまっている。
そこで akka.pattern.after メソッドを使う。

akka.pattern.afterを使ったFutureのタイムアウト設定

こちらを参考にした stackoverflow.com

import akka.actor.ActorSystem

import scala.concurrent.duration._
import scala.concurrent.{ ExecutionContext, Future, TimeoutException }

object Main {
  implicit class FutureHelper[T](val f: Future[T]) {
    def withTimeout(timeout: FiniteDuration)(implicit ec: ExecutionContext, system: ActorSystem): Future[T] = {
      val timeoutF = akka.pattern.after(timeout, system.scheduler) {
        Future.failed(new TimeoutException("timeout occurred!"))
      }
      Future.firstCompletedOf(Seq(f, timeoutF))
    }
  }
}

implicit classFuturewithTimeout というメソッドを生やしている。
akka.pattern.after メソッドを使って timeout 時間経つとFutureが失敗し TimeoutException が発生する。で、Future.firstCompletedOf メソッドで先に完了した方を結果として返すことでFutureのタイムアウト設定を実現している。

使い方はこんな感じ

import scala.concurrent.ExecutionContext.Implicits.global

implicit val actorSystem: ActorSystem = ActorSystem("ActorSystem")

// 1秒後にタイムアウトするFuture
val future1 = Future {
  println("1st future start at " + Thread.currentThread().getId)
  Thread.sleep(3000) // 3秒スリープ
  true
}.withTimeout(1.second)

future1 onComplete {
  case Success(value) => println("future1 done: " + value)
  case Failure(exception) => println("future1 failed: " + exception)
}

結果

1st future start at 17
future1 failed: java.util.concurrent.TimeoutException: timeout occurred!

ちゃんとタイムアウトしているヽ(´ー`)ノ

注意点

implicitで渡すExecutionContextのスレッドプールが枯渇していたときは期待する動作をしない。

implicit val actorSystem: ActorSystem = ActorSystem("ActorSystem")
implicit val ec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1)) // スレッド数1のスレッドプール作成

// 1秒後にタイムアウトするはずのFuture
val future1 = Future {
  println("1st future start at " + Thread.currentThread().getId)
  Thread.sleep(3000) // 3秒スリープ
  true
}.withTimeout(1.second)

future1 onComplete {
  case Success(value) => println("future1 done: " + value)
  case Failure(exception) => println("future1 failed: " + exception)
}

結果

1st future start at 17
future1 done: true

Why?

3秒スリープする Future が生成され、スレッドを取得して実行

withTimeout メソッド内の timeoutF はスレッドを取得できず実行待ち(今回作ったExecutionContextはスレッドプールのサイズが1のため)

timeoutF がスレッドを取得できるのは3秒スリープする Future が完了してスレッドがプールに戻されてから

future1 が完了してしまう

今回は極端な例だが実際のアプリケーションでも起こりうる(というか起こってしまった)ので気をつけたい。

Gatlingのexec { session => ???}, jsonPathではまったこと

Gatlingとは?

https://gatling.io/

負荷テストのためのツール。Scalaで書くことができる。うれしい。

ここではGatlingを書いていくなかではまったことを書いていく。 

1. exec { session => ???} ではリクエストは投げられない

execメソッドの基本的な使い方は以下の通り。

exec {
  http("テストリクエスト")
    .get("/users") // /usersにGETリクエストを投げる
}

しかし、以下の場合は実際に/usersにリクエストは投げられない。

exec { session =>
  http("テストリクエスト")
    .get("/users") // 実行時にリクエストが投げられない!
 session // Session型を返すようにしないとコンパイルエラー
}

execにsessionを受け取る関数を渡す場合は、リクエストを投げる処理は書かないようにする。

なぜこのようなことをしようとしたかというと、http("テストリクエスト").get("/users")を関数にしてかつSessionに保存された値をリクエストパラメータとして使おうとしたため。 次のようなことをしようとした。

def usersRequest(session: Session): HttpRequestBuilder = {
  http("テストリクエスト")
    .get("/users")
    .queryParam("id", session.get("userId"))
}

exec { session =>
  usersRequest(session) // リクエストは投げられない
  session
}

この場合は以下のようにすればよい

def usersRequest(): HttpRequestBuilder = {
  http("テストリクエスト")
    .get("/users")
    .queryParam("id", "${userId}")
}

exec {
  usersRequest(session)
}

sessionを渡していないのにどこかに保存されているのであろうsessionの内容にアクセスできてしまうことに個人的には気持ち悪さを感じるが、そこはGatlingのお作法ということで。

2. レスポンスJSONからjsonPathメソッドを使って値を取り出すときは、一度Sessionに保存すること

レスポンスJSONが期待するものかどうかを確認するには以下のように書く。

http("request")
...
.check(jsonPath("$..result").exists) // 返ってきたJSONが{"result": ...}であるかどうかチェック

resultキーに対する値(Int型)を取り出してなにかしら処理をしたい場合に以下のように書いたがうまく値を取り出せなかった。

http("request")
...
.check {
  val result = jsonPath("$..result").ofType[Int] // resultに期待した値が入らない
}

値を取り出したい場合は一度Sessionに保存することでうまくいった。

http("request")
...
.check {
  jsonPath("$..result").ofType[Int].find.saveAs("result")
}

// ↑のあと次のように取り出せる
exec { session =>
  val result = session("result").as[Int]
}

がんばればjsonPathから値を取り出すこともやろうと思えばできるらしい。

参考:scala - How do I retrieve a value from a JSON string using gatling JsonPath.query? - Stack Overflow

import com.fasterxml.jackson.databind.ObjectMapper

http("request")
  .check {
    val json = """{"id":"derp"}"""
    val parsed = new ObjectMapper().readValue(json, classOf[Object])
    
    // to retrieve the ids as a list:
    val ids = JsonPath.query("$.id", parsed).right.get.toList
    
    // the only "id" result of the query
    val id = ids.head
}