Help us understand the problem. What is going on with this article?

Facebookの新言語Skipの構文を雑に見てみた(1)

More than 1 year has passed since last update.

つい先日、Facebookが新しいプログラミングSkipの処理系をOSSとして公開しました。Skipの特徴として、公式ページでは

  1. Caching with Reactive Invalidation
  2. Safe Parallelism
  3. Efficient and Predictable GC
  4. Hybrid Functional/Object-Oriented Language
  5. Great Developer Experience
  6. Built by a Team of Veterans

の6つが挙げられています。このうち全てを詳細に調べるのは面倒なので、今回は、4.の「Hybrid Functional/Object-Oriented Language」の部分についてのみ調べてみることにしました。結論からいうと、Skipの言語仕様は、特に構文においてはScalaやKotlinの影響が色濃く、セマンティクスもそれらの言語の影響を大きく受けていると言えそうだと感じました。以下、Skipの構文について述べていきます。なお、コード例はDocsのページのコード例をだいたい元にしており、コードがそのまま動作するか確認してはいないことに注意してください。Playgroundで簡単にコードを実行してみることはできるのですが、mainメソッドがないと実行ができないのでややめんどくさいです。また、非常に短いコードでもコンパイルが遅いです。

関数定義

fun add(x: Int, y: Int): Int {
  x + y
}

キーワードに fun を使い、返り値の型の後に中括弧が来て本体が来る辺りは、Kotlinの文法の影響があるようです。ちょっと違うのは、Kotlinでは非ラムダ式の場合 return キーワードが必須なのに対してSkipでは必要ないところでしょうか。

変数宣言と更新

fun add(x: Int, y: Int): Int {
  z = y;
  !z = 1;
  x + z
}

ここで面白いのは、レキシカルな最初の代入が変数の宣言を兼ねているところです。動的型付き言語ではよくありますが、静的型付き言語では比較的珍しい仕様だと感じます。拙作のOnionでもこの方式を採用していましたが、変数の宣言が目立たなくなるという欠点はあるように感じます。一方で、単なる代入に見える断片を宣言と解釈できるのは、変数を更新する場合は !y = 1; のように、先頭に!を付けなければいけないという制約があるためではないかと考えています。

変数の型宣言

変数の型を明示的に指定することもできます。

fun add(x: Int, y:Int): Int {
  y: Int = 0; // yはInt
  !z = 1;
  x + z
}

Skipの型推論はさほど強くないように見えますし、明示的に型を割り当てる構文は必要そうです。

void型

C系列の void と違って、ちゃんと唯一の値 void を持っています。たとえば、以下のプログラムは正しいです。

fun notVeryUseful(): void {
  x = void;
  x
}

もはやC系言語の意味での void とは違っているので、単に Unit にすればいいのにと思うのですが、何故旧来の void という語を引きずったのでしょうか。

制御構文:

whileifbreak といったおなじみのものがそろっています。

fun getAgeWhile(name: String, people: Sequence<Person>): Int {
  iter = people.values();
  current = iter.next();
  while (current.isSome()) {
    person = current.fromSome();
    if (person.name == name) {
      break person.age
    }
    !current = iter.next();
  } else -1 // Return -1 if the person is not found.
}

無名関数(クロージャ):

最近の言語ではお馴染みです。Skipでは、仮引数に -> をつけることで、無名関数(クロージャ)を作る
ことができます。

fun sumVector(v: Vector<Int>): Int {
  sum = 0;
  v.each(x -> !sum = sum + x);
  sum
}

左辺値

ここはやや特殊ですが、どうやらSkipではある変数を左辺値として参照する場合は、!x のように、変数名の前に、プレフィクス!を付ける必要があるようです。これが、あとで副作用をトラッキングすることに使うのかも
しれません。

クラス定義とメソッド

以下を見るとわかりますが、Scala/Kotlinと近い方式を採用しています。クラス名の後にクラスパラメータが来る方式です。

class Point(x: Int, y: Int) {
  fun add1X(): Point {
    Point(this.x + 1, this.y)
  }
}

オブジェクトの新規生成のためにnew Point()でなく、Point(...)を使うのはKotlinの影響でしょうか。

名前付き引数

これまでの構文はScala/Kotlinの影響が大きく見られましたが、名前付き引数はちょっと違った構文を採用
しています。

class Person {name: String, age: Int} 

fun main(): void {
  p = Person {name => "Kota Mizushima", age => 34};
  print_raw(`p.age = ${p.age}`)
}

クラス名の後に、 {} で囲んだ仮引数列を与えることで、インスタンス生成時に引数名を指定することができる
ようです。メソッドの引数名が自動的にAPIの一部になってしまう設計よりは、このように、名前付き引数を指定
できる形式にする方が事故を起こす可能性が減ってよさそうです。

代数的データ型

Skipのドキュメントでは、ツリーを作るのに冗長な構文を避けるためにあるように書かれていますが、後の例を
見ると、これは代数的データ型をクラスでエンコードする方式を取りつつ、冗長な記述を避けるための構文
のように見えます。

base class Parent<T> {
  children = A() | B(Int) | C(Int, Bool)
}

このように書いたものが実際には

base class Parent<T>
class A<T>() extends Parent<T>
class B<T>(Int) extends Parent<T>
class C<T>(Int, Bool) extends Parent<T>

