2013年12月14日土曜日

F# と OCaml

 この記事はF# Advent Calendar 2013に参加しています。14日目を担当させてもらいます。

F#とは

F#についてはMSDNでは
F# は、従来のオブジェクト指向プログラミングと命令型 (手続き型) プログラミングに加えて、関数型プログラミングをサポートするプログラミング言語です。 Visual F# 製品は、F# アプリケーションの開発と F# コードを使用した他の .NET Framework アプリケーションの拡張をサポートします。 F# は、.NET Framework 言語のファースト クラスのメンバーであり、関数型言語の ML ファミリに著しく似ています。
…と説明されています。MLファミリと書かれていますが、中でもオブジェクト指向的要素が追加されたOCamlとはかなりの類似点していて、F#で提供されるコアライブラリの基本部分はOCamlのモジュールと一致しています。そこでOCamlとF#の違い、その理由について探ってみようと思います。

F#とOCamlの違い

何よりも大きいのはオブジェクトおよびガベージコレクタでしょうか。F#は.NET Framework上で動作するため、すべてのオブジェクトは.NETオブジェクトでありSystem.Objectの派生クラスです。このことは全てのオブジェクトはSystem.Typeを通じてRTTI; Run-Time Type Infomationが得られることを意味します。対してOCamlは独自のGCでありSystem.TypeのようなRTTIは提供されていません。元々コンパイル時に型チェックされているため、実行時に型情報は不要なわけです。そのため実行時にObj.magicを用いてデータを強引にキャストしてしまうこともできます。これはC++言語でいうreinterpret_castであり、F# / .NETでは不可能な行為です。
 構文に対する細かい優先順位もところどころ違います。もちろん構文の違いといえばF#には独自の軽量構文がありますし私自身便利に使っていますが、OCamlとの比較においては論外ということで、それ以外について。 F#では.NET Frameworkで提供されるnamespaceが扱え、「.」(ピリオド)で区切ります。更にプロパティやメソッドも「.」でつながれ、正しいメンバー参照である限りどこまででもつなぐことができます。例えばSystem.String.Empty.Count.ToString()とか。しかしOCamlではUIDとLIDしかありません。UIDとは大文字から始まる識別子、LIDとは小文字から始まる識別子です。先ほどのObj.magicはObjがUIDでmagicがLIDとなります。namespaceはなくクラスメソッド参照は「#」となるため「.」の優先順位が異なります。
 構文といえば独自に新しい演算子が作成できる点、これはF#とOCamlと共通で、通常の.NET Frameworkからするとかなり異質な行為です。逆にコンピュテーション式はF#独自の機能です。コンピュテーション式内は通常のF#構文のままですが、書かれた式は直接コンパイルされるのではなく、構文解析後、コンピュテーション式のビルダークラスのメソッド呼び出しへと変換されてコンパイルされます。このような機能はOCamlにはありません。
 …というのは嘘で、Camlp4という特殊なモジュールがあります。Camlp4とはPre-Processor-Pretty-Printer for OCamlという意味です。一般的にコンパイラというのは、構文パーサーがソースコードからAST; Abstract Syntax Treeを構築し、ASTを何等かのバイナリに変換を行います。OCamlでは通常の構文パーサー以外にCamlp4が提供する別のParserに差し替えることが可能です。といってもただ構文パーサーを差し替えても同じ構文しか使えないのでは実装が2種類あるだけ何も面白くありません。独自のParserを作成しそれに差し替えることもできます。 ただし、独自のParserを全て作り上げるのは大変なことなので、新たに構文を増やさないのであればFilterというASTを別のASTに変換する機能もあります。これはF#のコンピュテーション式に近い行為ですが、どのように変換するかをプログラム的に制御できるため自由度は高いです。 また、ParserとFilterまで用意するならついでということでPrinterも用意されています。こちらはASTをコンパイルするのではなく別のデータ形式に変換することができます。それ以外にもいろいろありますが、あまり詳しくないのでこの辺りまでで。
 実はCamlp4はRevisedというOCamlとは別の構文で書かれています。つまりCamlp4をコンパイルするにはCamlp4のRevised Parserが必要であり、ある種のセルフホスト状態になっています。なぜそうなっているかというとOCamlの構文が気に食わないそうで、funとfunctionは同じものだからキーワードを分ける意味がないとか、変数と関数を同じletにすべきではないとか、どこかに書かれていました。

