kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

Onion開発日誌/2016/11/05 単一式を本体として持つメソッドや関数定義を可能に

最近、Klassicと同時にOnionもちょくちょく更新するようになったので、変更点など書いていこうと思う。

今日は、メソッドや関数定義に、文ではなく式を取れるようにする改良を行った。今どきの言語だと当たり前だが、Onionを最初に開発した2004-2005年の時点ではそれほど当たり前ではなかったのだ…と思う。

というわけで、

class Adder {
public:
  def add(x: Int, y: Int): Int = x + y;
}

のような定義が可能になった。セミコロンが必須なところがださいので、この辺もじきに不要になるようにしたい(多少面倒だが)。

github.com

Scala初学者はvarを使っても良い

TwitterScala関係のつぶやきをみていると、どうも、特に初学者の方に、

varを使ってはいけないので、Scalaは難しい…

という意見が散見されるようです。

しかし、Scalaを学び始めのときにいきなりvarを断つ必要はなく安心してvarを使ってもいいと思います (そもそもvarが全く不要なら言語仕様にあるわけないですし)。慣れれば、varを明らかに使わなくて 良い箇所はわかってくるので、そこからval/varを使うのを意識しても遅くないと思います。

もちろん、いつもでも全部var使ってるとどうかという議論はあるのですが、それはScalaに慣れてから 考えればよい話です。Scalaを学ぶのにvarを使わないという縛りがきついのなら、無理しなくていいよ というのが言いたいことです。

Elixirを学んでみる(1) - parser combinator

最近、Elixirがなんとなく盛り上がってきている気がするので、この機会に入門してみることにした。お題は相変わらずparser combinator。解説はめんどうくさいのでコードだけ貼り付けておく。

わかったこと。

  • Elixirの文法は結構くせがある。文法はRubyに影響を受けたということだけど、Rubyの文法とはまた別の癖がある
  • Elixirは動的型付き言語だが、関数が定義されているか、引数の数があっているかなどがある程度静的にチェックされるので、凡ミスは意外にコンパイラが見つけてくれる。
  • f.(x) という文法はちょっと気持ち悪い。
  • defで定義したものをオブジェクト化するときに、 &f/1のように引数の数を明示しなければいけないのは、Erlang譲りっぽいけど、ちょっと面倒くさい
  • パターンマッチは普通に使いやすい
  • String.sliceの第二引数で与える範囲がJavaとかのsubstringと違ってちょっと迷った
  • mixで簡単にプロジェクトの雛形とテストの雛形を生成できて便利
  • 関数(第一級でないものの方)を呼び出すときに、fのように書くだけで呼び出せるのはRubyっぽい
  • Ruby以上になんでもかんでもendで閉じる傾向がある

文法は癖があるが、おおむねまっとうな言語だなあというのは初見の印象。

gist.github.com

Kotlinの謎(解明編)

今日書いた

kmizu.hatenablog.com

についてですが、原因がわかりました。まずは以下のコードを見てください

Welcome to Kotlin version 1.0.0 (JRE 1.8.0_91-b14)
Type :help for help, :quit for quit
>>> val a: () -> Int = { 2 }
>>> val b: () -> Unit = { 2 }
>>> println(a())
2
>>> println(b())
kotlin.Unit

変数abの右辺は共に{ 2 }になっており、これは0引数の無名関数を表しています。一方、aの型は引数なしでIntを返す関数、bの型は引数なしでUnitを返す関数となっており、型に互換性がないにも関わらず、代入することができています。

このことから、無名関数の式の型は、期待される型によって(も)決定されるのだろうということがわかります。具体的には、2行目の{ 2 }の型は期待される型が() -> Unitなので、{ 2 }自体が() -> Unit型になり、そこから、

{ -> 2; Unit }

のようにUnitが補われているのだろうと思います。

F.kt:3:25: warning: the expression is unused
  val b: () -> Unit = { 2 }
                        ^

という警告が出る辺りもそれを裏付けていると言えそうです。

Kotlinの謎

Kotlinという言語の型システムは凄くおおざっぱに言ってしまうと、Scalaと非常によく似ています。

  • TopがAny
  • BottomがNothing
  • Byte, Short, Int, Float, ....といった、プリミティブ型相当の型が継承階層に組み込まれていて、型名まで同じ
  • Generics
  • Declaration-site variance
  • Use-site variance

違う部分、たとえばnullabilityを型システムレベルでサポートしてるとかはもちろんあるのですが、AnyとNothingという名前まで真似してる辺り、非常にScalaの影響が大きいのは間違いありません。

一方で、Kotlinがあえて採用しなかったものの一つに、暗黙の型変換があります。たとえば、Scalaでは、任意の型はUnitに暗黙に型変換できます。そのため、たとえば

val x: Unit = 1

というコードはコンパイル可能です。一方で、Kotlinは

val x: Unit = 1

というコードはコンパイルを通りません。これはまあ設計判断としてはどっちもありだと思います。

さて、それはいいのですが、一つ妙な点があります。次のプログラムを見てください:

gist.github.com

runは引数として渡された0引数無名関数を呼び出し、その最後の評価値を返すメソッドです。ここで、run<Unit> { 1 + 1 }とすることで、1 + 1の結果を強制的にUnitに変換しようとしています。普通に考えると、IntからUnit への暗黙の型変換が駄目なのだからコンパイルも通らなそうなのですが、何故かコンパイルを通ってしまいます。

これはKotlinが暗黙の型変換をサポートしていないという事実と反しているようにも思える。あるいは、実装のバグなのかもしれません。というわけで、Kotlinに詳しい方、何故これがコンパイルを通るか(どういうコードが生成されるかはわかっているので、仕様的に何故通るべきなのか)教えていただけないでしょうか(教えてクン)

Macro PEG 0.0.10 リリース

かなり久しぶりのリリースです。主な変更点は、

Conflict between an expression parenthesis syntax and call syntax · Issue #16 · kmizu/macro_peg · GitHub

を修正した点でしょうか。これで、

STRING = STRING_MACRO("\"") / STRING_MACRO("'");
STRING_MACRO(Q) = Q (!(Q / "\\") . / "\\" .)* Q;

のようなPEGにおいて、Q (!(Q / "\\") が正しく Q(!(Q / "\\") の連接として解釈されます(今までは Qの呼び出しと解釈されていた)。修正方法としては、マクロ名につづいてスペースが続かずに引数の括弧が続く場合はマクロ呼出し、スペースが挟まれている場合は連接として解釈するようにしました。当初、グルーピングは[]という別の記号を使おうと思いましたが、記述として気持ち悪いので、現状のシンタックスをあまり変えないようにしました。

Scalaの学習コストを下げるための心得

追記Twitterで、「それって、言語マニアにしかできない技のような気が」という指摘を受けました。自分としては一般的に適用可能な話だと思っていますが、あるいは自分の感性が著しくずれているのかもしれません。その辺承知の上でお読みください。

Scalaは習得が難しい言語だ、とよく言われます。また、実際問題として、Scalaの言語仕様の全体はそれなりに複雑でもあります。しかし、それはたとえばJavaでも言語仕様の全体像を把握するのは難しい話であり、Scalaに限った話ではありません。にも関わらず、Scalaの習得が難しいとよく言われるのはプログラミング言語の学習モデルが誤っているからではないかと最近思うようになりました。そこで、Scala(や他の言語も含めて)のコストを下げるために必要な心得についてちょっと書いてみます。

これは、Scala関数型プログラミング言語である、という主張と相反するように見えますが、オブジェクト指向関数型プログラミングは特に対立するものではありません。一方で、Scalaの核となる部分は、従来のオブジェクト指向言語(たとえばJava)の発展系であり、Scheme/ML/Haskellといったラムダ計算をベースとする言語とは明らかに異なります。Scalaは核となるオブジェクト指向言語関数型プログラミング言語から輸入した機能を取り込んだ言語と見るべきです。ですから、Scalaを使う際にいきなりパラダイム・シフトする必要はありません。必要になったときに理解すればいいのです。

  • 文法の一般形と省略形を区別する

たとえば、Web上にあるScalaのメソッドの文法解説として、

def メソッド名(引数: 引数の型, ...): 返り値の型 = { //返り値の型は省略可能
  return hoge // return は省略可能
}  // 単一行の場合は{}を省略可能

のようなものを見ますが、これは誤りです。正しくは、

def メソッド名(引数: 引数の型, ...): 返り値の型 = 式 // 返り値の型は省略可能
// { } は要素を順に評価し、最後の要素の評価値を返す式

です。この二つで何が違うかというと、前者は本来省略形でないものも省略形と認識してしまうことによって、余計なことを覚える必要があるという点です。

また、クラス継承において、継承元が一個の場合はextends、二個以上の場合はそれに加えて、withが必要という解説もありますが、これも厳密には正しくありません

class A extends (Super1 [with ....])

が正しい一般形です。Scalaの文法は、一般形を正しく認識すればそれほど複雑ではない部分が多いのですが(プレースホルダ構文は例外的に複雑です)、省略形と一般形とを誤って認識すると途端に覚えることが増えてしまいます。

  • 式を基本として考える

Javaなどから来るとやや異質に感じるかもしれませんが、Scalaでは制御構造も含めてほとんどの「いわゆる文」が式です。式は評価すると必ず(例外が投げられない限り)何らかの値を返します。

たとえば、

if(1 < 3) {
  println("1 < 3")
}

と書いたとき、この式の返す値の型はUnitです。また、

a match {
  case 1 => "1"
  case 2 => "2"
  case 3 => "3"
  case _ => "otherwise"
}

という式が返す値の型はStringです。そして、式の評価値は変数に代入できるので、当然のことながら(match式に型が付く場合)

val b = a match {
  case 1 => "1"
  case 2 => "2"
  case 3 => "3"
  case _ => "otherwise"
}

は基本的にコンパイルを通ります(例外はありますが)。

  • 「関数」と「メソッド」を区別する

これは、コップ本とかScala言語仕様ですら正しく用語を使い分けしていないのが悪いのですが、Scalaにおいて

  • 関数は第一級の値である
  • メソッドは第一級の値でない

という根本的な違いがあります。そして、あるものが関数かどうかを判定するのはとても簡単で、ぶっちゃけていうと、defを使って定義されたもののみがメソッドで、それ以外が関数です。

  • Implicit Conversionは本質的な機能ではない

若干語弊があるのですが、そもそも機能としてはImplicit Parameter(によって実現される機能)が本質的であり、Implicit Conversionはその特殊な形です。より正確には、A => Bという型のImplicit Parameterがあったときに、これはA型からB型へ暗黙に変換ができるとみなしてコンパイラがコードを挿入するというだけの話です。Implicit Conversion怖いという意見をよく見るますが、そもそもそれは本質的なものではないのだ、ということです。

考えながら項目を継ぎ足していったのでイマイチ整理しきれていませんが、まとめとしては、核となる機能と枝葉の機能を区別せよ。そして、枝葉の機能は可能な限り核となる機能への変換としてとらえよ(そうすれば覚えるべきことを最小化できる)、ということになるかと思います。このアドバイスはScalaだけでなく新しい言語を高速に覚える際に役立つと私は思っています。

自分流のプログラミング言語学習法をまとめた感じなので、異論があるかもしれませんが、「個々の機能を丸暗記する」学習法は効率が悪いのだ、ということは強く言いたいです。