と等価であると説明されているわけですが、これはまさしく代数的データ型のためのシンタックスシュガーに他ならないように見えます(後で出てくるパターンマッチの話を合わせて考えると)。

ジェネリクス

Scala/Kotlinのジェネリクスとだいたい同じです。

fun identity<T>(x: T): T { x }
class MyReference<T>(T)

Kotlinのそれとちょっと違う点として、型パラメータの宣言位置が関数名の後に来ている点で、これは、型パラメータの適用との構文的整合性を考えると改良といっていいと思います。

ジェネリクスの制約

Javaでいう<T extends IntConvertible>、Scalaでいう[T <: IntConvertible]、Kotlinでいう<T : IntConvertible> と同じで、特筆すべきところは特にありません。

fun sum<T: IntConvertible>(v: Vector<T>): ...

トレイト

最近、トレイトと称する機能を持った言語がいくつもあって混乱するのですが、Skipのトレイトは、実装すべきメソッドのシグニチャを規定するものであっても、それ自体は型になりえないようです。ドキュメントには、

trait Showable {
  fun toString(): String;
}

というトレイト Showable があったとき、Showableという型は存在していないと述べられています。では、これがあることによってどのような問題が解決されるかについては、ドキュメントのトレイトのページでは明確に述べられていないため、とりあえずおいておきます(たぶん、バイナリメソッドを定義するときに役立つのではないかと想像しますが)。

静的メソッド

ここまで、割とScala/Kotlinに近い方向だったのですが、シングルトンオブジェクトに関数的なメソッドを入れるという方向は選ばなかったようで、JavaやC#の静的メソッドと同様のものが普通に使えるようです。

class Math {
  static fun plus1(x: Int): Int {
    x + 1
  }
}

fun testMath(): Int {
  Math::plus1(0)
}

ここでは Math クラスの静的メソッド plus1testMath から呼び出しています。

拡張クラス

最近のオブジェクト指向言語では割とよく見かけるようになった機能の一つですが、既存のクラスに対してメソッドを追加したように見せかけることができます。

trait Hashable {
  fun hash(): Int;
}

extension class String uses Hashable {
  fun hash(): Int {
    ...
  }
}

ちょっと面白いのは、extention classに続いて、拡張対象のクラス名を指定するだけでなく、拡張に使うトレイトを指定している点です。この uses が必須なのか気になったのでPlaygroundで試してみたところ、別に書かなくてもコンパイルは通ったので、拡張メソッドとしてこのトレイトにあるメソッド群を実装してほしい、というときに使うのでしょうか(要調査)。

遅延メソッド

親クラスでは実際の型がわからないが、親クラスではとりあえず実装を書いておいて、子クラスで型を与える、といったときに使うようです。

base class MyValue {value: this::TV} {
  type TV;

  deferred static fun make(value: TV): this {
    static { value }
  }
}
class Child extends MyValue {
  type TV = Int;
  // inherited
  // static fun make(value: Int): this
}

ここで、Scalaの型メンバらしきものがどういう機能なのかが気になるところですが、MyValue クラスの makeメソッドでは、実際の型が決まっていない TV 型を返すということだけが決まっているのですが、 Child クラスで TV = Int とされることで、 make の実際の型が決まるようです。type memberらしき機能の使い方といい、Scalaの影響が濃く感じられるところです。

値クラス

これはScala等にもある機能なのですが、なるべくヒープ割り当てを行わないようにする機能のようです。

value class Pair(x: Int, y: Int)
  • タプル
my_pair = (1, 2);

my_pair = Tuple2(1, 2);

のシンタックスシュガーであるというのはまんまScalaであり、Scalaの影響が大きいようです。

可変性

これまで見てきた部分は、おおむねScala/Kotlinから継承した部分であり、あまり面白みのある部分ではありませんでしたが、可変性に関するアプローチは異なっています。Mutabilityのページに書いてあるのはあくまで概要なので、完全に正しいかは自信がないですが、ある型Tがあったとき、mutable Treadonly TTは互換性がないようです。たとえば、仮にクラスの状態を変更するメソッドがあったとしても、(immutableな)Tに対して状態をを変更するメソッドを呼び出すことはできないようです。また、mutable Treadonly T型になった場合は、該当クラスの readonly メソッドしか呼び出すことができないようです。

この辺はおそらくSkipのキモでもある部分なので、後日もうちょっとちゃんと調べてみたいところです。

その他、

  • パターンマッチ
  • nullability(あるいはOption)
  • 文字列補間
  • マクロ

など色々ありますが、あまり特筆すべきものが感じられなかったので、省略します。

まとめ

ざっとSkipの構文について眺めてみました。構文についてはScala/Kotlinのものをある程度受け継ぎつつ、可変性に関するアプローチが多少変わっている、といったところでしょうか。あまり新鮮さの感じられない言語ではありますが、そこのところには元々さほど力を入れていないのでは、という気がしました。

ちなみに、今回やたらScala/Kotlinの影響について書きましたが、実際には他の言語の影響も多々あるかと思います。ただ、構文的にはScala/Kotlinの影響がだいぶ大きいという印象です。

ただ、今回、可変性に関するアプローチはほんのちょっとしか調べられていなかったので、これについて深堀りをすることで印象が変わるかもしれません。次回は、可変性に関する型システム上の扱いについて詳しく調べてみたいと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした