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 }
参考資料
- constで定義した値(string)をポインタとして扱おうと思ったら怒られた - 寝ても覚めてもこんぴうた
- pointers - Find address of constant in go - Stack Overflow
- Taking address of string literal
つまづき2:[]int
を type 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 typeV
is assignable to a variable of typeT
("x
is assignable toT
") if one of the following conditions applies:
V
andT
are identical.V
andT
have identical underlying types but are not type parameters and at least one ofV
orT
is not a named type.V
andT
are channel types with identical element types,V
is a bidirectional channel, and at least one ofV
orT
is not a named type.T
is an interface type, but not a type parameter, andx
implementsT
.x
is the predeclared identifiernil
andT
is a pointer, function, slice, map, channel, or interface type, but not a type parameter.x
is an untyped constant representable by a value of typeT
.
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
A 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
orT
is not a named type.
を見てみます。MyIntSlice
型はdefined typeなのでnamed typeのひとつです。一方[]int
型は型リテラルでnamed typeではありません。
よって
but are not type parameters and at least one of
V
orT
is not a named type
の部分はMyIntSlice
型と[]int
型については満たしていることがわかります。
では条件の前半部分に戻って
V
andT
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
andT
have identical underlying types but are not type parameters and at least one ofV
orT
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
日本語だと「表現可能な」みたいな感じでしょうか。
A constant
x
is representable by a value of typeT
, whereT
is not a type parameter, if one of the following conditions applies:
x
is in the set of values determined byT
.T
is a floating-point type andx
can be rounded toT
'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, andx
's componentsreal(x)
andimag(x)
are representable by values ofT
's component type (float32
orfloat64
).
日本語に雑に訳すと以下のようなかんじでしょうか。
定数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
型では代入可能性の以下の条件を満たしたため代入可能でした。
V
andT
have identical underlying types but are not type parameters and at least one ofV
orT
is not a named type.
この条件をMyInt
型とint
型は満たさないわけですが、差分はどこかというと
at least one of
V
orT
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 typeT
.
を満たします。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.keySet
はSet[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 ⇒ Boolean
はFunction1[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
dictionary
をString => String
の関数として扱うことができるのでmap
に渡すことができます。
もしかしたらここで疑問に思う方もいるかもしれません。dictionary
に存在しない文字列を渡したらどうなるの?と。その場合は実行時エラーになります。
val dictionary = Map("谷" -> "ravine", "口" -> "port") dictionary.apply("谷口") // 実行時エラー! java.util.NoSuchElementException: key not found: 谷口
今回はあらかじめfilter
でdictionary
に存在するものしか渡ってこないため大丈夫というわけです。
さて、先程はfilter
とmap
を使って「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]
はmap
とflatMap
を持っています。
// 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(雑)。AWSのDynamo DB、GCPのCloud Spannerに相当するサービス。 docs.microsoft.com
今回はDatabase id: AtomicCounterTest、Container id: Counter(Partition key: /key、Unique keys: /key)で作成した。ドキュメントはkeyとcountプロパティを持つ。
Cosmos DBでAtomic Counterの実現
Stored Procedureを使ったトランザクション管理
Atomic性を実現するためにトランザクション管理をする必要がある。そのままではAtomic性が保証されないので、Stored Procedureという機能を使うことで実現する。 SDKやCLIを使って登録する他、Azure Portalからも作ることもできる。
今回はspCountUp
という名前でStored Procedureを作ってみた。Stored ProcedureはJavaScriptで記述する。
実装は以下。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上からテストすることができる。
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 class
で Future
に withTimeout
というメソッドを生やしている。
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とは?
負荷テストのためのツール。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 }