Magnolia Tech

いつもコードのことばかり考えている人のために。

Scala3学習メモ: Scala3におけるType Lambda

Scala2におけるType Lambdaについては、下記のブログエントリが詳しい。

underscore.io

最初に、型パラメータの数を一つとして定義していると、型パラメータを二つ取る物を渡せない。

scala> trait Functor[F[_]]
trait Functor

scala> type F1 = Functor[Option]
type F1

scala> type F2 = Functor[List]
type F2

scala> type F3 = Functor[Map]
                         ^
       error: Map takes 2 type parameters, expected: 1

Mapが取る二つの型のうち、一つが決まれば、残るは一つなので大丈夫。

scala> type IntKeyMap[A] = Map[Int, A]
type IntKeyMap

scala> type F3 = Functor[IntKeyMap]
type F3

ただし、いちいち途中の階層の型を定義するのは面倒くさい。

そこで、無名の型を定義して、その中で一つの型を確定させ、その型を返す方法が発明された。これなら1行で済むし、余計な命名も不要になる。

scala> type F5 = Functor[({ type T[A] = Map[Int, A] })#T]
type F5

でも何をやっているのかわかりづらい。

そこで、Scala3には専用の構文(=>>)が導入された。だいぶ謎記号が減ったScalaだけど、ここに来て一つ増えた。

scala> type F6 = Functor[ [A] =>> Map[Int, A] ]
// defined alias type F6 = Functor[[A] =>> Map[Int, A]]

Scala2の時のtype lambdaの書き方をすると、対応が分かりやすい。

scala> type F7 = Functor[({ type T[A] = Map[Int, A] })#T]
// defined alias type F7 = Functor[[A] =>> Map[Int, A]]

型のカリー化もできるそうだ。

type TL = [X] =>> [Y] =>> (X, Y)

型パラメータが得られば、こんな指定もできる。

scala> def foo[A[_,_],B](functor: Functor[({type AB[C] = A[B,C]})#AB]) = ???
def foo[A[_$3, _$4], B](functor: Functor[[C] =>> A[B, C]]): Nothing

scala> def foo[A[_,_],B](functor: Functor[ [C] =>> A[B, C]]) = ???
def foo[A[_$1, _$2], B](functor: Functor[[C] =>> A[B, C]]): Nothing

ライブラリを作る人でなければ、このような汎用性のための機能はいらないかもしれないけど。

あと、これらのコードを動かしている最中に、aliasの挙動がよく分からなくなったけど、良い記事を見つけた。

blog.shibayu36.org

夏休みのお供に『アンダースタンディング コンピュテーション』

技術書なんて必要なところを拾い読みするか、読書会でいろんな人の考え方や経験を知るきっかけに使うもの...慣れてくるとそんな感覚になってきますが、せっかくの夏休みなので初心に帰って、全部頭から再度まで読み切って、かつコードの写経もやってみて…というのをじっくりやるのもいいかもしれません。

そんな時には、できれば改めて基礎からじっくり学ぶ系の本が良いので、『アンダースタンディング コンピュテーション』などは如何でしょうか。

出版されたのは2014年ともう時間もけっこう経っていますが、「そもそもプログラミング言語って何だ?」「計算機って何だ?」「っていうか、ここで言う計算って何だ?」という問いかけに丁寧に答えてくれる1冊になっています。つまり、普段なかなか真正面から学ぶ機会のない、コンピューサイエンスで学ぶことのうちの一つですね。

特に前半の、Rubyを使ってプログラミング言語を実装する部分は単に写経するだけでも、構造が見えて学びが多いです。というか、絶対に写経した方がいいですね、わかった気になって単に読み進めていくと、「あれ、全然分からなくなった...」という瞬間がやっていきます。

中盤の、オートマトンから正規表現も、普段なにげなく使っている正規表現がどのような理論の上で構築されているのか、実際に動くプログラムで実感することができます。

後半の「第II部 計算と計算可能性」はかみごたえが有るというか、毎回自分も途中で挫折してしまうのですが、じっくり読んで理解を進めていく機会には、このくらいの難易度が欲しいですね。

というわけで、残念ながら自由に外にも出かけられない夏休み、クーラーの効いた部屋でじっくり技術書を、「考え、実行しながら」読み進めて、考えを深めるのはいかがでしょうか?

Scala3学習メモ: Intersection型

Scalaではclassの定義時にtraitをmix-inできる。

scala> trait A
// defined trait A

scala> class B extends A
// defined class B

scala> val o1 = new B
val o1: B = B@36b53f08

この時、変数の型はBと推論されている。

また、オブジェクトの生成時にもmix-inできる。この時の型は先ほどと異なる結果となる。

scala> class C
// defined class C

scala> val o2 = new C with A
val o2: C & A = anon$1@5486ee92

この時の型はScala3から導入されたIntersection型により、変数はCとAを同時に持つ型として推論されている。

変数定義だけでなく、当然引数の型の宣言でも使える。

scala> def method(obj: A & C) = println(obj.toString)
def method(obj: A & C): Unit

scala> method(o2)
repl$.rs$line$6$$anon$1@5486ee92

Intersectionには型の継承関係が整理されているので、詳しくは公式ドキュメントを見ると良い。


Scala2の時は、Compound Typeという型で推論されていた。

scala> trait A
defined trait A

scala> class C
defined class C

scala> val o = new C with A
o: C with A = $anon$1@ecd379a

もちろん明示的に指定できる。

scala> val o2: C with A = new C with A
o2: C with A = $anon$1@22c7ef94

この変数の型におけるwithを使ったCompound Typeの記法は将来は廃止される予定であるとされている。

Scala3で明示的に指定してもIntersectionで置き換えられている。

scala> val o4: A with C = new C with A
val o4: A & C = anon$1@799971ac

Scala3学習メモ: newキーワードが不要になった

case classではない、通常のクラスでもapplyメソッドを持つコンパニオンオブジェクトが自動生成されるようになったので、newキーワードを使わなくてもオブジェクト生成ができるようになった。

scala> case class Person(name: String, age: Int)
// defined case class Person

scala> Person("Mike", 42)
val res0: Person = Person(Mike,42)

scala> class Company(name: String)
// defined class Company

scala> Company("ANAHEIM ELECTRONICS")
val res1: Company = Company@3253d771

toStringメソッドが再定義されているわけではないので、オブジェクトのダンプの見え方は従来と変わらない。

programming in scala fifth edition

Scala3対応の第5版。日本ではScala 2.13対応の第4版が出たばかりですが、原著はScala3対応となりました。

以前と構成も変わった箇所も多いですが、すべてのサンプルコードがScala3のquite style(要はpythonっぽいインデントベースの記法)になっていたり、implicit系の機能が全てScala3のgivenやusing、extensionといったキーワードに置き換わってところが特徴です。

新しい機能には比較的「これはScala3からの機能です」と書かれていますが、説明なしに解説が始まっている機能も有り、「絶対にScala3のソースしか読まない!書かない!」という方には良いのですが、Scala2ベースのソースが消えてなくなる訳でもないので、とまどうかもしれないですね。


というわけで、しばらくは、Scala2に入門する方が多いと思うので、今からScalaへ入門する方は第4版の翻訳版の方をお勧めします。

言葉にすること、言葉にする前と、言葉にした後のその先のこと

そもそも解くべき課題が設定できた時点で勝ったも同然、みたいな話も有るけど、みんながみんなそんな役回りでも無いし、「課題は無限に湧いてくる」状態であることも有るのでそこは議論しないとして...

  1. 考えられる
  2. 考えたことを言葉にできる
  3. その考えに至った背景や構造を整理できる
  4. 考えたことが妥当、正当であることが説明できる

この順に難易度が高いな、と思っているけど、果たしてどうすれば次のステップに進めるようになるのか、と考えている。

と、onkさんの見事な言語化エントリを読んで思ったのでした。

onk.hatenablog.jp

Scala3学習メモ: Scalaとnull, Scala2とScala3と

Rustの公式ドキュメントにnullに関する興味深い記事("Null References: The Billion Dollar Mistake"(Null参照: 10億ドルの間違い))が引用されています。

Enumを定義する - The Rust Programming Language

nullは、インタフェースにおける双方の合意として取り得る値とするか、しないかがいつもあいまいになってしまう点が問題。ある変数の型を見てもnullを取り得るかは明示されていないにも関わらず、人によって「nullを考慮すべき」「nullは取り得ない」の二つの状態が混在してしまう。たとえ、同じ人が双方のコードを書いていたとしても。

ScalaにおけるNull型のクラス階層における位置付けと、nullの代入

https://docs.scala-lang.org/resources/images/tour/unified-types-diagram.svg

https://docs.scala-lang.org/tour/unified-types.html

Scalaにおける型の階層ではNull型はAnyRefのサブタイプになっています。そして、AnyValのサブタイプではありません。

scala> class Person(name: String, age: Int)
// defined class Person

scala> val person: Person = null
val person: Person = null

scala> val num: Int = null
1 |val num: Int = null
  |               ^^^^
  |               Found:    Null
  |               Required: Int

つまり、AnyRefを継承しているクラス型の変数にはNull型の唯一の値であるnullを代入できますが、AnyValを継承しているInt型の変数にはnullの代入はできない、ということを示します。

Option型によるnullへの対応

しかし、nullはやっかいです。参照すればすぐにNull pointer exceptionが発生します。意図してインタフェースとしてnullを取り得る設計としたとしても、受け取る側がnullに対応するコードを十分に書けないこともあるし、意図してnullは取り得ない設計としたとしても渡す側がうっかりnullを渡すことが有ります。

前者は、requireなどで引数のnullチェックを行うルールにすべきですが、残念ながら強制力がありません。

強制力を持たせるためにはインタフェースの境界を、Option型でラップすることです。

scala> case class Person(name: String, age: Int)
// defined case class Person

scala> def printPerson(person: Option[Person]): Unit =
     |   person match {
     |     case Some(p) => println(s"name:${p.name}, age: ${p.age}")
     |     case None    => println("no person")
     |   }
     |
def printPerson(person: Option[Person]): Unit

scala> val p1 = null
val p1: Null = null

scala> printPerson(Option(p1))
no person

scala> val p2 = new Person("Arthur", 42)
val p2: Person = Person(Arthur,42)

scala> printPerson(Option(p2))
name:Arthur, age: 42

Option型でラップされていることで、Some、Noneに対応するコードを必ず書くための強制力が働き、nullが有る世界と、nullが無い世界を分離されました。

(Optionのapplyにnullを渡すとNoneが生成され、null以外を渡すと、その値をラップしたSomeが生成されます)

世界は平和になりました...

いや、そうか?では全ての外部からもたらされる変数をいちいち全部Option型でラップするのか?意図しないnullの混入を防ぐためにはtoo muchではないか?

EXPLICIT NULLS

nullにおける問題は、その変数がnullを取り得るか、明示されていないことにある...インタフェースの設計として明示に書いてあればいいけど、ドキュメントに頼るのも限界があるし、内部のインタフェースでいちいち文書化もしていられないだろう。

そもそもAnyRefがnullを許容しているからいけない...ということで、Scala3からAnyRefのサブタイプへのnull代入を不可とするモードが追加になりました。

https://docs.scala-lang.org/resources/images/scala3/explicit-nulls/explicit-nulls-type-hierarchy.png

Explicit Nulls | Scala 3 Language Reference | Scala Documentation

クラス階層が変更になっています。

ただし、デフォルトで有効になるのではなく、コンパイラオプションとして-Yexplicit-nullsを設定したときだけ有効になります。

$ scala -Yexplicit-nulls
scala> case class Person(name: String, age: Int)
// defined case class Person

scala> val person: Person = null
1 |val person: Person = null
  |                     ^^^^
  |                     Found:    Null
  |                     Required: Person

scala> val num: Int = null
1 |val num: Int = null
  |               ^^^^
  |               Found:    Null
  |               Required: Int

もうnullを代入することはできません。

どうしてもNullを代入したい時は、明示的にNull型を許容することを指定します。

scala> val person: Null = null
val person: Null = null

しかし、これではNull型に固定されてしまうため、あまり役に立ちません。

Scala3から導入されたUnion型が使えます。

scala> val person: Person | Null = null
val person: Person | Null = null

こうして、「うっかり」nullが混入し、Null Pointer Exceptionが発生すること限りなく抑制することができ、かつ記述も簡潔になりました。

世界は少し平和になりました。