kmizuの日記

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

新言語Klassic作り始めました

まあタイトルの通りなんですが、とりあえずこれまでの自分の言語作成遍歴についても触れておきます。

まあ、こんな感じです(他にも色々作ったtoy言語があるのですが略)、自分が作った中で一番規模が大きいのがOnion(だいたいJava 1.4くらいまでの仕様を一通り持っていて、バイトコードを生成できる)、ついでYappという感じです。ただ、いずれもProof of Concept的な色彩が強いものであり、実用まで持っていけてないあたりが自分の飽きっぽさを示しているなあという感じです。

ただ、プログラミング言語作成をライフワーク(?)としてる自分としては、そろそろ実用に使えるプログラミング言語を作らねばということで重い腰を上げることにしました。

新言語の名前はKlassicで、オブジェクト指向関数型言語(ただし、両者のハイブリッドではなく統合したものとう意味)でかつ、メタプログラミングの機能が強力、構文にこだわる、くらいしか決めていませんが、今後がんがん開発するつもりです。

スターがあれば開発の励みにもなるので、もしよろしければお願いします(ぺこり)

github.com

何故ClassicでなくKlassicかというと、ぐぐらびりてぃを考慮してのことです。ちなみに、Matzさんが昔卒論で作った言語の名前はClassicだったそうな…。

ScalaでMLスタイルのモジュールを使ったプログラミングをする

何はともあれ以下のコードを見てください(ちなみに複素数クラスの実装は、

d.hatena.ne.jp

を参考にさせていただきました):

trait Complex {
  type T

  def re(a: T): Double

  def im(a: T): Double

  def make(re: Double): T

  def plus(a: T, b: T): T

  def minus(a: T, b: T): T

  def multiply(a: T, b: T): T

  def divide(a: T, b: T): T
}

object Complex extends Complex {
  case class C(re: Double, im: Double)
  type T = C

  def re(a: T): Double = a.re

  def im(a: T): Double = a.im

  def make(re: Double): T = C(re, 0.0)

  def plus(a: T, b: T): T = C(a.re + b.re, a.im + b.im)

  def minus(a: T, b: T): T = C(a.re - b.re, a.im - b.im)

  def multiply(a: T, b: T): T = C(a.re * b.re - a.im * b.im, a.im * b.re + a.re * b.im)

  def divide(a: T, b: T): T = {
    require(b.re != 0.0 || b.im != 0)
    val x = Math.pow(b.re, 2) + Math.pow(b.im, 2)
    C((a.re * b.re + a.im * b.im) / x, (a.im * b.re - a.re * b.im) / x)
  }
}

基本的に、

  • MLのsignature=Scalaのtrait
  • MLのstructure=Scalaのobject
  • MLのfunctor=Scalaのclass

ととらえれば、それなりにMLスタイルのモジュールをまねることができます(というのは、Scala関係の発表資料かどこかで読んだのですが思い出せない)。これは特に、Scalaが抽象型メンバを持っていることに寄っています。なお、「それなりに」と書いた通り、includeとかそのままでは真似られないものが色々あります。

追記:上のコードだと、Complex.Cでcase classの実装が参照できちゃうので、実際には

trait Complex {
  type T

  def re(a: T): Double

  def im(a: T): Double

  def make(re: Double, im: Double): T

  def plus(a: T, b: T): T

  def minus(a: T, b: T): T

  def multiply(a: T, b: T): T

  def divide(a: T, b: T): T
}

object Complex {
  val C: Complex = new Complex {
    case class C(re: Double, im: Double)
    type T = C

    def re(a: T): Double = a.re

    def im(a: T): Double = a.im

    def make(re: Double, im: Double): T = C(re, im)

    def plus(a: T, b: T): T = C(a.re + b.re, a.im + b.im)

    def minus(a: T, b: T): T = C(a.re - b.re, a.im - b.im)

    def multiply(a: T, b: T): T = C(a.re * b.re - a.im * b.im, a.im * b.re + a.re * b.im)

    def divide(a: T, b: T): T = {
      require(b.re != 0.0 || b.im != 0)
      val x = Math.pow(b.re, 2) + Math.pow(b.im, 2)
      C((a.re * b.re + a.im * b.im) / x, (a.im * b.re - a.re * b.im) / x)
    }
  }
}
object Main {
  import Complex.C
  def main(args: Array[String]): Unit = {
    val x = C.make(1.0, 1.0)
    val y = C.make(2.0, 2.0)
    println(C.plus(x, y))
  }
}

のように書くべきですね。こうすれば、抽象型メンバTがcase class Cを完全に隠蔽してくれます。

IOと例外の取り扱いについて:どっちが良いスタイル?

追記:Java 7以降(つまり、現在)はtry-with-resources構文があるので、それを使えばよいです。ここでは、Java 6かそれ以前のコーディングスタイルについて主に言っています。

kmizu.hatenablog.com

におけるkrxrossさんのコメント

java脳だと、「fw = new PrintWriter(new File(file))」で例外が発生したら、fwが不定のまま、finally句の「fw != null」で困ってしまう。 そうならないために、nullで初期化するという話に見えます。 多分JAVAだと、nullを代入しないとコンパイルエラーになるか、eclipsが警告を出したと思います。 scalaでも同じではないですか ? 良く解っていない超初心者より

を見て、強烈に不安になってしまったのですが、Javaだとこういうのが良いスタイルなんでしたっけ?

//Style (A)
package ex.callbyname
import java.io.File
import java.io.FileWriter
def fwloop (file: String, cond: => Boolean, update: => Unit)(body: => String) {
  var fw: FileWriter = null
  try {
    fw = new PrintWriter(new File(file))
    while (cond) {
      fw.write(body)
      update
    }
  } catch {
    case e: Exception => println("Error: " + e.getMessage())
  } finally {
    if (fw != null) {
      fw.close()
    }
  }
}

いや、このスタイル、Javaで正直結構見たことあるのですが、ファイルオープンの失敗と、ファイル書き込み中の失敗という意味の異なる例外を同じ階層で扱うという点であまりよくないというか、ファイルオープンの失敗はリカバー可能な場合が多いが、ファイル書き込み中の例外は外に投げっぱなしにすることしかできないことが多いという点で非常によくないスタイルだと思うのですがいかがでしょうか?

自分なら、このコードは、

//Style (B)
package ex.callbyname
import java.io.File
import java.io.FileWriter
def fwloop (file: String, cond: => Boolean, update: => Unit)(body: => String) {
  try {
    val fw = new PrintWriter(new File(file))
    try {
      while (cond) {
        fw.write(body)
        update
      }
    } catch {
      case e: Exception => println("Error In IO: " + e.getMessage())
    } finally {
      fw.close()
    }
  } catch {
    case e: Exception => println("Error in Open: " + e.getMesssage())
  }
}

のように書きます。この方が、異なる階層の例外を別々に処理できるうえに、ブロック付きopenのようなものを書きたい場合にもスタイルを変えずに済むからです:

import java.io.File
import java.io.FileWriter
def openForWrite(file: String)(body: PrintWriter => A): A = {
  val fw = new PrintWriter(new File(file))
  try {
    body(fw)
  } finally {
    fw.close()
  }
}

皆さんはどう思われるでしょうか?

Crystalの型システムで遊んでみた

この記事はCrystal Advent Calendar 2015 23日目の記事です。

まず最初にお断りしておきますが、私はCrystal言語に関しては初心者もいいところです。じゃあなぜCrystal Advent Calendarにわざわざ参加したかというと、Crystalは実用言語としてはやや特殊な静的型システムをもっており、静的型スキーの自分にとって面白くうつったからです。なお、Crystalの型システムについて正確に知りたい場合, Crystalの型とメソッドについてのドキュメントの方がよほど参考になるかと思います。

typeof(X)

Crystalにはtypeof式というものがあり、引数に与えた式の型をClass型のオブジェクトとして返してくれます。この記事では、Crystalの色々な式をtypeofに渡すことで、Crystalの型システムを探検してみたいと思います。

全ての値はオブジェクト(オブジェクトにのみ型がつく)

CrystalではRubyと同様、全ての値は(オブジェクト指向言語における)オブジェクトです。そして、オブジェクトにのみ型がつきます。これは、実用オブジェクト指向言語ではかなり珍しい型システムだと思います。

たとえば、Array#mapメソッドについて考えてみます。普通の静的型付きオブジェクト指向言語では、mapメソッドにおおむね次のような型を割り当てることでしょう(なお、配列の要素型についてはT型が割り当てられているものとします):

map: forall U. (T -> U) -> Array(U)

これは、型引数Uに対して、Tを受け取ってUを返す関数(Rubyでいうブロック)を渡すと、要素型がUArrayが返ってくるということを意味しています。細かい型の表記法の違いを除けば、Scalaでも

http://www.scala-lang.org/api/2.11.7/index.html#scala.collection.immutable.List@mapB:List[B])

Javaでも

C#でも

https://msdn.microsoft.com/ja-jp/library/bb548891(v=vs.110).aspx

基本的な部分はだいたい同じです(これらの言語ではいずれもメソッドを値として取り出すことはできませんが)。

しかし、Crystalではmapメソッドの定義の時点では型が付きません。では、どこで型が付くかというと、mapメソッド呼び出した結果に対して初めて型が付くのです。呼び出した結果に対して型がつくというのが重要です。

試しに次のようなCrystalプログラムを実行してみましょう。

def a_method(obj)
  if false
    obj + obj
  end
  obj
end
puts typeof(a_method(1))
puts typeof(a_method(1.0))
puts typeof(a_method("Hoge"))
#puts typeof(a_method(true))

a_methodでは引数として受け取ったオブジェクトの型を出力だけして、その他には何も行いません(obj + objは決して実行されない)。このプログラムを実行すると、

Int32
Float64
String

と表示されます。これは、a_methodが引数の型ごとにインスタンス化されていることを示しています。

一方、最後の行のコメントを外すと、次のような型エラーが出ます

Error in ./hoge.cr:10: instantiating 'a_method(Bool)'

puts typeof(a_method(true))
            ^~~~~~~~

in ./hoge.cr:3: undefined method '+' for Bool

    obj + obj
        ^

================================================================================

Bool trace:

  ./hoge.cr:1

    def a_method(obj)
                 ^

ポイントは、a_method(true)の呼び出し先のフローまで探索し、Bool + Boolのようなメソッドが存在しない事がわかってからコンパイルエラーにしている点です。これは、Crystalがプログラムのグローバルな解析を行っているからこそ可能になっています(なお、if false ...endで囲んであるのは、実行時エラーでないことを示すためです)。

ところで、プログラムのグローバルな解析を前提にしたこのような型システム、分割コンパイルを言語仕様レベルで妨げている気がするのですが、プログラムの規模が大きくなったとき、果たして大丈夫なのでしょうか...。

Union型

Crystalは、制御フローによって結果の型が変わるようなプログラムも扱うことができます。たとえば、次のようなCrystalプログラム

a = nil
puts typeof(a)
if true
  a = "r < 5"
  puts typeof(a)
else
  a = 1
  puts typeof(a)
end

puts typeof(a)

は、次のような結果を表示します。

Nil
String
(String | Int32)

最初のputsでは、aは必ずnilに初期化されるのでNil型に、その次のputs(ifの条件式がtrueなので必ずこちらが実行される)では、文字列が代入されるのでString、最後のputsでは、if式の実行結果次第で型が変わるので(elseが決して実行されないのはこのケースでは自明だが、一般には実行時にならないとわからない)、StringまたはInt32のどちらかの型をあらわすUnion型String | Int32になります。

このように、Crystalでは制御フローに依存した型をUnion型を用いて表現することができます。

型注釈

Crystalではメソッドの引数および返り値の型を書かなくても可能な限り頑張って推論してくれますが、その結果、わけのわからないコンパイルエラーのトレースに悩まされる可能性もあります。Crystalでは引数および返り値の型を明記することmもできます。

例えば、次のようなプログラム

def plus(a : Number, b : Number) : Number
  a + b
end
plus(1, 2)
plus(true, false)

は次のようなコンパイルエラーになります。

Error in ./hoge.cr:5: no overload matches 'plus' with types Bool, Bool
Overloads are:
 - plus(a : Number, b : Number)

plus(true, false)
^~~~

plusメソッド(Number, Number)を受け取って、Numberを返すメソッドであることを明記したことによって、plusの呼び出し先でコンパイルエラーになるのを防げています。

ちなみに、型注釈を外すと次のようなコンパイルエラーになります。

Error in ./hoge.cr:5: instantiating 'plus(Bool, Bool)'

plus(true, false)
^~~~

in ./hoge.cr:2: undefined method '+' for Bool

  a + b
    ^

================================================================================

Bool trace:

  ./hoge.cr:1

    def plus(a, b)
             ^

plusメソッドの呼び出し先まで表示されるので型エラーの原因がわかりにくくなっています。

共変と反変

Crystalでは配列はジェネリックな型を持ちます。たとえば、

["A", "B", "C"]

の型はArray(String)型になります。さて、ここで気になるのは、ジェネリックな型同士の関係は一体どうなるのかということです。まずは、共変の場合についてです。

def covariant(arr : Array(Object))
  # arr[0] = 1
  puts typeof(arr)
end
arr = ["A", "B", "C"]
puts typeof(arr)
covariant(arr)

メソッドcovariantは引数としてArray(Object)を受け取りますが、それに対してArray(String)を渡そうとしています。結果は次のようになります。

Array(String)
Array(String)

コンパイルが通ったので、配列の要素型は共変な型になっているのかとも思いましたが、メソッド内部でも型がArray(String)のままなので、どうもそうではなく型引数のObjectが単純に無視されているようです。念のため、反変型引数の場合についても試してみましたが、やはり型注釈がスルーされているようです:

def contravariant(arr : Array(Object)) : Array(String)
  arr
end

contravariant(["A", "B", "C"])

この点について、Crystalのドキュメントを読んでも納得の行く解釈が得られなかったので、識者の方には、この挙動の意味について教えていただけると助かります。

停止しない式の型?

Crystalの型チェッカはどうやら、制御フローも見て型を決定しているようなので、こういう式はちゃんと型がつくのか?というので試してみました:

def f(exp)
  if exp.is_a?(Array(Int32))
    f(Array(String).new(1))
  elsif exp.is_a?(Array(String))
    f(Array(Float32).new(1))
  elsif exp.is_a?(Array(Float32))
    f([1])
  else
    1
  end
end
puts typeof(f([1]))

このプログラム、fを実際に呼び出すと無限に再帰してしまいますが、typeofの結果は…

Int32

てっきり型チェッカが停止しなくなるかと期待(?)したのですが、ちゃんと停止した上で、何故かInt32が推論されました。ちなみに、

def f(exp)
  if exp.is_a?(Array(Int32))
    f(Array(String).new(1))
  elsif exp.is_a?(Array(String))
    f(Array(Float32).new(1))
  elsif exp.is_a?(Array(Float32))
    f([1])
  else
    f(1)
  end
end
puts typeof(f([1]))

のようにすると、評価結果が返ってこない式の型をあらわすと思われるNoReturnになります。

Crystalには他にもtypeof使った魔術の例が載っているのですが、型推論アルゴリズムがいまいち判然としませんね…。

ともあれ、Crystalはプログラムのグローバル解析を利用した、凄まじく柔軟な型付けを行っていることが確認できました。今後、Crystalを実用で使うことがあるかはわかりませんが、玩具としては丁度いいのではないかと思えてきました(^^:

Scala雑談会やります(2016年1~2月予定)

詳細な時期は未定(来年初頭予定)ですが、Scala雑談会なるものをやろうと企画しています。Scalaの勉強会といえば、160回超の開催を続けているrpscalaが有名ですが、今回やろうとしているのは「勉強会」ではなく「雑談会」です。これの元ネタは、5年ほど前まで毎年冬に細々とやっていた「言語雑談会」というイベントで、プログラミング言語好きな同好の士が集まってプログラミング言語に関係あったりなかったりすることをひたすらダベるだけというものです。

Scala雑談会もそれにならって、Scalaに関係あったりなかったりすることをだらだらダベったり、もくもくとコーディングしたり、何か発表したい人が発表したり、そんなひと時の癒し(?)を提供できればと思っています。当初、副題に、Scala老人会と付けていたために、一見さんお断りみたいな雰囲気にしてしまいましたが、自分としては特にそんな気持ちはなく、Scala初学者の方でもなんとなく楽しんでもらえればと思います(濃い話をする人がいたらスルーしてください)。

夕食もピザなどを注文して会場でだらだらとやれればと思っています。というわけで、そのようなことが可能な会場も募集しておりますのでよろしくお願いします。

なお、言語雑談会の雰囲気については、ぐぐってもらえれば私のブログやはまじさんの日記やkinabaさんの日記などがヒットすると思うので参考にしてください。

OpenJDK javacにみるMartin Odersky氏の痕跡

Martin Odersky氏と言えば、Scalaの開発者として有名ですが、そんな彼がJDK 1.3以降のjavacの作者でもある(ただし、その時点ではジェネリクスはオフであった)ことは知る人ぞ知る話になっています。

さて、ということは、OpenJDKのjavacのソースコードにOdersky先生の痕跡が残されているのではないかと思い、ちょっと調べてみたところ、テストコードにもろそのものがありました。

jdk8u/jdk8u/langtools: 84061bd68019 /test/tools/javac/generics/odersky/

ディレクトリ名がoderskyとかマンマですね。ジェネリクスのテストに痕跡が残っているというのも、いかにもOdersky先生らしいところです。

特にオチはありません。

『Scalaファンクショナルデザイン ―関数型プログラミングの設計と理解』の雑感

表題の書籍について、出たとき(2015/5/29)に買って随分放置していたのだが、最近、一通り読んでみたので簡単な感想を書いてみようと思う。

結論からいうと、Scalaについて特に使う予定はないがおおまかにどんな言語か知っておきたいという方にはそこまで悪くない本だが、副題の「関数型プログラミングの設計と理解」について本書で学ぶのはオススメできない。そういう人には、Scala関数型デザイン&プログラミング ―Scalazコントリビューターによる関数型徹底ガイドをオススメしておこう。こっちの本も、ScalaHaskellスタイルのFPをすることにこだわり過ぎて、Scala Wayからは外れていると感じる部分もあるのだが、少なくとも関数型プログラミングについて丁寧に取り組んでいるとはいえる。

本書を読む前に気になっていたのは、技術的な点での瑕疵がどの程度あるかということだったが、この点に関しては本書はまずまず合格点を上げられる出来と言っていい。Scalaの高度な機能についてあまり触れられていないという点に関しては不満が残るが、それは本書が目的としたところではなかろう。

技術的な正確さに関して気をつけていると思われることに関しては、たとえば以下のような記述(p.29)に現れている:

Scalaでは、メソッドはdefキーワードで定義されたものになります。また、メソッド以外のものを関数とし、この場合の関数は第一級オブジェクト(first-class object)としての性質を持つ関数、すなわち第一級関数(first-class function)と呼ばれるものです。

メソッドはdefキーワードで定義されたものになるというのは、自分がScalaにおけるメソッドと関数の区別として口を酸っぱくして言っていることなのだが、その辺りについてきちんと調べていることが伺える(偉そうだ)。

なお、本書で振れられていないScalaの重要な機能には以下のようなものがある:

  • lazy val
  • implicit conversion
  • implicit parameter(型クラスとしての用法を含む)
  • Generics(およびHigher-kind Generics)

次に、「関数型プログラミングの設計と理解」について、本書がどのようなアプローチをしているかというと…はっきり言ってウゲゲと思ってしまうものなのだ。一例を挙げよう。

本書では、名前渡しの機能を使って、制御構造を自作する例が多く取り上げられているのだが、その中で、ファイル書き込みの制御構造と題して次のようなメソッドfwloopを定義している。

package ex.callbyname
import java.io.File
import java.io.FileWriter
def fwloop (file: String, cond: => Boolean, update: => Unit)(body: => String) {
  var fw: FileWriter = null
  try {
    fw = new PrintWriter(new File(file))
    while (cond) {
      fw.write(body)
      update
    }
  } catch {
    case e: Exception => println("Error: " + e.getMessage())
  } finally {
    if (fw != null) {
      fw.close()
    }
  }
}

fwに初期値nullを代入してるがこれそもそも不要なんじゃという細かいツッコミはおいておく。問題は、このfwloopメソッドcond, update, bodyがループのたびに変化することを前提にした設計になっており、関数型的な設計とは極めて相性が悪いという点である。このメソッドは次のように使うそうだ…

var a = data
fwloop("dataout.txt", !a.isEmpty, a = a.tail) {
  a.head.mkString("\t") + "\r\n"
}

本書の著者がここで何がやりたかったかというと、おそらく、名前渡しの機構を使って副作用を起こし、C言語風のforをエミュレートしたかったのだろうが、そんなことをScalaでやるべきではない!他にもいくつか例はあるが、この一例を見ただけで、本書から「関数型プログラミングの設計と理解」について学ぶのは絶望的だというのはわかったと思う。

本書で著者は、全体的に「頑張っている」のだが(このため、単純な事実誤認は少なくなっており、その点は単なる手抜き入門本と比べて良い部分だと思う)、それがどうも明後日の方向に向いているように感じる。

というわけで、本書は、あくまでScalaの言語機能のプチ紹介ツアーとして読むべきものであり、コード例から何かを学ぼうとしてはいけない。それはほぼ必ずScala Wayから外れていると言える。