• 作成:

Emacsで拡張子.plがPerlなのかPrologなのか分からない問題をディープラーニングってやつでなんとかしました

問題

PerlSWI-Prologは共に同じ拡張子.plを使っています。

なので適切に拡張子がついていても、 shebangなどがない限り、ファイルを開くだけでは、テキストエディタは通常の方法では、今開いたソースコードがPerlのようなものなのか、 Prologのようなものなのか判別できないことになります。

個人的にはRakuの方はともかく、 Perlはあまり書いたことがないのですが、 GNU/LinuxシステムでPerlが入ってないことがあまりないため、共存させようとProlog側を.prologとすることで誤魔化していました。

何故この拡張子を選んだのかと言うと、 GNU Emacs のデフォルトの、 auto-mode-alist で、

("\\.prolog\\'" . prolog-mode)

と設定されてたからというだけです。

最近Prologを実装したり触ることが増えてきました

最近、 logict: A backtracking logic-programming monad. のようなものを使ったHaskellでのDatalogの模倣でプログラミングしていました。

そしてとうとう本当にPrologの方言のインタプリタをHaskellでほぼフルスクラッチで実装することになりました。

ncaq/prohell: Prolog like programming language は大学の課題(2023-2016=7年前?)が終わったあと全く触ってなかったのである意味リベンジですね。まあこれはシンタックスをLightweightにしたのは構わないのですが、 Prologの重要機能をあまり実装しなかったので、大学の時にあまり真面目にPrologを実装したとは言い難いですし、大学からRubyと正規表現で書けと命令が来たので書いただけで、本気で言語処理系を書くならばHaskell, Rust, Scala, TypeScriptのどれかを使って、 Rubyで真面目にやることは無いでしょう。

.plを適切に処理してほしい

というわけでPrologを触ることが増えたので、 Emacsで.plのソースコードを開いた場合、 PrologかPerlか適切に判定してほしいわけです。

他の人を見ると思い切りよく.plをprolog-modeに設定している方が多いですが、そこまでの思い切りは私は持てません。

Perlコードにはshebangが必ず存在すると仮定する手もありますが、別にPerlコードでも独立してるスクリプトだけでは無いでしょう。

ディープラーニングでなんとかしてもらうことにしました

色々調べた所、以下のリポジトリを見つけました。

andreasjansson/language-detection.el: Automatic programming language detection of code snippets, in Emacs Lisp

以下の記事でこのパッケージの概要は掴めると思います。

language-detection.el : elispでプログラミング言語を自動判定できるってホント?

機械学習を使うのはちょっと大仰な気もしますが、とりあえずPerlとPrologに対応しているようなので、これをうまく使えば自動判定が出来るのではないでしょうか。

本来はewwで見たコードブロックや、拡張子のないファイルなど、完全に言語が推定できないものに使うのでしょうけれど、今回はPerl or Prologの時だけ判定してくれれば十分でしょう。

モード切り替えを実装

Entrypointsを見ると、

  • language-detection-buffer: バッファの言語を推定して出力する
  • language-detection-string: 引数の言語を推定して返す

の2つしか無いようなので、これを組み合わせてモード切り替えを実装する必要があるでしょう。

公式サイトのREADMEのeww向け実装を少々参考にすれば簡単に実装できそうですね。

出来ました。

(defun language-detection-mode-switch ()
  "`(language-detection-buffer)'の結果に従ってモードを切り替えます。
典型的な使い方として、PerlとPrologを自動識別することを考えています。"
  (interactive)
  (let ((mode
         (pcase (language-detection-buffer)
           ('ada 'ada-mode)
           ('awk 'awk-mode)
           ('c 'c-mode)
           ('cpp 'c++-mode)
           ('clojure 'clojure-mode)
           ('csharp 'csharp-mode)
           ('css 'css-mode)
           ('dart 'dart-mode)
           ('delphi 'delphi-mode)
           ('emacslisp 'emacs-lisp-mode)
           ('erlang 'erlang-mode)
           ('fortran 'fortran-mode)
           ('fsharp 'fsharp-mode)
           ('go 'go-mode)
           ('groovy 'groovy-mode)
           ('haskell 'haskell-mode)
           ('html 'html-mode)
           ('java 'java-mode)
           ('javascript 'javascript-mode)
           ('json 'json-mode)
           ('latex 'latex-mode)
           ('lisp 'lisp-mode)
           ('lua 'lua-mode)
           ('matlab 'matlab-mode)
           ('objc 'objc-mode)
           ('perl 'perl-mode)
           ('php 'php-mode)
           ('prolog 'prolog-mode)
           ('python 'python-mode)
           ('r 'r-mode)
           ('ruby 'ruby-mode)
           ('rust 'rust-mode)
           ('scala 'scala-mode)
           ('shell 'shell-script-mode)
           ('smalltalk 'smalltalk-mode)
           ('sql 'sql-mode)
           ('swift 'swift-mode)
           ('visualbasic 'visual-basic-mode)
           ('xml 'sgml-mode)
           (_ nil))))
    (if (not mode)
        (error "モードを推定できませんでした。")
      (if (not (fboundp mode))
          (error (concat "モードに対応する関数がインストールされていません: " mode))
        (funcall mode)))))

エラーにまだ遭遇していないので、エラー処理をちゃんと出来ているかの自信はあまりありません。

これをauto-mode-alistに適応すれば良いですね。実際の割り当てにはleaf.el:mode引数を使いました。

(leaf prolog
  :mode
  (("\\.pl\\'" "\\.pro\\'" "\\.P\\'") . language-detection-mode-switch))

:after tをつけて遅延してると:mode自体が呼び出されないので注意です。

解決しました

とりあえずいくつかのファイルを開いてみた所、ちゃんと認識しているようです。パッケージの最後の更新が2016年だったので少し不安でしたが、 PerlもPrologも新規にシンタックスがガラっと変わるような言語では無いのであまり気にする必要は無いですね。

今回はPerlとPrologを解決するという小さいタスクに適応しましたが、拡張子が不明なファイルでメジャーモードを決めるという本来想定されてそうな使い方に使うのも良さそうですね。