FsLexYaccを使ってみた

less than 1 minute read

F#向けの字句解析・構文解析フレームワークであるFsLexYaccを使ってみたのでその所感を書いておきます。

今回作ったもののリポジトリはこちらです。京大情報学科の実験科目の教科書にあるMLインタプリタの実装(OCaml+ocamllex+Menhir向け)をF#+FsLexYaccで書いてみたというだけの内容です1

導入

.Net Core SDK 2.2がインストールされていることを前提とします2

$ mkdir fslexyacc-test && cd fslexyacc-test
$ dotnet new console -lang=F#

でリポジトリを作ったあと、fslexyacc-test.fsproj を開いて

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <RootNamespace>fslexyacc_test</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <!--ここに追加-->
    <PackageReference Include="FsLexYacc" Version="9.0.2" />
    <!--追加ここまで-->
    <Compile Include="Program.fs" />
  </ItemGroup>

</Project>

のように PackageReference の行を足し、dotnet buildなどするとFsLexYaccがダウンロードされます。

なお、私の環境ではwarning NU1608: 依存関係の制約外で検出されたパッケージのバージョン: FsLexYacc 9.0.2 では FSharp.Core (>= 4.3.4 && < 4.4.0) が必要ですが、バージョン FSharp.Core 4.5.2 は解決されました。と、F#のバージョンが新しすぎる旨警告が出ますが、とくに問題は起きていません。

使い方

字句解析・構文解析自体の解説は私の手に余るので、前述の教科書の説明などをお読みください。ここにはocamllex+Menhirと比較した差異を書いておきます。

……といっても、ほとんどocamllex+Menhirと同じでした(なのであまり書くことがない)。FsYaccはMenhirではなくocamlyaccの移植なので、Menhir独自っぽい機能3はありませんでしたが、それ以外はほぼコピペで動きます。

ヘッダ部分など、OCamlのコードを書く部分はもちろんF#のコードに変える必要がありますが、(F#のコードはだいたいOCamlなので)基本的には機械的に直すだけでした。List.assocに対応するものがF#標準ライブラリにはなさそうだったので自分でそれっぽいものを書きましたが、ググったら出てくるスニペットを使っても良かったですね……。F#はOCamlと命名規則が違うので、ここを書き換えていくと結構な量になりますが、命名規則なので従わなくても動作に問題はありません(Linterには怒られますが)。

注意する必要があるのはfsprojファイルの書き方です。fsprojはF#のMakefileにあたるものですが、(Visual Studioを使わない場合は)自動で変更されないので自分で書く必要があります。今回の場合は次のようになりました。これで dotnet run するだけでFsLexとFsYaccが走り、全体のコンパイルが行われ、実行されるようになります。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <RootNamespace>fslexyacc_test</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <!--FsLexYaccを使う設定-->
    <PackageReference Include="FsLexYacc" Version="9.0.2" />
    <!--構文解析に現れる型の定義ファイル-->
    <Compile Include="Syntax.fs" />
    <!--Parser.fsyをFsYaccで処理-->
    <FsYacc Include="Parser.fsy">
      <OtherFlags>--module Parser</OtherFlags>
    </FsYacc>
    <!--Lexer.fslをFsLexで処理-->
    <FsLex Include="Lexer.fsl">
      <OtherFlags>--module Lexer --unicode</OtherFlags>
    </FsLex>
    <!--生成されたParser.fsとLeser.fsをコンパイルする-->
    <Compile Include="Parser.fs" />
    <Compile Include="Lexer.fs" />
    <!--以下は字句解析・構文解析以降の処理を行うプログラム-->
    <Compile Include="Environment.fs" />
    <Compile Include="Eval.fs" />
    <Compile Include="Program.fs" /><!--エントリポイント-->
  </ItemGroup>

</Project>

fsprojは順序が意味を持つことに注意してください。上から順に読み込まれるので、ファイルの依存順に書きます。また、エントリポイントがあるファイルは最後に書く必要があります。

dotnet run で全体のビルドができるのですが、FsLexやFsYaccのコマンド実行時にエラーが起きた場合にはまともなエラーメッセージを表示してくれません。直接コマンドラインからFsLexやFsYaccの実行ファイルを呼んでやればちゃんとエラーメッセージを表示してくれたので、私はそうしました。上に書いた方法でFsLexYaccを入れる(=Nugetで入れる)と、実行ファイルは %USERPROFILE%\.nuget\packages\fslexyacc\9.0.2\build\fslex\net46\ のような場所に入ることになります。

その他

上のリポジトリのコードはWindows上で書いたものですが、(もちろん)Linuxでも動きました。しかし、Windows上では;;のあとEnterを押すと実行してくれていたのに対し、Linux上ではその後^Dを入力しないと実行してくれませんでした。同じ動作にしたい場合は入力周りで少し注意しないといけないようです(面倒なので修正していませんが)。

あと、FsLexYaccの何が嬉しいかということですが、私が言えることととしてはF#はOCamlと比較してWindows環境で動かすのが非常に簡単であることがあります。また、.NetなのでC#などから呼びやすいことが期待できます。.Netな構文解析ライブラリは他にもありますが、parsec系のものはLex/Yacc系のものとは勝手が違います4。C#のLex/Yacc系のものと比べての良さは、……F#で書けるというところでしょうか。

  1. 私はこの科目を受けていませんが、友人から存在を聞いて面白そうだったのでやってみました。とても教育的で良かったです。 

  2. FsLexYaccは.Net Framework上でも(おそらく)動くのですが、この記事では.Net Coreの場合のみを扱います。 

  3. 今回出てきた範囲では「構文にマッチした部分を $1 などでなく t=TOKEN として t で参照できる」というものだけでした。 

  4. 私はsprache(C#のparsec)を使おうとして挫折した経験があります……