FSharp Printer for Camlp4

というわけで、Camlp4に含まれているOCaml Printerを元にFSharp Printerを作ってみました。Camlp4がparse可能なソースコードをF#コードに変換して出力できることになります。patch形式にしているのは、修正個所がわかりやすくなるのとASTの変更に追従を考えて、ですが…無意味かな?
 先に使い方を説明しておくと、コンパイルするにはコンパイラとパッチ元ファイルのバージョンを一致させる必要があります。現時点でリポジトリに含めているのは4.01.0向けのソースになります。コンパイル方法は、通常のOCamlと少し異なり、Camlp4を使います。
$ ocamlc -I +camlp4 -pp camlp4rf -c Camlp4FSharpPrinter.ml
でCamlp4FSharpPrinter.cmoが生成されます。
 使うためにはこのファイルを参照可能なディレクトリならどこでも構わないですがとりあえず、
$ cp Camlp4FSharpPrinter.cmo $ocaml/lib/camlp4/Camlp4Printers/
とコピーします(尚、パッチ元のOCaml.mlのあるディレクトリとは別です)。その上で、OCamlソースコードをF#コードに変換するには
$ camlp4of -printer Camlp4FSharpPrinter -o fsharp-source.ml ocaml-source.ml
と実行します。
 ということでコードをペタペタ。

F#とOCamlの違い

またですが…FSharp Printerを作っていて気づいた点をいくつか。
 F#の軽量構文はインデントで表現するため、メンバーの無いクラスを表現することができません。ごくまれに存在するmarker interfaceのようなことが表現できなくて困ったりします。こういう場合でも冗長構文なら表現することができます。
 OCamlはモジュールをまたぐ構造体メンバーアクセスの際にはモジュール名を挟みます。someObject.ModuleName.memberNameという感じにいきなりモジュール名が出現するため心臓に悪いです(最初の方に書いたUID / LIDはこのことです)。F#では型情報は全て把握できているためsomeObject.memberNameになります。
 F#では配列を含むindexerが全て .[] 演算子で表現されます。そのため .[] が出現した時点でindexerの存在する型に確定していないとコンパイルできません。対してOCamlにはindexerという汎用的な概念はなく、配列要素にアクセスする .() と文字列にアクセスする .[] のみとなっています。つまり、F#とは逆でこれらの演算子から型推論することができます。この型推論条件の違いを埋め合わせるために、FSharp PrinterではArray.get / String.getに置き換えています。
 OCamlの==演算子、!=演算子も困ったことに。単純に=演算子、<>演算子に置き換えることはできません。とりあえずわかる範囲でパターンマッチに展開することにしました。この辺りの細かいOCamlの動作(及びそれに相当するF#のコード)はよくわかっていません。
 OCamlはパターンマッチに'0'..'9'のように文字範囲を表現することができますがF#にはありません。こちらはwhen句に展開しています。
 F#ではクラス内のletはprivateスコープですが、OCamlはいわゆるprotectedスコープであり派生クラスから参照可能です。こういった違いはさすがにFSharp Printerで埋め合わせることはできません。コンストラクターの書式も違いますがこちらは結構強引に対応させています。
 もっと大きな問題として、OCamlにあってF#に存在しないモジュールの呼び出し…これについてはF# PowerPackを使ってください。
 最後にどうにもならない爆弾を…。F#というか.NETには値型があるため効率のいい配列操作ができますが、OCamlはこれができません。そのため、stringがbyte arrayのように使われています。つまりOCamlのstringはmutableです。対してF#のstringはimmutableです。この違いはどうにも吸収できません。OCamlソースをF#に移植する際には、mutableに扱われているstringをbyte[]に変換するところから始まると言ってもいいでしょう。

以上、とりとめもないF#とOCamlの比較でした! Camlp5…? 知らない子ですね。

ところでこのblogのテーマ読み辛い…。元々Pタグでレイアウトするつもりでスタイルを書いてたのに、エディターが更新されてDIVタグ&BRタグしか埋めてくれない…。

0 件のコメント: