Hugo blog source

blog

DONE License

License

  • All articles (sentence) are distributed under CC BY.
    Creative Commons Attribution 4.0 International Public License (CC BY)
    Copyright (c) Hiroshima.lisp - https://lisp.netlify.app
    https://creativecommons.org/licenses/by/4.0/legalcode
    
  • All source code are distributed under CC0.
  • All cited source code respects the license of the cited source.
    • GPLv3 - Emacs related source code.
    • MIT

Dependent materials/sources license

Using Hugo blog theme

Respected Hugo blog theme

DONE Netlify + Hugo + ox-hugoでブログを初める第一歩

Intro

HTMLジェネレータのseml-mode.elとOrg->HTMLジェネレータ拡張のorglyth.elを作っていて、自分のブログくらいはそれらを使って構築しようと思っていました。

しかし結局落ち着かないまま1年が経過し、leaf.elのドキュメントや、専門分野のまとめなどネットで公開したいものだけ溜まっていました。

conao3.comのドメインも2017年から持っていますが、3年もの間まともなコンテンツは提供されませんでした。

ここらへんで諦めてさっさとHugoとNetlifyで公開してしまえということで、きちんと整備されたツールを使わせてもらい、この度ようやくconao3.comでまともなコンテンツを提供できるようになりました。

Netlify

よい情報で溢れているので省略します。

NetlifyはDNSも提供しているようなので、「さくらのドメイン」で取得したconao3.comをNetlify DNSで解決させるようにしました。

また、デフォルトでは古いHugoが動いているようなので、「Build & Deploy」->「Environment」->「Environment variables」で「HUGO_VERSION」をローカルのバージョンで設定します。 私の場合「0.69.2」でした。

Hugo

  • Hugoのインストール

    Install Hugoを参考にHugoをインストールします。

    記載がありませんが、Archの場合はpacmanで簡単に入ります。(Archlinux - Package)

    # pacman -S hugo
    $ hugo version
    Hugo Static Site Generator v0.69.2/extended linux/amd64 BuildDate: unknown
    
  • ブログテンプレートの展開

    Quick Startを参考にテンプレートを展開します。

    私はローカルのレポジトリ置き場は ~/dev/repos にしています。

    $ cd ~/dev/repos
    $ hugo new site netlify-src
    

    以下、 netlify-src はhugoサイトのルートディレクトリということにします。

  • ブログテーマの適用

    Hugo themesからいい感じのテーマを探し、submoduleでレポジトリに持ちます。

    テーマはローカルで簡単にカスタマイズできる仕組みがあるので、多少気に入らないところがあっても大丈夫です。

    $ cd quickstart
    $ git init
    $ git submodule add https://github.com/lxndrblz/anatole.git themes/anatole
    

    テーマごとにカスタマイズする方法が違うので、テーマのREADMEを読んで設定をして下さい。

    なお、私は当時ローカルで簡単にカスタマイズできる機構を知らず、forkでカスタマイズするしかないと思っていたので、「Make a Hugo blog from scratch」を参考に0からテーマを作りました。

    私が作ったテーマはconao3/anatole-extで公開しています。

  • サンプル記事の追加

    サンプルの記事を追加します。

    テーマが exampleSite というフォルダを持っている場合は、それらのファイルを netlify-src/ にコピーするだけでいいです。

    コマンドでテンプレートを作成するには次のコマンドを実行します。 実行後、 content/blog/my-first-post.md が作成されているので、よしなに編集します。

    $ hugo new blog/my-first-post.md
    
  • ローカルサーバーの起動

    Hugoは簡単にローカルサーバーを起動でき、さらに依存ファイルが変更されたときに自動でブラウザにリロードさせることができます。

    $ hugo server -D
    

    サーバーを起動すると localhost:1333 で配信されます。

  • 静的サイトのビルド

    サイト全体のビルドは下記で行います。

    Netlifyで公開するならローカルでビルドする必要はないのですが、最終的にどんなツリーになっているかを確認することができます。

    $ hugo
    

ox-hugo

  • ox-hugoのインストールと設定

    ox-hugoをインストールし、適宜設定を行ないます。

    hugo | IMADENALE」を参考に無難な構成を考えましたが、もっといい方法がある気がします。 とりあえず私は下記の設定と方針でしばらくやってみます。

    (leaf ox-hugo
      :doc "Hugo Markdown Back-End for Org Export Engine"
      :req "emacs-24.4" "org-9.0"
      :tag "docs" "markdown" "org" "emacs>=24.4"
      :added "2020-05-05"
      :url "https://ox-hugo.scripter.co"
      :emacs>= 24.4
      :ensure t
      :after ox
      :custom ((org-hugo-front-matter-format . "yaml")))
    
  • orgファイル管理の方針

    ox-hugo用のorgファイルは netlify-src/org/{{user}}.org に持つことにしました。

    #+title: Hugo blog source
    #+author: conao3
    #+date: <2020-05-05 Tue>
    #+options: ^:{}
    
    #+hugo_base_dir: ../
    #+hugo_section: blog
    
    #+link: files file+sys:../static/files/
    
    * blog
    :PROPERTIES:
    :EXPORT_HUGO_SECTION: blog
    :END:
    
    ** DONE test                                                      :meta:hugo:
    CLOSED: [2020-05-05 Tue 19:21]
    :PROPERTIES:
    :EXPORT_FILE_NAME: test
    :EXPORT_DATE: 2020-05-05T00:00:00+09:00
    :EXPORT_HUGO_LASTMOD: [2020-05-05 Tue 16:20]
    :END:
    
    testestest.
    

    もし将来的にこのorgファイルが超巨大なファイル(1万行~)になれば、適宜 netlify-src/org/archive-{{user}}-{{num}}.org に移すことにします。

    レベル1はセクションの分類に使い、レベル2のheadingから記事のツリーと解釈されます。

  • 静的ファイル管理の方針

    filesのlinkは「画像の埋め込みテスト | imadenale」を参考にしました。

    スクリーンショットやPDFは netlify-src/static/files 以下に持つことにします。

    netlify-src/static はレポジトリ肥大化を避けてconao3/netlify-srcからconao3/netlify-src-blobに切り出し、submoduleで持つことにします。

    参考記事ではfilesのリンクをURLで設定していましたが、「Org Modeのリンク機能で情報集約 | Qiita」を参考に fils+sys: 指定を使うとorgの画像インライン表示もできますし、きちんとox-hugoによってリンクが修正され、正しいマークダウンが出力されました。

    • Input

      #+link: files file+sys:../static/files/
      
      #+attr_html: :width 128px
      [[files:logo.jpg]]
      
    • Output

      {{< figure src="/files/logo.jpg" width="128px" >}}
      
    • Rendering

Deploy

NetlifyのデプロイはGitHubにpushするだけです!

Hugoのビルドはとても早く、pushしてNetlifyのログを見に行くともう終わっています。

ビルドできたら個人Slackに通知飛したりするのをまた今度やりたいと思っています。

Conclusion

Netlify + Hugo + ox-hugoで快適なブログ執筆環境を整えることができました!

ようやく情報発信する環境が整ったので、どんどん情報を発信していきたいと思います。

まずは見やすいleaf.elのドキュメントを書きます!

また、執筆現在はconao3/netlify-srcconao3/netlify-src-blobをパブリックレポジトリとしていますが、後でプライベートレポジトリに変更し、Patreonの特典にします。

本来公開するものを非公開にすることでしか価値を付けられないことをお許しください。

ぜひPatreonで私の活動のサポートをして頂けるとうれしいです!

DONE 個人ブログのパーマリンクに関する最適戦略について

Intro

Webで情報を公開するにあたって、パーマリンクの設計は将来に渡って重要なものです。

このブログは人生において3つめのブログであり、これまでの2つのブログは勝手にアーカイブしてネットから消してしまいました。

しかしこのブログは今のところ死ぬまで付き合うつもりであり、それが出来るようにコンテンツの少ない今、設計する必要があります。

ブログ管理の裏側については「Netlify + Hugo + ox-hugoでブログを初める第一歩」に書きましたが、パーマリンクの設計は表側の設計であり、ブログの人生の中で途中で変えることは許されていません。

もし無理矢理変えれば、これまでシェアしてもらったURLの意味は失くなり、404ページに多くの読者を案内してしまうことになります。

さて、このブログではどのようにパーマリンクを設計すると良いのでしょうか。

調査

パーマリンクの設計といっても選択肢は多くありません。

そもそも使える情報が タイトル公開日時連番 くらいしかないので、代表的なものは以下のような形式か、その組み合わせです。

  • {{base-url}} / {{title}} /
  • {{base-url}} / {{short-title}} /
  • {{base-url}} / {{category}} / {{short-title}} /
  • {{base-url}} / {{num}} /
  • {{base-url}} / {{year}} / {{month}} / {{day}} /

{{title}} はそのまま記事のタイトルを使う方法です。 日本語のタイトルはそのままURLエンコードされ、URLになるパターンです。

{{short-title}} はSEOに強い、記事ごとにつけられた短いタイトルを使う方法です。 あえて日本語を使う理由もなく、普通は英数字のみで長くても30文字くらいでまとめます。

{{category}} は記事ごとに設定されたカテゴリーを使う方法です。

{{num}} はサイトごとにカウンタを持っておいて、その連番をURLに使用する方法です。

{{year}}, {{month}}, {{day}} は記事の公開日です。

評価

さて、これらの方法のなかで、どれがよいのでしょうか。

大きく分けると2つの分類ができ、記事ごとに新たに考える方法と自動で生成する方法です。

  • 記事ごとに新たに考える方法

    • {{short-title}}
      • メリット
        • SEOやURLシェアの観点から最高の選択肢
      • デメリット
        • 記事ごとに短いタイトルを英語でつけなおさなければならない
        • 記事を少ない英単語で表現するのは一般的に難しく、長くなってしまう場合もある
        • 少ない英単語で命名するため、URL衝突を考慮する必要がある
        • もし、Emacsと命名した記事より、もっと適切な記事を書いた場合にその名前はもう使えない
    • {{category}}
      • メリット
        • URLから得られる情報がある
        • カテゴリー名は記事名に比べて、ずっと少ないので、命名はあまり苦でない
      • デメリット
  • 自動で生成する方法

    • {{title}}
      • メリット
        • 記事ごとに考えなくてよい
      • デメリット
        • 日本語のタイトルをつけるとURLが長大になる
    • {{num}}
      • メリット
        • 記事ごとに考えなくてよい
        • キリ番記事が作りやすい (100記事達成記事など)
      • デメリット
        • 記事を後から非公開にすると、欠番が生じる
        • URLから得られる情報が少ない
    • {{year}}, {{month}}, {{day}}
      • メリット
        • 記事ごとに考えなくてよい
        • URLを見れば公開日が分かる (技術系記事において新しい記事は重要)
      • デメリット
        • URLから得られる情報が少ない

というところでしょうか。 それぞれにメリットとデメリットがあるので、それを評価した後は好みの問題となります。

決定

前段の評価に基づいて、私はこのブログのパーマリンクを以下のように設計しました。

  • {{base-url}} / blog / {{year}} / {{random}}

まず、私はとてもものぐさなので、記事ごとにショートタイトルを与える方法は難しいです。 いくらSEOやURLシェアの観点から最良の選択だと言っても、記事を書くモチベーションを失っては元も子もありません。

タイトルをそのまま付ける方法は、記事ごとに自動で決定できるので魅力的ですがきびしいです。各見出しの id は日本語をURLエンコードしたものをしかたなく使うとして、2つの要素をURLエンコードすると簡単に256文字を超えます。もちろん現代ではURLの流さに制限はないですが、限度はあると思います。

カテゴリー管理も同じような階層が現われ、破綻することが目に見えています。 実は前回のブログはそれで破綻したのです。 /emacs//blog/emacs/, blog/advent/emacs/ などが乱立し、新たな記事をどの階層に作ればよいのか混乱する要因になりました。

連番を付ける方法も魅力的ですが、欠番が発生することに耐えられません。これは気分の問題です。

日付を使用する方法はとても良いと思います。 URLから得られる情報もありますし、後述するランダム値の衝突について「永遠」を考えなくてすみます。

ここまで、日付を使うことが決まり、タイトルもだめ、ショートタイトルもだめ、連番もだめといろいろな物を拒否した結果、一意性を確保するものとして使えるものはランダム値しかありません。

まとめ

使用するパーマリンクを設計することができました。 しかし、 {{random}} はどれだけのビット数を使い、どのようなフォーマットを使うべきでしょうか。

その考察は次の記事「誕生日のパラドックスから考察する、個人ブログURLに必要なランダムビット数」に譲るとして、この記事ではパーマリンク設計について大きな決定ができたので、とても満足しています。

なお、例外的に {{base-url}} / emacs / {{package-name}} というURLを使おうと思っています。 これはるびきちさんのパーマリンク設計を踏襲しています。 るびきちさんと同じようにEmacsのパッケージ紹介記事ではこのURLを使用しようと思います。

DONE 誕生日のパラドックスから考察する、個人ブログURLに必要なランダムビット数

tl;dr

  • ブログのURLにランダム値を使うとして、必要なビット数は何桁だろうか
  • ハッシュ衝突の問題は「誕生日のパラドックス」と同じ状況設定
  • 数学的な裏付けの元で、ランダムビットを28桁確保すれば十分

Intro

個人ブログのパーマリンクに関する最適戦略について」において、

  • {{base-url}} / blog / {{year}} / {{random}}

というURLをこのブログに使うことに決めました。しかし、 {{random}} はどれだけのビット数を使い、どのようなフォーマットを使うべきでしょうか。

幸い、 {{year}} を入れているので、「永遠」を考える必要はありません。 せいぜい「1年」でどれだけの記事を書くのかを見積れば良いことになります。

毎日、1記事を書いたとして \(365\) 記事、多めに見積もって \(400\) 記事とします。 さらに毎日、 \(5\) 記事書くことを上限と仮定すると \(5 \times 400 = 2000\) 記事となり、せいぜい \(2000\) 記事がある場合に必要なランダムビット数を求めたいと思います。

誕生日のパラドックス

誕生日のパラドックス - Wikipedia」より、

誕生日のパラドックス(たんじょうびのパラドックス、英: birthday paradox)とは「何人集まれば、その中に誕生日が同一の2人(以上)がいる確率が、50%を超えるか?」という問題から生じるパラドックスである。鳩の巣原理より、366人(閏日も考えるなら367人)集まれば確率は100%となるが、しかしその5分の1に満たない70人しか集まらなくても確率は99.9%を超え、50%を超えるのに必要なのはわずか23人である。

誕生日のパラドックスは論理的な矛盾に基づいているという意味でのパラドックスではなく、結果が一般的な直感と反しているという意味でのパラドックスである。

Wikipediaでも計算してありますが、ここでも計算しておきます。

きちんと問題として整理すると以下のようになります。

Aさんを含め、n人がいる。一年を365日とし、誕生日は全ての日で等確率とする。

  1. Aさんと同じ誕生日の人が存在する確率 \(P_1\)
  2. 同じ誕生日の人が存在する確率 \(P_2\)
  3. \(P_1\) と \(P_2\) が \(50\) %を超えるのにそれぞれ必要な人数 \(n_\text{min1}\), \(n_\text{min2}\)
  1. Aさんと同じ誕生日の人が存在する確率 \(P_1\)

    余事象で求める。

    Aさん以外の人は \(\frac{364}{365}\) の確率でAさんの誕生日と衝突しない。 Aさん以外の人は \(n-1\) 人存在するので、

    \[ P_1 = 1 - \qty(\frac{364}{365})^{n-1} \]

  2. 同じ誕生日の人が存在する確率 \(P_2\)

    余事象で求める。

    n人の誕生日が異なる確率は \[ \frac{364}{365}\cdot\frac{363}{365}\cdot\frac{362}{365}\cdots\frac{365-(n-1)}{365} = \frac{{}_{364}\mathrm{P}_{n-1}}{365^{n-1}} = \frac{{}_{365}\mathrm{P}_n}{365^n}\]

    よって \[ P_2 = 1 - \frac{{}_{365}\mathrm{P}_n}{365^n} \]

  3. \(P_1\) と \(P_2\) が \(50\) %を超えるのにそれぞれ必要な人数 \(n_\text{min1}\), \(n_\text{min2}\)

    \(n_\text{min1}\) については計算できる。

    \begin{aligned} P_1 = 1 - \qty(\frac{364}{365})^{n-1} &> 0.5 \\
    0.5 &> \qty(\frac{364}{365})^{n-1} \\
    \log_2(0.5) &> (n-1)\log_2\qty(\frac{364}{365}) \\
    -1 &> (n-1)(-0.003958) \\
    253.65 &< n \end{aligned}

    よって \(n_\text{min1}=254\) となる。

    \(n_\text{min2}\) については面倒なので、Pythonを用いて図示する。

    import matplotlib.pyplot as plt
    import numpy as np
    
    memo = np.full(400, -1.0)
    def NPn (n):
        """
        余事象の確率
        n人の誕生日が異なる確率
        """
        if n == 0 or n == 1:
            memo[0] = 1
            return memo[0]
        elif n >= 366:
            return 1;
        elif memo[n] != -1:
            return memo[n]
        else:
            p = NPn(n-1) * ((365-(n-1))/365.0)
            memo[n] = p
            return memo[n]
    
    def Pn (n):
        """
        n人のうち、2人以上誕生日が同じ確率
        """
        return 1 - NPn(n)
    
    n = 70
    with plt.style.context(('science-transparent')):
        y = [Pn(i) for i in range(n)]
        plt.plot(y)
    
        plt.xlabel('n')
        plt.ylabel('P_n')
    
        plt.axhline(0.5, color='C1')
        n_min2 = np.searchsorted(y, 0.5)
        plt.annotate('({}, {:.2f})'.format(n_min2, y[n_min2]),
                     (n_min2, y[n_min2]), (n_min2-15, 0.8),
                     arrowprops = dict(arrowstyle='-|>', mutation_scale=20))
    
        plt.show()
    

    グラフから、 \(n_\text{min2} = 23\) 。

上記のとおり \(n_\text{min1} = 254\), \(n_\text{min2} = 23\) となり \(P_\text{min2}\) は直感よりも少ないように思います。

直感との乖離の理由は「自分と同じ誕生日の人が存在する確率」と「同じ誕生日の組が存在する確率」を混同してしまうからです。

ランダム値の衝突

今回問題にしているようなランダム値の衝突については、 「あるひとつのランダム値が衝突する事象」ではなく、「どれでもいいのでランダム値が衝突する事象」を考える必要があるので、 まさに誕生日のパラドックスと同じ状況設定となります。

誕生日のパラドックスでは365個の集合でしたが、今回は変数 \(N\) とします。

前段の \(P_2\) を参考に、 \(N\) 個の集合から \(n\) 個ランダム値を取ってきたときに、同じ値が存在する確率 \(p\) は次の形となります。

\begin{aligned} p &= 1 - \frac{N-1}{N}\cdot\frac{N-2}{N}\cdot\frac{N-3}{N}\cdots\frac{N-(n-1)}{N} \\
&= 1 - \qty(\qty(1-\frac{1}{N})\qty(1-\frac{2}{N})\qty(1-\frac{3}{N})\qty(1-\frac{n-1}{N})) \\
&= 1 - \prod_{m=1}^{n-1}\qty(1-\frac{m}{N}) \end{aligned}

ここで、ネイピア数のテイラー展開は次の形です。 \(x\) が十分小さいときは1次までの近似が使えます。

\begin{aligned} e^{-ax} &= 1 - ax + a^2\frac{x^2}{2!} - a^3\frac{x^3}{3!} + a^4\frac{x^4}{4!} + \cdots \\
&\approx 1 - ax \end{aligned}

\(N\) はランダム値の全体総数なので、 \(\frac{i}{N}\) は十分小さく、ネイピア数のテイラー展開から近似を使えます。

\begin{aligned} p = 1 - \prod_{m=1}^{n-1}\qty(1-\frac{1}{N}m) &\approx 1 - \prod_{m=1}^{n-1}\exp\qty(-\frac{1}{N}m) \\
&= 1 - \exp\qty(\sum_{m=1}^{n-1}\qty(-\frac{m}{N})) \\
&= 1 - \exp\qty(-\frac{1}{N}\sum_{m=1}^{n-1}m) \\
&= 1 - \exp\qty(-\frac{n(n-1)}{2N}) \end{aligned}

さらに \(n\) について変形します。途中、 \(n\) が十分大きいことから、定数の減算を無視しました。

\begin{aligned} \exp\qty(-\frac{n(n-1)}{2N}) &= 1-p \\
-\frac{n(n-1)}{2N} &= \ln(1-p) \\
n(n-1) &= -2N\ln(1-p) \\
n^2 &\approx 2N\ln\qty(\frac{1}{1-p}) \\
n &= \sqrt{2N\ln\qty(\frac{1}{1-p})} \end{aligned}

数値計算

  • Nが与えられている場合

    ランダム値の大きさ(\(N\))が与えられている場合、何個ランダム値を取り出したら(\(n\))衝突するでしょうか。 なお \(p=0.5\) とします。

    \begin{aligned} n = \sqrt{2N\ln\qty(\frac{1}{1-0.5})} &= \sqrt{2N\ln2} \\
    &\approx 1.1774\sqrt{N} \\
    &\approx \sqrt{N} \end{aligned}

    最後の近似はオーダーを見積もるためにとても雑な近似を行いました。

    もしランダムビットが 64bit の場合、ランダム値の集合は \(2^{64}\) となり、 \(2^{32} = (42.9\text{億})\) 個(WolframAlpha)のランダム値を生成したら、確率 \(0.5\) で衝突することになります。

  • Nを求めたい場合

    逆に今回私は試行回数、1年 \(2000\) 記事(\(n=2000\))からランダム値の大きさ(\(N\))を求めたいと思っています。 なお、ランダム値の大きさ \(N\) はビット数 \(b\) を用いて \(N=2^b\) と書けます。

    衝突する確率 \(p\) は \(1\) %と仮定します。

    \begin{aligned} n &= \sqrt{2N\ln\left(\frac{1}{1-0.01}\right)} \\
    2000 &= \sqrt{2N\ln\left(\frac{1}{0.99}\right)} \\
    2000^2 &= 2N \cdot 0.0100 \\
    N = 2^b &= 1000 \cdot 2000 \cdot 100 \\
    b &= 27.5 \end{aligned}

    以上、ランダムビット数は28桁あれば十分ということになります。

    16進数の表記を使えば1文字で4ビットの自由度があるので、結局、8文字あれば十分ということが分かりました。

まとめ

誕生日のパラドックスを導入としてランダム値の衝突問題(ハッシュ衝突問題)について考えました。

長々書いてきましたが、数学的な裏付けのもとで必要なランダムビット数が分かり、パーマリンクのフォーマットを決めることができました。

次の記事「ox-hugo用のorg-captureテンプレートについて」ではこのフォーマットを実現するためのox-hugoの運用について書きたいと思います。

参考

DONE ox-hugo用のorg-captureテンプレートについて

Intro

下記のブログの記事で考えて、ようやく記事を新規作成するために必要な情報が整いました。

org-captureはいつも設定しておきながら、設定したことを忘れているのですが、ox-hugoを使うにあたってきちんと入門したいと思います。

org-capture

org-captureはEmacsのどこにいてもorgでメモを取るためのフロントエンドです。

org-captureのユーザーはたくさんおり、設定例も溢れていますが、 「自分でグローバルバインドを設定しなければならない」という点が性に合わず、結局使わずじまいでした。

少し本題からずれますが、そもそも C-c につづくアルファベット、修飾キー付きのアルファベット、プレフィックスを自由に設定できるとして、覚えられる気がしません。

私のglobal-mapはtransient-dwimだけを設定しており、他のバインドはマイナーモード、メジャーモードのキーマップのみです。 transient-dwimはtransientを利用したキーバインド管理パッケージです。

transient-dwimを M-= に設定している場合、org-captureは M-= M-o M-o で起動できます。

ox-hugoの設定

ファイル名をUUIDで自動生成するテスト - IMADENELE」と「最終変更日の手動設定 - IMADENELE」を参考にテンプレートを調整しました。

(leaf ox-hugo
  :doc "Hugo Markdown Back-End for Org Export Engine"
  :req "emacs-24.4" "org-9.0"
  :tag "docs" "markdown" "org" "emacs>=24.4"
  :added "2020-05-05"
  :url "https://ox-hugo.scripter.co"
  :emacs>= 24.4
  :ensure t
  :after org
  :require t
  :defun (org-set-property)
  :custom ((org-hugo-front-matter-format . "yaml"))
  :config
  (defun c/ox-hugo-add-lastmod nil
    "Add `lastmod' property with the current time."
    (interactive)
    (org-set-property "EXPORT_HUGO_LASTMOD"
                      (format-time-string "[%Y-%m-%d %a %H:%M]")))

  (leaf *ox-hugo--capture
    :require org-capture
    :defvar (org-capture-templates)
    :config
    (add-to-list 'org-capture-templates
                 '("b" "Create new blog post" entry
                   (file+headline "~/dev/repos/netlify-src/org/conao3.org" "blog")
                   "** TODO %?
:PROPERTIES:
:EXPORT_FILE_NAME: %(apply #'format \"%s-%s-%s\"
        (format-time-string \"%Y\")
        (let ((sha1 (sha1 (shell-command-to-string \"head -c 1k /dev/urandom\"))))
          (cl-loop for (a b c d) on (cdr (split-string sha1 \"\")) by #'cddddr repeat 2 collect (concat a b c d))))
:EXPORT_HUGO_TAGS:
:EXPORT_HUGO_LASTMOD:
:END:
"))
    (add-to-list 'org-capture-templates
                 '("p" "Create new package post" entry
                   (file+headline "~/dev/repos/netlify-src/org/conao3.org" "emacs")
                   "** TODO %?
:PROPERTIES:
:EXPORT_FILE_NAME:
:EXPORT_HUGO_TAGS: emacs
:EXPORT_HUGO_LASTMOD:
:END:
"))))

p はパッケージの紹介用で emacs ツリー以下に展開されます。

b は簡単なブログ用で :EXPORT_FILE_NAME は「誕生日のパラドックスから考察する、個人ブログURLに必要なランダムビット数」をもとに8桁の16進数を使って自動生成します。

8桁の16進数は /dev/urandom から1KB読んで、それのsha1の先頭8桁のみを取ることにしました。 この乱数は衝突するときわめて面倒なことになるので、できるだけ均質なランダム値を得る必要があります。 この仕組みを作った後は衝突しないことをもう祈るしかない。。(もし衝突すると単に新しい記事で古い記事が上書きされて、特に警告は出ない。。)

まとめ

org-captureの設定をすることによってブログ記事を書くハードルがとても下がりました。

これから情報発信に努めていきたいと思います!

TODO Arch Linuxでいい感じに揃ったスクリーンショットを撮り、管理する方法

Netlify + Hugo + ox-hugoでブログを初める第一歩

TODO Test

test test test

test test test

平方根の高さを揃える \(\sqrt{g}\) に \\(\sqrt{h}\) は \mathstrut と \smash コマンドを使って \(\ssqrt{g}\) と \(\ssqrt{h}\) のように表示できる。

test

\[a\]

\[\RR\]

\[\div a\]

\[\sin(\frac{a}{b})\]

\[a \between b\]

TODO VMにArch Linux入れて初期設定するまで

intro

DONE 令和時代のEmacsカラーテーマの作り方

Intro

昨日の夜あまりにも進捗が出ないので、寝てしまおうと思ったらこんな感じのツイートを不覚にも見てしまいました。

ちょっと興味を引かれたので、さっとEmacsポートを作る気になりました。 今のSolarizedなら簡単にできるはずです。

「今の」というのは、私が出したパッチ「[new feature] solarized.el as multi color scheme」が入ったSolarized。

いろいろ議論を大きくしてしまいましたが、結局私のコードは元気に動いており、私もいつも自作カラーパレットでSolarizedを使っています。

今回、iceberg-theme.elをどのように作ったのか、まとめておきます。

成果物はこちら。iceberg-theme.el

前提知識

  • Emacsにおけるface

    face とは下記のように defface で各パッケージで定義されており、ユーザーのディスプレイ環境に応じて様々な色を設定できるようになっています。

    下記の face の例において、 feather の例では backgrounddarklight かで色の分岐を行なっていますし、Emacs標準添付パッケージの isearch では background 判定に加え、最大表示色数も考慮されています。

    (defface feather-dashboard-header
      '((((background dark))
         :background "#223377"
         :foreground "white"
         :weight bold :height 1.3 :family "Sans Serif")
        (((background light))
         :background "#abd7f0"
         :foreground "black"
         :weight bold :height 1.3 :family "Sans Serif"))
      "Face for feather-dashboard header."
      :group 'feather)
    
    (defface isearch-fail
      '((((class color) (min-colors 88) (background light))
         (:background "RosyBrown1"))
        (((class color) (min-colors 88) (background dark))
         (:background "red4"))
        (((class color) (min-colors 16))
         (:background "red"))
        (((class color) (min-colors 8))
         (:background "red"))
        (((class color grayscale))
         :foreground "grey")
        (t (:inverse-video t)))
      "Face for highlighting failed part in Isearch echo-area message."
      :version "23.1")
    

    あとはこの face を言語をパース後、ある意味のところに貼り付けたり、オーバーレイである文字のところに適用したりして使います。

  • Emacsにおけるカラーテーマ

    Emacsのカラーテーマはこれらパッケージが提供する face を一括で変更してしまうものです。

    具体例はemacs-jp/replace-colorthemesにたくさんあります。

    このように face の名前と対応するスタイルを逐一変更するものがEmacsのカラーテーマです。

    例えばaalto-dark-themeの冒頭はこのようになっています。

    (deftheme aalto-dark
      "aalto-dark theme")
    
    (custom-theme-set-faces
     'aalto-dark
    
     '(default ((t (:background "DeepSkyBlue3" :foreground "white"))))
     '(mouse ((t (:foreground "black"))))
     '(cursor ((t (:background "yellow"))))
     '(border ((t (:foreground "black"))))
    
     '(bold ((t (:bold t :background "blue3" :foreground "white"))))
     '(bold-italic ((t (:italic t :bold t :foreground "blue3"))))
     '(calendar-today-face ((t (:underline t))))
     '(diary-face ((t (:foreground "red"))))
     '(font-lock-builtin-face ((t (:foreground "LightSteelBlue"))))
     '(font-lock-comment-face ((t (:foreground "OrangeRed"))))
     ;; ...
     )
    
    (provide-theme 'aalto-dark)
    

    カラーテーマパッケージがどのようなことをしているか掴めてきましたでしょうか。

  • カラーテーマの問題点

    さて、Emacsの face は先程のようにパッケージごとに定義されています。 もし、パッケージが増えた場合、カラーテーマはどのように対応するべきでしょうか。

    簡単ですね。新しいパッケージが増えたら、その都度 face の設定を書き足すのです。。

    例えばandreas-themeでは gnusparen というパッケージに対応していることが分かります。

    このようにカラーテーマごとに「対応しているパッケージ」が異なっており、メンテナンスされていないカラーテーマを使っていると、パッケージが決めたデフォルトの色がそのまま表示されることになります。

    そのため「色が暗い」「色がビビットすぎる」「そもそも見えない」などの問題が発生します。

Solarized

  • Solarizedのface定義

    Emacsコミュニティによって長年メンテナンスされているSolarizedはどうなっているでしょうか。

    実際のファイルはsolarized-emacs/solarized-faces.elです。

    face の再定義は2000行に及び、もし新しいパッケージが生まれたとしても有志の手によって速やかに追加されています。

    これをSolarizedだけで使うのはもったいありません。

    Solarizedも内部では基本となる10色を定義し、その色で face を再定義しています。

    では、基本となる10色を外部から指定できるようにしたらSolarizedの仕組みに乗った上で簡単にカラーテーマが作れるのではないか。

    それを提案したのがzk_phiさんの「[Emacs] メタ・カラースキームとしての solarized」であり、私のパッチはそれを本家で実現したものです。

  • Solarizedから作るカラーテーマ

    SolarizedのAPIで使いやすいのは solarized-create-theme-file-with-palette です。 この関数は最暗色、最明色に加え、アクセントカラー8色を指定することでSolarizedの face 定義を使うカラーテーマファイルを生成し、 .emacs/themes 以下に保存します。

    早速icebergからアクセントカラーを貰ってカラーテーマを生成しましょう!

    (solarized-create-theme-file-with-palette 'dark 'solarized-iceberg-dark
      '("#161821" "#c6c8d1"
        "#e2a478" "#e27878" "#e27878" "#a093c7" "#b4be82" "#84a0c6" "#89b8c2" "#84a0c6"))
    

    上記で書いた色はVim Scriptのソースからではなく講演資料から色を貰いました。

    なお、icebergではアクセントカラーが6色のようなので、Solarizedが求めているアクセントカラー8色に足りません。そのため2色をダブらせて生成しました。

    関数を実行して .emacs/themes 以下に保存されたテーマファイルは load-theme でロードできます。

微調整の仕方

Emacsの本文部分は default という face が当てられているのですが、solarizedのデフォルトでは文字色が最明色になりません。。

まずそこからカスタマイズしていきます。

solarized-create-theme-file-with-palette の第3引数にS式のリストを渡すことでテーマのカスタマイズができます。

(solarized-create-theme-file-with-palette 'dark 'solarized-iceberg-dark
  '("#161821" "#c6c8d1"
    "#e2a478" "#e27878" "#e27878" "#a093c7" "#b4be82" "#84a0c6" "#89b8c2" "#84a0c6")
  '((custom-theme-set-faces
     theme-name
     `(default ((,class (:foreground ,base3 :background ,base03)))))))

カスタマイズはSolarizedの内部で色がどのように管理されているかを少し知っておく必要があります。

基本的に次のように色の名前が管理されています。

<brightest                                 darkest>
base03 base02 base01 base00 base0 base1 base2 base3

<accent colors>
yellow orange red magenta violet blue cyan green

さらに色のバリエーションとして yellow-dyellow-l という色も定義されており、第3引数で使用できます。

これらは solarized-create-color-palette という関数の中で自動生成されています。

完成形

本家のiceberg.vimのスクショと比較しながら、間違っている facedescribe-face で確認して直す。。という作業を繰り返すと徐々に本家に近づいていきます。

最終的に次のような形になりました。

(defun iceberg-theme-create-theme-file ()
  "Create iceberg-theme color theme using solarized API.

Solarized pallete.

brightest                                   darkest
base03 base02 base01 base00 base0 base1 base2 base3

yellow orange red magenta violet blue cyan green"
  (solarized-create-theme-file-with-palette 'dark 'solarized-iceberg-dark
    '("#161821" "#c6c8d1"
      "#e2a478" "#e27878" "#e27878" "#a093c7" "#b4be82" "#84a0c6" "#89b8c2" "#84a0c6")
    '((custom-theme-set-faces
       theme-name
       `(default ((,class (:foreground ,base3 :background ,base03))))
       `(vertical-border ((,class (:foreground ,base03))))
       `(mode-line ((,class (:foreground ,base2 :background ,base02))))
       `(mode-line-inactive ((,class (:foreground ,base0 :background ,base03))))

       `(font-lock-comment-delimiter-face ((,class (:foreground "#6b7089"))))
       `(font-lock-comment-face ((,class (:foreground "#6b7089"))))
       `(font-lock-preprocessor-face ((,class (:foreground ,green)))) ; yellow
       `(font-lock-type-face ((,class (:foreground ,cyan)))) ; yellow
       `(font-lock-builtin-face ((,class (:foreground ,green)))) ; base0

       `(diff-function ((,class (:foreground ,violet-1fg))))
       `(diff-header ((,class (:foreground ,green))))
       `(diff-hunk-header ((,class (:foreground ,green))))
       `(diff-file-header ((,class (:background ,base03 :foreground ,green))))
       `(diff-added ((,class (:background ,violet-1bg :foreground ,violet-1fg))))
       `(diff-indicator-added ((t (:foreground ,violet))))
       `(markdown-header-face ((,class (:foreground ,yellow))))
       `(markdown-header-rule-face ((,class (:foreground ,green))))
       `(markdown-markup-face ((,class (:inherit default))))
       `(markdown-url-face ((,class (:foreground ,magenta))))
       `(markdown-link-face ((,class (:foreground ,green :underline t))))
       `(markdown-inline-code-face ((,class (:foreground ,cyan))))
       `(markdown-pre-face ((,class (:foreground ,cyan))))
       `(sh-quoted-exec ((,class (:foreground ,violet))))
       `(haskell-type-face ((,class (:inherit default))))
       `(haskell-constructor-face ((,class (:inherit default))))
       `(haskell-operator-face ((,class (:foreground ,green))))
       `(haskell-definition-face ((,class (:inherit default))))
       `(web-mode-block-delimiter-face ((,class (:inherit default))))
       `(web-mode-html-attr-value-face ((,class (:foreground ,cyan))))
       `(web-mode-mode-type-face ((,class (:inherit default))))
       `(web-mode-function-call-face ((,class (:inherit default))))
       `(web-mode-keyword-face ((,class (:foreground ,green))))
       `(web-mode-constant-face ((,class (:foreground ,cyan))))
       `(web-mode-variable-name-face ((,class (:foreground ,cyan))))
       `(web-mode-html-tag-bracket-face ((,class (:foreground ,green))))
       `(org-verbatim ((,class (:foreground ,cyan))))
       `(php-php-tag ((,class (:inherit default))))
       `(php-constant ((,class (:inherit default))))
       `(php-paamayim-nekudotayim ((,class (:foreground ,green))))
       `(php-object-op ((,class (:foreground ,cyan))))
       `(php-variable-name ((,class (:foreground ,cyan))))
       `(php-variable-sigil ((,class (:foreground ,cyan))))))))

テーマを作る関数にしているのは「requireしただけでEmacsの動作を変えない」というパッケージ作成のルールに従うためです。

これでユーザーはiceberg-themeをインストール後、 iceberg-theme-create-theme-file を実行した後で M-x load-theme solarized-iceberg-dark と実行できるはずです。

まとめ

現在ではSolarizedの資産を生かし、簡単にカラーテーマを作成することができます。

ぜひ独創的かつ目にやさしいカラーテーマを作っていただき、日々のEmacs生活がよりよりものになれば良いなと思っています。

DONE Archのrsyncで転送できないときに確認すること

Intro

celpa.conao3.comは私が契約している100GBの「さくらのレンタルサーバー」を向いているのだけど、その公開ディレクトリにGitHub Actionsからrsyncでデプロイしたい。

もちろん、GitHubのワーカーに秘密鍵をアップロードすることになるので、その秘密鍵は上手い具合に制限を設ける必要があります。

ローカルで試していたとき上手くいかなかったので、そのメモ。

普通のrsync

rsyncは接続先のサーバーにrsyncを起動して、そのプロセスと通信します。

どんなコマンドが実行されているかは -vvv オプションを付けることで表示されます。

$ rsync -vvv -an README.md conao3@conao3.sakura.ne.jp:/home/conao3/
opening connection using: ssh -l conao3 conao3.sakura.ne.jp rsync --server -vvvnlogDtpre.iLsfxC . /home/conao3/  (9 args)
sending incremental file list
[sender] make_file(README.md,*,0)
...

sent 60 bytes  received 368 bytes  285.33 bytes/sec
total size is 15,379  speedup is 35.93 (DRY RUN)
[sender] _exit_cleanup(code=0, file=main.c, line=1189): about to call exit(0) (DRY RUN)

opening connection using の行の rsync 以降が接続先で実行されているコマンドです。

そのため、そのコマンドだけ実行できる鍵を登録するには以下のように authorized_keys に登録します。 (-vvv はいらないので削る)

command="rsync --server -nlogDtpre.iLsfxC . /home/conao3/",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-rsa AAAA...

rsyncdを使ったrsync

  • 鍵の作成

    rsync用の鍵を新しく作る。 GitHub Actionsのsshが古く、Openssh形式の秘密鍵が読めなかったのでPEM形式で出力するようにオプションを付けた。

    鍵の名前は sakura-rsync_rsa と入力した。

    cd .ssh
    ssh-keygen -t rsa -b 4096 -m PEM -C ""
    
  • 鍵の登録

    ファイルをやりとりしたいサーバーにログインし、 authorized_keys に公開鍵 sakura-rsync_rsa.pub を登録する。

    command="rsync --server --daemon --config=/home/conao3/.rsyncd.conf .",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-rsa AAAA...
    
  • rsyncd.confの作成

    私が使っているのは共有サーバーなので、当然、ユーザーホーム以下しか触れない。 そのため rsyncd.conf~/.rsyncd.conf に作成することにしました。

    use chroot = no
    read only = yes
    
    [www]
      read only = no
      write only = yes
      path = /home/conao3/www
    
    [cache]
      path = /home/conao3/www/files/cache-40bbae27
    
  • 使ってみる

    なぜかファイル転送されない。。。

    $ rsync -vvv -e "ssh -i sakura-rsync_rsa" /home/conao/dev/template/elisp conao3@conao3.sakura.ne.jp::foo/
    opening connection using: ssh -i sakura-rsync_rsa -l conao3 conao3.sakura.ne.jp rsync --server --daemon .  (10 args)
    sending daemon args: --server -vvve.LsfxC . foo/  (4 args)
    skipping directory elisp
    send_file_list done
    send_files starting
    send_files phase=1
    send_files phase=2
    send files finished
    total: matches=0  hash_hits=0  false_alarms=0 data=0
    
    sent 16 bytes  received 11 bytes  18.00 bytes/sec
    total size is 0  speedup is 0.00
    [sender] _exit_cleanup(code=0, file=main.c, line=1189): about to call exit(0)
    

    -a オプションを付けると転送された。

    $ rsync -a -vvv -e "ssh -i sakura-rsync_rsa" /home/conao/dev/template/elisp conao3@conao3.sakura.ne.jp::foo
    opening connection using: ssh -i sakura-rsync_rsa -l conao3 conao3.sakura.ne.jp rsync --server --daemon .  (10 args)
    sending daemon args: --server -vvvlogDtpre.iLsfxC . foo/  (4 args)
    sending incremental file list
    [sender] make_file(elisp,*,0)
    send_file_list done
    [sender] pushing local filters for /home/conao/dev/template/elisp/
    [sender] make_file(elisp/README.org,*,2)
    [sender] make_file(elisp/Makefile,*,2)
    [sender] make_file(elisp/elisp-template.el,*,2)
    [sender] make_file(elisp/.github,*,2)
    [sender] make_file(elisp/elisp-template-tests.el,*,2)
    [sender] make_file(elisp/git-hooks,*,2)
    [sender] make_file(elisp/.gitignore,*,2)
    [sender] make_file(elisp/LICENSE,*,2)
    [sender] make_file(elisp/Cask,*,2)
    send_files starting
    
  • GitHub Actionsから使う

    こんなファイルを .github/workflows/test.yml に保存すればできる。

    GitHub ActionsでSSHを使う - Qiita」で紹介されていた shimataro/ssh-key-action を使わせてもらいました。

    name: Main workflow
    on:
      push:
        branches:
          - master
      pull_request:
        branches:
          - master
    
    jobs:
      build:
        runs-on: ubuntu-latest
        strategy:
          matrix:
            emacs_version:
              - '26.3'
        steps:
          - uses: actions/checkout@v1
          - uses: actions/setup-python@v1.1.1
          - uses: purcell/setup-emacs@master
            with:
              version: ${{ matrix.emacs_version }}
          - uses: conao3/setup-cask@master
          - name: Install SSH key
            uses: shimataro/ssh-key-action@v1
            with:
              private-key: ${{ secrets.SSH_KEY }}
              public-key: ${{ secrets.SSH_KEY_PUBLIC }}
              known-hosts: ${{ secrets.KNOWN_HOSTS }}
    
          - name: Cache fetch
            run: |
              rsync -av --progress -e 'ssh' conao3@conao3.sakura.ne.jp::cache/celpa.tar.gz /tmp/
              rm -rf working
              tar xf /tmp/celpa.tar.gz
    
          - name: Build
            run: |
              make
    
          - name: Deploy
            run: |
              rsync -av -e 'ssh' --delete --exclude=working ./ conao3@conao3.sakura.ne.jp::www/celpa/
    
          - name: Cache push
            run: |
              rm /tmp/celpa.tar.gz
              tar czf /tmp/celpa.tar.gz working
              rsync -av --progress -e 'ssh' /tmp/celpa.tar.gz conao3@conao3.sakura.ne.jp::cache/
    

まとめ

rsyncを使ってサイトをGitHub Actionsからデプロイできるようになりました。

参考

DONE CELPA (Conao3’s Emacs Lisp Package Archive) をデプロイした話

Intro

MELPAのレビューが遅い。

MELPAのメンテナの方には感謝してしきれないが、実際のところ、レシピ追加PRを出して2週間から3週間音沙汰ないまま放置されたり、先方のレビューを反映して修正しても、そこから2週間、3週間待たされることがままあり、ストレスを抱えていました。

私はあまりinit.elを大きくしたくなく、ある程度の大きさになるとパッケージで切りだして (leaf hoge) で簡単にアクティベートしたいと考えているため、MELPAに入れてもらえないと el-get で取ってこないといけませんでした。

ここでさらに問題があり、 el-getpackage との相性が悪く、微妙な評価タイミングの違いがとても重要になります。

init.elを自分で全てコントロールしていたとしても、おまじないの部分が増えるし、leaf-managerで自動管理している場合は原理的に不可能でした。

そのためMELPAっぽく、さらにレシピの追加を自分でコントロールできるアーカイブが必要となり、今回、celpa.conao3.comをデプロイしました。

MELPAの仕組み

MELPAは単に利用するもので、自分でMELPAを作りたい場合、どこから手をつけたら良いか分かりませんでした。

ただ、takaxpさんがwikiを翻訳して下さっており、この通り進めればなんとかなりました。

もちろんwiki(Custom Melpa Archive, Melpa and package build)も参照しました。

MELPAソースの修正

  • レシピの削除

    MELPAのクローンを作ろうとしているわけではないので、MELPAに既に登録されているレシピは必要ありません。 全部削除してしまいます。

  • レシピの追加

    欲しいパッケージのレシピをおもむろに追加します。もちろんMELPAの形式と一緒のものです。

  • index.htmlの修正

    いい感じにindex.htmlを修正します。 おそらくjsonが足らない(CELPAではダウンロード数の記録はしてない)らしく、MELPAに付属しているjavascriptが動きません。

    適当にjsを書いたり、リンクを修正したりします。

  • GitHub Actionsの設定ファイル追加

    GitHub Actionsで30分ごとにビルドが走るようにします。

    name: Main workflow
    on:
      push:
        branches:
          - master
      pull_request:
        branches:
          - master
      schedule:
        - cron: '*/30 * * * *'
    
    jobs:
      build:
        runs-on: ubuntu-latest
        strategy:
          matrix:
            emacs_version:
              - '26.3'
        steps:
          - uses: actions/checkout@v1
          - uses: actions/cache@v1
            with:
              path: working
              key: ${{ runner.os }}-working
          - uses: actions/setup-python@v1.1.1
          - uses: purcell/setup-emacs@master
            with:
              version: ${{ matrix.emacs_version }}
          - uses: conao3/setup-cask@master
    
          - name: Build
            run: |
              git status
              make add-package-build-remote
              make pull-package-build
              make
          - name: Install SSH key
            if: github.ref == 'refs/heads/master'
            uses: shimataro/ssh-key-action@v1
            with:
              private-key: ${{ secrets.SSH_KEY }}
              public-key: ${{ secrets.SSH_KEY_PUBLIC }}
              known-hosts: ${{ secrets.KNOWN_HOSTS }}
    
          - name: Deploy
            if: github.ref == 'refs/heads/master'
            run: |
              rsync -av -e 'ssh' --delete --exclude=.git --exclude=working ./ conao3@conao3.sakura.ne.jp::www/celpa/
    

まとめ

振り返ってみるとMELPAをCELPAとして動かせるように修正するのは簡単でした。

やはりミスが許されず、時間がかかるのは秘密鍵/権限回りで、その部分で半分以上の時間を費しました。

レシピを直接pushできるというのはMELPAを使っていた頃には考えられない快適さです。

ぜひみなさんもオレオレMELPAをデプロイして、package.elだけでパッケージを管理できるようにしましょう!

DONE Emacsで競技プログラミング | ojのラッパーパッケージを書いた話

Intro

ちょっとEmacsで競技プログラミングをしてみようかと思いました。

どうやらojoj-templateという便利なツールがあるようなので、Emacsから簡単に実行できるようoj.elというパッケージを作成しました。

oj, oj-template

ojは多数のオンラインジャッジに対応しているCLIツールです。

オンラインジャッジによって実装されている機能に制限はありますが、サンプルケースのダウンロード、テスト、提出、ランダムインプットの生成などの機能があります。

さらにoj-templateは入出力サンプルからそれっぽいcppとpythonファイルを生成してくれます。

また、 oj-prepare はコンテストURLを指定すると、そのコンテストの全ての問題について oj donwloadoj-template をしてくれます。

コンテスト中は時間との戦いなので、これらのコマンドによる時短は大きなものになると思います。

oj.el

oj.elは先般の oj, oj-template をEmacsから実行できるようにしたものです。

(leaf oj
  :ensure t
  :custom ((oj-compiler-c . "clang")
           (oj-compiler-python . "pypy")
           (oj-default-online-judge . "atcoder")))

単に関数にバインドしただけでなく、多少の改善を入れています。

例えば oj はダウンロードするためにURL全体を指定する必要がありますが、 oj-default-online-judge で設定されているオンラインジャッジを規定のものと考え、入力を省略できるようにしています。 (CLIツールとしての方針と干渉するものではなく、エディタの拡張としての求められる機能として盛りこんだものです。)

例えば atcoder デフォルトに設定されている状況で、 M-x oj-prepareabc167 を入力すると下記コマンドが実行されます。

oj-template https://atcoder.jp/contests/abc167

さらに、 abc167/a と入力すると下記コマンドが実行されます。

oj-template https://atcoder.jp/contests/abc167/tasks/abc167_a

このように推測できる部分については、より推測するように実装しています。

詳しい使い方についてはREADMEを参照して下さい。 基本的に oj-install-package, oj-prepare, oj-test, oj-submit の順にコマンドを実行すればいい感じに動くはずです。

まとめ

ojをEmacsから使いやすくするラッパーパッケージを書きました。

こういう方向のパッケージもお手軽で良いですね。

DONE Emacsで最終行にポイントを動かさないようにするアドバイス

Intro

Elispのadviceはとても便利。

末尾にポイントを移動しない

Emacs-jpのスレッドで次のような質問があった。

こんにちは! 2 行のファイルがあったとき、 3 行目にカーソル移動できないようにしたいです 何か方法は無いでしょうか

このような細かい挙動はアドバイスで簡単に実現できる。

(leaf simple
  :preface
  (defun c/advice-next-line (fn &rest args)
    (let ((maxline (count-lines (point-min) (point-max))))
      (if (< maxline (line-number-at-pos (line-beginning-position 2)))
          (message "restricted! (next-line)")
        (apply fn args))))
  (defun c/advice-forward-char (fn &rest args)
    (let ((maxline (count-lines (point-min) (point-max))))
      (if (< maxline (line-number-at-pos (+ (point) 1)))
          (message "restricted! (forward-char)")
        (apply fn args))))
  :advice ((:around next-line c/advice-next-line)
           (:around forward-char c/advice-forward-char)))

(leaf simple
  :advice-remove ((next-line c/advice-next-line)
                  (forward-char c/advice-forward-char)))

末尾にポイントを移動しようとしたら改行する

さらに、仕様を変えて、「最終行以上にポイントを動かそうとしたときに、自動で改行を入れる」ことも簡単に実現できる。

(leaf simple
  :preface
  (defun c/advice-next-line (fn &rest args)
    (let ((maxline (count-lines (point-min) (point-max))))
      (if (< maxline (line-number-at-pos (line-beginning-position 2)))
          (progn (goto-char (line-beginning-position 2))
                 (save-excursion (insert "\n")))
        (apply fn args))))
  (defun c/advice-forward-char (fn &rest args)
    (let ((maxline (count-lines (point-min) (point-max))))
      (if (< maxline (line-number-at-pos (+ (point) 1)))
          (insert "\n")
        (apply fn args))))
  :advice ((:around next-line c/advice-next-line)
           (:around forward-char c/advice-forward-char)))

(leaf simple
  :advice-remove ((next-line c/advice-next-line)
                  (forward-char c/advice-forward-char)))

これらのアドバイスで「ファイル末尾に必ず改行がある」ことを強制できる。

まとめ

ただ、今回の要求については「ファイル保存時に改行がない場合、末尾改行を付加する」というオプションがあり、私はこれを使っている。

(leaf files
  :custom `((require-final-newline . t)))

とはいえ、このようなオプションがないときElispのアドバイスはとても便利。

細かい挙動がストレスになることもあるので、簡単にカスタマイズできるEmacsは良いエディタ。

DONE 制御記号付きのmake-processの出力をいい感じに処理する方法

Intro

Emacsからプロセスを実行できます。

実行する方法はいろいろありますが、最も低レベルな関数は make-process です。

最もユーザーに近い関数は shell-command で、 M-! に割り当てられています。 この関数はコマンド文字列を渡すだけで、お手軽に実行できます。

make-process

make-processは非同期プロセスを作成します。

必須のオプションは :name で、プロセスオブジェクトを返却します。 さらに :buffer, :command, :coding, :connection-type, :noquery, :stop, :filter, :sentinel, :stderr オプションが存在し、細かくプロセスの管理方法を制御できます。

基本的に :name, :buffer, :command を指定することになります。

(setq proc
      (make-process
       :name "sh"
       :buffer (get-buffer-create "*proc*")
       :command '("hub" "clone" "conao3/celpa")))

(progn
  (delete-process proc)
  (shell-command "rm -rf celpa"))

ただ、コマンドから送られてきた文字列を素朴に出力するため、gitのように CR を利用してリッチに出力するコマンドの出力は盛大に壊れます。

CR を解釈しながら賢く処理をすれば良いのですが、とても面倒です。

Emacs-jpで助けを求めると、eshellの関数を利用すれば良いのではという助言があり、調べてみると、きちんと出力できました。

eshellの仕組みに乗っかるので、 eshell-mode にしなければフィルター関数を使えないかと思いましたが、よく見ればいくつかのマーカーを設定するだけで使えました。

(with-current-buffer (get-buffer-create "*proc*")
  (set (make-local-variable 'eshell-last-input-start) (point-marker))
  (set (make-local-variable 'eshell-last-input-end) (point-marker))
  (set (make-local-variable 'eshell-last-output-start) (point-marker))
  (set (make-local-variable 'eshell-last-output-end) (point-marker))
  (set (make-local-variable 'eshell-last-output-block-begin) (point)))

(setq proc
      (make-process
       :name "sh"
       :buffer (get-buffer-create "*proc*")
       :command '("hub" "clone" "conao3/celpa")
       :filter (lambda (proc string)
                 (eshell-output-filter proc string))))

(progn
  (delete-process proc)
  (shell-command "rm -rf celpa"))

まとめ

Emacsは良いエディタで、Elispは強力ですが、GitをElispで実装するのは現実的ではありません。

このように外部コマンドとElispを有効に協調させながら作業しやすい環境を整えていきましょう。

DONE リストに関数を適用して最初にnon-nilを返した値を得る方法

このような関数をさくっと使いたい。 3つのポイントがあり、「リストに関数を適用して」「最初にnon-nilが返ってきた」「その値を得る」ということ。

2つめのポイントまでなら cl-find-if がそのまま使える。 cl-find-if は「リストに関数を適用して」「最初にnon-nilが返ってきた」「元の値を得る」ことができる。

(cl-find-if (lambda (elm) (when (<= 3 elm) (* elm elm))) '(1 2 3 4))
;;=> 3

私が欲しいのはこういう関数です。

(defun first-value (fn lst)
  "Apply FN for LST and return first non-nil value."
  (funcall fn (cl-find-if fn lst)))
;;=> first-value

(first-value (lambda (elm) (when (<= 3 elm) (* elm elm))) '(1 2 3 4))
;;=> 9

Emacs-jpで聞いたところ、koshさんに seq-some を教えてもらいました。

(seq-some (lambda (elm) (when (<= 3 elm) (* elm elm))) '(1 2 3 4))
;;=> 9

便利。

DONE Lispでスペシャルフォームのorやandをapplyする方法

orandapply したい。

つまり、いろんな操作によって得られたリストに対して orand がとりたい。

河合さんに every を教えてもらいました。

(mapcar #'featurep '(emacs leaf use-package-leaf))
;;=> (t t nil)

(cl-every #'identity (mapcar #'featurep '(emacs leaf use-package-leaf)))
;;=> nil

(cl-every #'identity (mapcar #'featurep '(emacs leaf)))
;;=> t

たしかに上手くいっている。 everyand 相当だったので、 someor 相当となる。

(cl-some #'identity (mapcar #'featurep '(emacs leaf)))
;;=> t

(cl-some #'identity (mapcar #'featurep '(use-package-leaf use)))
;;=> nil

もちろん、今回の方法はスペシャルフォームではないので、評価してはいけない式が後続している場合、普通にエラーになる。

(or nil t (/ 1 0))
;;=> t

(cl-some #'identity (list nil t (/ 1 0)))
;;=> Debugger entered--Lisp error: (arith-error)
;;     /(1 0)
;;     (list nil t (/ 1 0))
;;     (cl-some (function identity) (list nil t (/ 1 0)))
;;     eval((cl-some (function identity) (list nil t (/ 1 0))) nil)
;;     scratch-comment--elisp--eval-last-sexp(t)
;;     scratch-comment--eval-last-sexp(t)
;;     scratch-comment-eval-sexp()
;;     funcall-interactively(scratch-comment-evalp-sexp)
;;     call-interactively(scratch-comment-eval-sexp nil nil)
;;     command-execute(scratch-comment-eval-sexp)

TODO base64の画像をEmacsで表示する方法

ネタ元はROCKTAKEYさんのEmacs-jpでの発言。 (create-image (base64-decode-string “base64エンコードの画像”) nil t :width nil)

DONE Emacsに存在するwithプレフィックスのマクロ一覧

with マクロは便利。

with-temp-bufferwith-current-buffer は超基本マクロだし、helmなども with-helm-buffer という同じ文脈のマクロを提供している。

ふと、 with プレフィックスのマクロがどれだけEmacsに用意されているのか気になったので調べた。

ppp, promise, asyncに依存して、次のコード片を実行すれば調べられる。

31シンボル見つかった。

emacs-version
;;=> "26.3"

(ppp-list
 (promise-wait-value
  (promise-wait 5
    (promise:async-start
     (lambda ()
       (let (res)
         (mapatoms
          (lambda (elm)
            (when (and (fboundp elm)
                       (string-prefix-p "with-" (symbol-name elm)))
              (push elm res))))
         (sort res (lambda (a b) (string< (symbol-name a) (symbol-name b))))))))))
;;=> (with-auto-compression-mode
;;    with-case-table
;;    with-category-table
;;    with-coding-priority
;;    with-connection-local-profiles
;;    with-current-buffer
;;    with-current-buffer-window
;;    with-demoted-errors
;;    with-displayed-buffer-window
;;    with-electric-help
;;    with-eval-after-load
;;    with-file-modes
;;    with-help-window
;;    with-isearch-suspended
;;    with-local-quit
;;    with-mutex
;;    with-no-warnings
;;    with-output-to-string
;;    with-output-to-temp-buffer
;;    with-selected-frame
;;    with-selected-window
;;    with-silent-modifications
;;    with-syntax-table
;;    with-temp-buffer
;;    with-temp-buffer-window
;;    with-temp-file
;;    with-temp-message
;;    with-timeout
;;    with-timeout-suspend
;;    with-timeout-unsuspend
;;    with-wrapper-hook)

先のコード片で面倒なことをやっているのは、いわゆる emacs -q で実行した結果を得たいため。 今起動しているEmacsでの結果が欲しい場合はpromiseを消せば良い。

82シンボル見つかった。

(ppp-list
 (let (res)
   (mapatoms
    (lambda (elm)
      (when (and (fboundp elm)
                 (string-prefix-p "with-" (symbol-name elm)))
        (push elm res))))
   (sort res (lambda (a b) (string< (symbol-name a) (symbol-name b))))))
;;=> (with-auto-compression-mode
;;     with-buffer-modified-unmodified
;;     with-case-table
;;     with-category-table
;;     with-coding-priority
;;     with-connection-local-profiles
;;     with-current-buffer
;;     with-current-buffer-window
;;     with-decoded-time-value
;;     with-demoted-errors
;;     with-displayed-buffer-window
;;     with-editor
;;     with-editor*
;;     with-editor--setup
;;     with-editor-async-shell-command
;;     with-editor-cancel
;;     with-editor-debug
;;     with-editor-emacsclient-path
;;     with-editor-emacsclient-version
;;     with-editor-emulate-terminal
;;     with-editor-export-editor
;;     with-editor-export-git-editor
;;     with-editor-export-hg-editor
;;     with-editor-finish
;;     with-editor-kill-buffer-noop
;;     with-editor-locate-emacsclient
;;     with-editor-locate-emacsclient-1
;;     with-editor-mode
;;     with-editor-output-filter
;;     with-editor-process-filter
;;     with-editor-read-envvar
;;     with-editor-return
;;     with-editor-server-window
;;     with-editor-set-process-filter
;;     with-editor-shell-command
;;     with-editor-shell-command-read-args
;;     with-editor-usage-message
;;     with-electric-help
;;     with-eval-after-load
;;     with-feather-dashboard-buffer
;;     with-file-modes
;;     with-helm-after-update-hook
;;     with-helm-alive-p
;;     with-helm-buffer
;;     with-helm-current-buffer
;;     with-helm-default-directory
;;     with-helm-display-marked-candidates
;;     with-helm-in-frame
;;     with-helm-quittable
;;     with-helm-temp-hook
;;     with-helm-time-after-update
;;     with-helm-window
;;     with-help-window
;;     with-httpd-buffer
;;     with-isearch-suspended
;;     with-ivy-window
;;     with-local-quit
;;     with-lsp-workspace
;;     with-lsp-workspaces
;;     with-mutex
;;     with-no-warnings
;;     with-output-to-string
;;     with-output-to-temp-buffer
;;     with-parsed-tramp-file-name
;;     with-ppp--working-buffer
;;     with-selected-frame
;;     with-selected-window
;;     with-silent-modifications
;;     with-slots
;;     with-syntax-table
;;     with-temp-buffer
;;     with-temp-buffer-window
;;     with-temp-file
;;     with-temp-message
;;     with-timeout
;;     with-timeout-suspend
;;     with-timeout-unsuspend
;;     with-tramp-connection-property
;;     with-tramp-file-property
;;     with-tramp-progress-reporter
;;     with-vc-properties
;;     with-wrapper-hook)

DONE Emacsでプレフィックスキーにバインドする方法

ネタ元はEmacs-jp。

namnium 06:43 すみません、とても小さい質問なのですが、検索ワードが思いつかなかったので質問させてください。 yasnippetでキーバインド C-x i i をスニペット挿入に割り当てたところ、今まで使用していた insert-file C-x i を打てなくなってしまったのですが、このようにキーバインドを途中で打ち切りたくなった時はどうすればいいのでしょうか? とりあえず現在は M-x insert-file で諦めています…

conao3 08:18 インタラクティブなら、C-x i iにnilをバインドした後にC-x iを再バインドします。 インタラクティブでなくていいなら、init.elの該当行を削除して再起動するだけです。

namnium 09:48 両立って無理な感じですか?

conao3 09:50 うーん、、独自にキーを待つ関数をC-x iに登録したら実現できるかもしれませんが、、

ということでEmacsにできないことはないので、実装してみた。

(leaf async-await
  :ensure t
  :config
  (leaf c/insert-file
    :leaf-defer nil
    :pre-setq ((c/insert-file-enable . t)
               (c/insert-file-delay . 1.0))
    :bind (("C-x i" . nil)
           ("C-x i i" . yas-insert-snippet))
    :config
    (funcall
     (async-lambda ()
       (while c/insert-file-enable
         (await (promise:delay c/insert-file-delay))
         (warn "%s" (prin1-to-string
                     (which-key--this-command-keys)))
         (when (equal [24 105] (which-key--this-command-keys))
           (warn "Detect: C-x i")
           (await (promise:delay c/insert-file-delay))
           (when (equal [24 105] (which-key--this-command-keys))
             (warn "Redetect: C-x i")
             (call-interactively #'insert-file))))))))
(setq c/insert-file-enable nil)

async-awaitとwhich-keyに依存しています。

ループを止める手段が用意されてないので、外から c/insert-file-enablenil に設定することで止めます。

あと call-interactivelyquit すると無限ループが止まってしまうんですが、なぜだろう。。

C-x i を打って、Redetectされる前に i を素早く入力すると、 yas-insert-snippet が実行されます。 Redetectされると insert-file が実行されます。

DONE Emacsのキーマップlookup順序について

元記事アーカイブのアーカイブ。なにか問題があれば教えて下さい。以下、アーカイブ。


何となく忘れてしまうのでメモ。

Info の 22.8 Searching the Active Keymaps, 22.9 Controlling the Active Keymaps などから。

「belong」は誰がその変数を管理するかとか、変数の所属先という感じ。

基本

全バッファ共通は global-map 、メジャーモードは local-map を設定する。

マイナーモードはまとめて minor-mode-map-alist に入っていて、マイナーモードの変数が onoff で有効無効が決まる。 このリストの順番も重要。

テキストには文字単位で keymap と言う属性をつけて、キーバインドを設定できる。

あと、これらのどれにもヒットしなければ、 local-function-key-map でキーの変換が行われる。 例えば、環境によって C-i (タブ文字) と <tab> は区別されているが、実質は同じと見なした方が便利なのでここで変換される。

優先順位は見たまま。 text-property 'keymap > minor-mode-map-alist > local-map > global-map > local-function-key-map

override

この優先順位を少し覆すことが出来るしくみが overriding 関係。 CSS の important! のような感じ。

まず、メジャーモードよりマイナーモードの方が優先されてしまうので、これを覆す仕組みが minor-mode-overriding-map-alist 。 ただ、覆そうとしているマイナーモードの名前を知らないといけない。

次に、文字単位の keymap をメジャーモードやマイナーモードから覆す仕組みが overriding-local-map

最後に、端末入力の最上位で入力を乗っ取る仕組みが overriding-terminal-local-map 。 これは影響範囲がその端末だけ。

emulation-mode-map-alists は他のメジャーモードやマイナーモードが別のモードのキーマップを乗っ取ったり、 remap で挙動を変えたりする場合に使うもの。 検索してもあまり情報がない (cua, viper とかぐらい)。

DONE Emacs Lisp の文字列操作まとめ

元記事アーカイブのアーカイブ。なにか問題があれば教えて下さい。以下、アーカイブ。


基本的には Emacs Lisp の Info を読むと何となくパーツはそろっていることは分かる。 ただ、パーツが特殊だったりしてよく使い方を忘れるのでよく見る情報をメモ。

基本的情報源

以下の情報はローカルにブックマークするなどして即引けるようにしておくと便利。

  • Info 4 Strings and Characters
      1. 文字列と文字
    • 文字列の操作、比較、変換など
  • Info 32.2 Examining Buffer Contents
    • 31.2 バッファの内容を調べる
    • バッファの文字列取得
  • Info 32.19 Text Properties
    • 31.19 テキスト属性
    • テキストプロパティ(テキスト属性)のいろいろ
  • Info 34 Searching and Matching
      1. 探索と一致
    • 探索、正規表現
  • Info 38.10 Width
    • 38.9 表示幅
    • 非 ASCII 文字列を含む文字や文字列の幅(画面上の長さ)、端っこ切る
  • 逆引き Emacs Lisp 文字列
    • それなりに数が多い
  • no title
    • 実は必要なことはここに大体書いてあったりする

Ruby との文字列操作対応表

参照: 文字列操作の比較表: Ruby, Python, JavaScript, Perl, C++ - bkブログ 基本的に破壊的操作は無し。

Ruby (String)Emacs Lisp注釈
s = “abc”(setq s “abc”)変数にバインドするならlet使う方が一般的
s = x + y(concat x y)
s == x(equal s x)string-equal が正式
s % [x, y]
sprintf(s, x, y)(format s x y)
[x, y, z].join(s)(mapconcat ‘identity (list x y z) s)単に結合するなら (apply ‘concat (list x y z)) もアリ
s.capitalize(capitalize s)
s.capitalize!
s.center(x)(cfw:render-center x s)calfw.el 参照
s.chomp(regexp-in-string “[\n\r]+$” "” s)
s.chomp!
s.chop(substring s 0 -1)改行文字の扱いが違う
s.chop!
s.clear(clear-string s)
s.concat(x)
s.count(x)ループで回すか、一時バッファを使ってre-search-forward で回数を数えるとか
s.crypt(x)rot13-string とか、 EPA とか
EzCrypt
s.delete!(x)(regexp-in-string x "” s)破壊的には行えない
s.downcase(downcase s)
s.downcase!
s.each_byte { x … )(loop for x across s do … )文字単位でループ、xは文字コード(数値)文字コードからstringにするには char-to-string
s.each_line { x … )(loop for x in (split-string s “[\n\r]") do … )
s.empty?(= 0 (length s))nullチェックが必要
s.end_with?(x)(string-match (concat x “$”) s)sに改行が入らないことが前提
s.gsub(x, y)(regexp-in-string x y s)
s.gsub!(x, y)
s.hex
s.to_i(16)(string-to-number s 16)
s.include?(x)(string-match x s)もし見つかれば開始位置が返ってくる
s.index(x)(string-match x s)
s.insert(i, x)一時バッファでやるか、自分でちぎってつなげるか
s.intern
s.to_sym(intern s)シンボルテーブルに登録したくなければ make-symbol
s.length
s.size(length s)文字数が返る
s.ljust(x)(cfw:render-left x s)calfw.el 参照
s.lstrip(regexp-in-string “^[ \t]+” "” s)sに改行が入らないことが前提
s.lstrip!
s.match(x)
x.match(s)(string-match x s)マッチ結果はグローバルに保持している match-string, match-beginning, match-end 等参照
s.next!
s.succ!
s.oct
s.to_i(8)(string-to-number s 8)
s.partition(x)string-matchで自分でちぎる
s.replace(x)
s.reverse(apply ‘string (reverse (append s nil)))
s.reverse!
s.rindex(x)一時バッファで search-backward 等を使う
s.rjust(x)(cfw:render-right x s)calfw.el 参照
s.rpartition(x)string-matchで自分でちぎる
s.rstrip(regexp-in-string “[ \t]+$” "” s)sに改行が入らないことが前提
s.rstrip!
s.scan(x) { … }一時バッファで re-search-forward 等を使う
s[i]
s.slice(i)(aref s i)文字コード(数値)が返ってくる文字コードからstringにするには char-to-string
s[i..-1]
s.slice(i..-1)(substring s i)
s[i, l]
s.slice(i, l)(substring s i (+ i l))
s[i..j]
s.slice(i..j)(substring s i (1+ j))
s[i…j]
s.slice(i…j)(substring s i j)
s.split(x)(split-string s x)
s.start_with?(x)*4(string-match (concat “^” x) s)sに改行が入らないことが前提
s.strip(regexp-in-string “\\(^[ \t\n\r]+\\ [ \t\n\r]+$\)” "” s)sに改行が入らないことが前提
s.strip!
s.sub(x, y)string-match を使って自分で初回を取ってくる
s.sub!(x, y)
s.swapcase
s.to_f(string-to-number s)整数に見える場合は整数が返る
s.to_i(string-to-number s)
s.to_s(substring s 0)
s.tr!(x, y)
s.tr_s!(x, y)
s.unpack(x)bindatを使う
s.upcase(upcase s)
s.upcase!
Regexp.escape(s)
Regexp.quote(s)(regexp-quote s)

※もし間違いを見つけたら教えてください。

  • 表示幅について

    format の桁指定("%5s” など)で表示幅のパディングが出来るが、あふれた文字列は切り落とされずに全部表示されてしまう。

    指定幅で切り取りたい場合は、自分であらかじめ文字列の長さを見て切り落と(truncate)しておく必要がある。

    切り落としには substring とかではなくて、全角文字などについても適切に幅を見てくれる truncate-string-to-width を使うと良い感じ。

  • 文字列内の複雑な検索、置換など

    replace-regexp-in-string を使えば大抵のことが出来る。関数も取れるので Ruby の gsub 相当も出来る。

    ただ、複雑な正規表現をこねくり回したい場合は以下のように一時バッファを使う。

    ;; input に文字列が入っている
    ;; output に変換後の文字列を入れたい
    (setq output
      (with-temp-buffer
        (insert input)
        (goto-char (point-min))
        (while (re-search-forward REGEXP nil t)
            ;; ここで match-string などを使って何かする
         )
        (buffer-string)))
    
  • テキストプロパティについて

    HTML の DOM オブジェクトに独自属性を付加するように、自由に属性を入れることが出来る。 HTML とは違って1文字単位でプロパティが設定できるが、大抵はまとまった領域にプロパティを設定する。

    テキスト属性を使えるようになると、かなり複雑なアプリケーションが作れるようになる(と思う)。

  • アプリケーションからの利用

    テキストプロパティに必要な値をごっそり入れておき、キー操作でカーソル上の文字列からテキストプロパティを取ってきて必要なアクションを行うというような方法が一般的に行われている。大抵は、行全体や必要な領域全体にプロパティをつけておいて、キー操作で取得するときは行頭の1文字目のプロパティを見るということがよく行われている。

    Dired や orgmode など、多くのプログラムがこのような設計になっている。しかしながら、場当たり的にテキストプロパティを使い始めると、テキストプロパティを舞台にしたスパゲティコードになるため、本格的に使う場合はあらかじめ構造化したデータを代表して入れておく等の工夫をした方がよいと考えている。

  • はまりポイントなど

    属性は文字列の操作などでもそのままコピーされてしまうので、欲しくない場合は substring-no-properties などで消す。

    属性の操作は破壊的なので、うっかり変えてはいけないところまで変えてしまうことがある。また、ソースコードリテラルの文字列オブジェクトには属性の操作ができない。なので、外から来た文字列に対して属性を操作する場合は、文字列をコピーしておくと安全。

    font-lock が有効なバッファで face に何か入れても速攻でクリアされてしまう。そんなときは font-lock-face に入れる。

    テキスト属性のループパターン バッファの中の特定のテキストプロパティについてループを書きたいときがある。そんなときは next-property-change または next-single-property-change などを使う。 Info の next-property-change にループのサンプルがある。

    以下は face の付いている領域についてループするイディオム。

    (defun iterate-text-prop (prop func)
      (loop with pos = (point-min)
            for next = (next-single-property-change pos prop)
            for text-val = (and next (get-text-property next prop))
            while next do
            (when text-val
              (funcall func pos next text-val))
            (setq pos next)))
    
    (iterate-text-prop 'face
      (lambda (begin end val)
        (and val (message ">> %S : %S" val (buffer-substring-no-properties begin end)))))
    

    N オーダーではあるけどもなかなか高速に動作するので、 O(N^2) のアルゴリズムでなければ問題なさそう。

DONE MELPAのパッケージページにHomepageリンクが追加

MELPAの公式がツイートしてた。

どうやらこのコミットらしい。このissueが引用されています。 さらにこのMLの投稿がリンクされている。投稿者はEmmsのメンテナであるYuniさん。

ポイントは以下。

What the MEPLA people are doing that I don't like:

    * Never communicate with the developers of the Emms in any way.

    * Omit many files that come with Emms.

    * Associate Emms with several Emms extensions that live only on
      MELPA and that we, the Emms developers, have never heard
      about. This would give anyone accessing Emms via MELPA that those
      extensions are somehow a part of Emms, when they are not.

      Maybe those extensions are good, in which case I would love for
      the developers to contact us, the Emms developers. But Maybe those
      extensions are bad, don't work, are out of date, or connect with
      non-free services.

    * Not even linking to the Emms home page
      (https://www.gnu.org/software/emms/).
Ideas for improvement:

    * Encourage people to speak to the developers of a project before
      packaging it.

    * Find a way of packaging a project as-is. For instance, Emms could
      be distributed as is, and the M/ELPA software could simply point
      at where Emms keeps its .el files for Emacs to find. This is
      instead of how I see ELPA working now, which is to force the
      software through a kind of a sieve (I think ELPA calls it a
      recipe) where only a select few files come out the other end.

      Emms doesn't need a recipe; it already comes organized and
      packaged for working with Emacs.

      It makes me think of taking bread, crumbling it up, the mushing
      those crumbs back together to re-form a new loaf of bread.

この投稿を受けて、MELPAにHomepageリンクの追加が提案されました。

このようにHomepageというボタンが追加されました。

ただ、、MLは個人的にあまり使いたくない。。

そもそも量が膨大すぎるし、この量をさばくUIもない。 本家の開発に参加したいのは山々なのですが、本質とは関係ないところで敷居を高くしている気がします。

TODO Elispにおけるquote(')とfunction(#')の使い分け

ネタ元はEmacs-jp。

functionを使うとバイトコンパイル時に関数が見つからない旨、警告を出してくれる。(kakiさんから)

chuntaroさん

24 で lexical binding を実装するまでは一緒だったけど、24 から function の方にクロージャを生成すると言う機能が追加されてて、
'(lambda () ...)
#'(lambda () ...)
とした時に違いがあります。
詳しくは一応↓このツイートに書いてあります。
https://twitter.com/shima_tetsuo/status/1056030917284622336
ちなみに、シンボルに対しては (quote symbol) (function symbol) は単にシンボルを返すだけだから、全く同じです。

defun した関数は (function hoge) の様にするのみなので、これは単にシンボル hoge が返るだけですね。 ちなみに、ソース(C言語)を見てみると

DEFUN ("function", Ffunction, Sfunction, 1, UNEVALLED, 0,
       doc: /* Like `quote', but preferred for objects which are functions.
In byte compilation, `function' causes its argument to be handled by
the byte compiler.  `quote' cannot do that.
usage: (function ARG)  */)
  (Lisp_Object args)
{
  Lisp_Object quoted = XCAR (args);

  if (!NILP (XCDR (args)))
    xsignal2 (Qwrong_number_of_arguments, Qfunction, Flength (args));

  if (!NILP (Vinternal_interpreter_environment)
      && CONSP (quoted)
      && EQ (XCAR (quoted), Qlambda))
    { /* This is a lambda expression within a lexical environment;
     return an interpreted closure instead of a simple lambda.  */
      Lisp_Object cdr = XCDR (quoted);
      Lisp_Object tmp = cdr;
      if (CONSP (tmp)
      && (tmp = XCDR (tmp), CONSP (tmp))
      && (tmp = XCAR (tmp), CONSP (tmp))
      && (EQ (QCdocumentation, XCAR (tmp))))
    { /* Handle the special (:documentation <form>) to build the docstring
         dynamically.  */
      Lisp_Object docstring = eval_sub (Fcar (XCDR (tmp)));
      CHECK_STRING (docstring);
      cdr = Fcons (XCAR (cdr), Fcons (docstring, XCDR (XCDR (cdr))));
    }
      return Fcons (Qclosure, Fcons (Vinternal_interpreter_environment,
                     cdr));
    }
  else
    /* Simply quote the argument.  */
    return quoted;
}

ちょっと分かり難いけど… (lambda …) となっているコンスセルのみに反応するようになってるのが分かると思います。

TODO mewが遅い件について

nom 12:24 もうずっと Mew を使っていますが,IMAP の fetch は確かに遅い気がします. サーバが Gmail の場合,読むのは Web UI で,返事書くのを Mew で書いたりすることもあります. nom 12:40 最新100件だけ fetch するのであれば, i (inc) ではなく,s (scan) して,range に last:100 のようにする方法がありますね.

TODO serch-forwardについて

roswell 22:23 “point がある行で、引数で指定した文字列の先頭に pointを移動” というような処理が必要なので、以下のような関数で動作確認はできたのですが … point がある行を読み取るのは、 (thing-at-point ‘line) でもできますが、もっと適切な関数等ありますでしょうか? 文字列検索 search-forward , word-search-forward は、文字列の最後に point 移動するので、string-match を使いましたが、指定文字列の先頭に point 移動するような関数ありますでしょうか? 1.,2. 組み合わせれば、もっと簡潔な記述できるような気がしていますが、もっと簡潔な記述ありますでしょうか? (defun my-move-point-to-begining-of-string-in-line (string) (interactive “sSearch String: “) (let ((pos (string-match string (thing-at-point ‘line)))) ; point がある行の、引数で指定した文字列の先頭に pointを移動 (if pos (move-to-column pos))))

zk-phi 11:15

  1. について、 `search-forward` した後は `match-beginning` で開始位置がとれるので、そこに `goto-char` してしまう手はありそうです。すると 1. の「 point のある行を文字列として切り出す」必要がなくなるぶん、気持ち簡潔になりますかね…?

(and (search-forward string (point-at-eol) nil t) (goto-char (match-beginning 0)) でもこれくらいしか思いつかないです (編集済み)

TODO EmacsでHTMLをいい感じにバッファに表示する方法

shr-render-buffer/shr-render-region

TODO Emacs-26.3新機能 add-variable-watcherを使ってみる

chuntaro 16:58

26.1 から add-variable-watcher なんてのが追加されてたんだな
(defvar my-var 1)
my-var
(add-variable-watcher 'my-var (lambda (symbol newval operation where)
                                (print (list symbol newval operation where))))
nil
(setq my-var 2)
(my-var 2 set nil) ; ← こんな事が分かる
2
(let ((my-var 3))
  (print my-var))
(my-var 3 let nil) ; let でも反応する
3
(my-var 2 unlet nil) ; 元に戻す事は unlet って言うのか
3

https://www.gnu.org/software/emacs/manual/html_node/elisp/Watching-Variables.html

chuntaro 17:00 add-variable-watcher を使うと、setq だけで処理を進められる邪悪なコーディングが出来てしまうから、あくまでもデバッグ目的で使うのが良いのかもね。 ただ、ビューアー的なものを作る場合は特定の変数の変化に対応出来るから便利かもしれない。

TODO push, setcdrについて

roswell 14:36 list の指定した位置のセルとセルの間に、セルを追加する場合、 例えば、下記の (0 1 2 3) の 1 と 2 の間に、 ‘a を追加する場合、 (setq xlist ‘(0 1 2 3)) => (0 1 2 3) (setcdr (nthcdr 1 xlist) (cons ‘a (nthcdr 2 xlist))) =>(a 2 3) xlist =>(0 1 a 2 3) の様に記述しているのですが、 もっと簡単な記述とかあればご教示いただけますでしょうか? 指定した位置に追加できる様な、関数など有りますでしょうか? (編集済み)

smzht 16:34 他にも方法はあると思いますが、こんなのとか。 (setq xlist ‘(0 1 2 3))

(defmacro add-to-list-2 (list element) `(setf ,list (cons ,element ,list)))

(add-to-list-2 (nthcdr 2 xlist) ‘a)

smzht 16:52 これでできるようです。 (push ‘a (nthcdr 2 xlist)) (編集済み)

roswell 17:32 @smzht さんご教示ありがとうございます。 defmacro の例より push の方が簡単に記述できるので 気に入りました。 info だと (setq l ‘(a b)) ⇒ (a b) (push ‘c l) ⇒ (c a b) l ⇒ (c a b) リストの先頭に追加する例だったので、 途中に追加した際に、その前の部分と接続がされるのか 想像にも及びませんでした。 (編集済み)

kosh 18:55 http://www.ic.unicamp.br/~meidanis/courses/mc336/2006s2/funcional/L-99_Ninety-Nine_Lisp_Problems.html L-99 の21問目が参考になるかも(ちょっとしたリスト遊びや再帰の勉強にもなる lisp (insert-at ‘alfa ‘(a b c d) 2) ;=> (A ALFA B C D)

kaki 22:38 push is a Lisp macro in `subr.el’.

(push NEWELT PLACE)

Add NEWELT to the list stored in the generalized variable PLACE. This is morally equivalent to (setf PLACE (cons NEWELT PLACE)), except that PLACE is only evaluated once (after NEWELT). とのことなので、 setf が使える対象には push も使えるみたいですね。これでちゃんとリストが繋がるのは、あとはコンスセルの構造を考えれば分かります。

TODO Emacs –batchで標準エラー出力に出力する方法

https://stackoverflow.com/questions/22455366/how-can-i-write-to-standard-output-in-emacs

TODO 競技プログラミングのためのC++リファレンス

cppは得意ではありませんが、競技プログラミングにおいては最強です。 特にClangの最適化の恩恵を得られることは大きいです。 コンテスト中にあちこちを見て回るのは時間の無駄なので、過去問演習中に必要だった情報をまとめておきます。

TODO Arch printer

http://cocu.hatenablog.com/entry/2016/07/19/225308

TODO AtCoderのjsonAPIについて

https://atcoder.jp/contests/abc086/standings/json にアクセスするとAC状況が見れる。 情報源はこのブログ http://tatsumack.hatenablog.com/entry/2018/12/09/113908 。* man

TODO スクリーンショットコマンドscrot(1)のオプションと使い方

ShortLongDescription

TODO mustacheのwarningをなおす

TODO ox-hugoのwarningをなおす

DONE Elispファイルをタブ/スペース混在インデントからスペースインデントに修正する方法

インデントの話

そもそもEmacsはタブ/スペース混在インデントが初期設定です。

そのためEmacs本体のソースなどはすべて混在インデントでインデントされています。 しかし「空白8つよりタブ1つにしてファイル容量を削減しなくては」という時代は明らかに終わったので、スペースでインデントしましょう。して下さい(泣)。

Emacs lisp style guideにもスペースでインデントすることが太字で記載されていますし、 white-space-mode での見た目も良いです。

(ということでほとんどのユーザーはグローバルで indent-tabs-modenil に設定しています。 そのため本体のソースはタブ/スペース混在インデントの部分とスペースインデントの部分が混在しています。カオス。)

これからパッケージを作り始めるときはさておき、歴史のあるソースで修正するのは多少の痛みを伴います。 しかし、Blameで一回変なコミットを見てしまうことを甘んじて受け入れてでもスペースでインデントして欲しいのです。

そもそもgit loggit blameには空白の変更を無視するオプション( -w )があるので履歴の面でもデメリットは無視できます。 Magitのblameでは -w はデフォルトで指定されていますし、GitHubのdiffでも無視することができます。

メリットとデメリットは以上です。

インデントの変更

「よし、スペースインデントにしてやるぜ。」とタブ/スペース混在インデントから考えを変えて下さった方。ありがとうございます。

しかし、すでに成長しきっているプロジェクトで数十のファイルがあるんだけど。。というときには次のスニペットを使って頂ければと思います。

  1. diredでElispだけをフィルタリング(dired-filter)して、
  2. 最初のファイルにポインタを合わせ、
  3. 次のスニペットを M-: から実行するだけです。
    (while (dired-get-filename nil 'no-error)
      (let ((file (dired-get-filename))
            (indent-tabs-mode nil))
        (with-temp-file file
          (insert-file-contents file)
    
          (emacs-lisp-mode)
          (delete-trailing-whitespace)
          (untabify (point-min) (point-max))
          (indent-region (point-min) (point-max)))
        (dired-next-line 1)))
    

多少時間がかかるかもしれませんが、diredでリストされているファイルの末尾まで処理を実行してくれます。

今後の悲劇を防止する

タブ/スペース混在インデントでソースが管理されていたということは、レポジトリオーナーやコントリビュータによって、また混在インデントによるコミットが行われる可能性が高いです。

あるディレクトリ以下で特定の設定を強制することができます。 .dir-locals.el です。

以下の .dir-locals.el をレポジトリルートに置いて今後の悲劇を防止できます。この方法はmagitのレポジトリでも採用されています。

((emacs-lisp-mode
  (indent-tabs-mode . nil))

.dir-locals.el をレポジトリルートに置きたくないんだけど。という場合には以下を各Elispファイルの末尾付近に置いておけば大丈夫です。

;; Local Variables:
;; indent-tabs-mode: nil
;; End:

なお、いろいろなプロジェクトを見て慣習を探ったのですが、 provideends here の間に置くことが多いようです。

(provide '{{pkg-name}})

;; Local Variables:
;; indent-tabs-mode: nil
;; End:

;;; {{pkg-name}}.el ends here

まとめ

タブ/スペース混在インデントは役目を終えました。

Emacsは後方互換性を重視しているためデフォルト値が変更される望みは薄いですが、コミュニティとしてはスペースインデントを使うということでなにとぞ。。

TODO andがapplyできない。。Lispでリストの全てが条件を満たすか調べる方法

cl-every

and はスペシャルフォームなので、 apply できません。

(apply #'and (mapcar #'evenp '(2 4 6 8 10)))
;;=> Debugger entered--Lisp error: (invalid-function #<subr and>)
;;     and(t t t t t)
;;     apply(and (t t t t t))
;;     eval((apply (function and) (mapcar (function evenp) (quote (2 4 6 8 10)))) nil)
;;     scratch-comment--elisp--eval-last-sexp(t)
;;     scratch-comment--eval-last-sexp(t)
;;     scratch-comment-eval-sexp()
;;     funcall-interactively(scratch-comment-eval-sexp)
;;     call-interactively(scratch-comment-eval-sexp nil nil)
;;     command-execute(scratch-comment-eval-sexp)

cl-libcl-every という、リストの全てが non-nil であることを検査する述語があります。

(require 'cl-lib)

(cl-every #'evenp '(2 4 6 8 10))
;;=> t

(cl-every #'evenp '(2 4 6 8 10 11))
;;=> nil

cl-some

同様に or もスペシャルフォームなので、 apply できません。

DONE Elispでstring-trimの代替を用意する方法

背景

string-trim とは与えた文字列の先頭と末尾の空白文字っぽいものを削除してくれる subr-x に定義されている便利関数です。 しかし、 string-trimsubr-x に追加されたのはEmacs-24.4からなので、Emacs-24.4未満で動かしたいエクストリームパッケージはその恩恵を得ることができません。 簡単な関数なので何度か作ったのですが、いちいち自分のパッケージをgrepするのが面倒なので、ブログポストとして残しておきます。

実装

string-trim はサブルーチンとして string-leftstring-right を組み合わせて実装されています。

(defsubst string-trim-left (string &optional regexp)
  "Trim STRING of leading string matching REGEXP.

REGEXP defaults to \"[ \\t\\n\\r]+\"."
  (if (string-match (concat "\\`\\(?:" (or regexp "[ \t\n\r]+") "\\)") string)
      (substring string (match-end 0))
    string))

(defsubst string-trim-right (string &optional regexp)
  "Trim STRING of trailing string matching REGEXP.

REGEXP defaults to  \"[ \\t\\n\\r]+\"."
  (let ((i (string-match-p (concat "\\(?:" (or regexp "[ \t\n\r]+") "\\)\\'")
                           string)))
    (if i (substring string 0 i) string)))

(defsubst string-trim (string &optional trim-left trim-right)
  "Trim STRING of leading and trailing strings matching TRIM-LEFT and TRIM-RIGHT.

TRIM-LEFT and TRIM-RIGHT default to \"[ \\t\\n\\r]+\"."
  (string-trim-left (string-trim-right string trim-right) trim-left))

つまり、これらを連結すればできます。

(defun my--string-trim (str &optional trim-left trim-right)
  "Trim STR of leading and trailing strings matching TRIM-LEFT and TRIM-RIGHT.

TRIM-LEFT and TRIM-RIGHT default to \"[ \\t\\n\\r]+\".
Original function is `string-trim'.
This function is polyfill for Emacs<24.4."
  (let ((res str))
    (setq res (replace-regexp-in-string (or trim-left "\\`[ \t\n\r]*") "" res))
    (setq res (replace-regexp-in-string (or trim-right "[ \t\n\r]*\\'") "" res))
    res))

また、この実装では replace-regexp-in-string を2回実行していますが、正規表現を工夫すれば1回で良いです。

(defun my--string-trim (str)
  "Trim STR of leading and trailing space like strings.

TRIM-LEFT and TRIM-RIGHT default to \"[ \\t\\n\\r]+\".
Original function is `string-trim'.
This function is polyfill for Emacs<24.4."
  (replace-regexp-in-string "\\`[ \t\n\r]*\\|[ \t\n\r]*\\'" "" str))

まとめ

string-trim を実装しました。 もう一度書きますが、この関数は subr-x に定義されている関数なので、「Emacs-24.4未満で動かしたいんだけど」というエクストリームパッケージ用です。 正攻法としてはEmacsの要求バージョンを上げてしまうのが一番早いので、そこは留意して頂ければと思います。

DONE GitHub Actionsで全てのジョブの実行を強制する方法

背景

GitHub Actions便利ですね。 着々とMSの資金力に囲われている気がしますが、便利なものには乗っかってしまった方がいいのが事実。

困っていたのはmatrixで複数バージョンのテストを走らせたときに、ひとつ失敗するとまだ終わってないジョブが強制的にキャンセルされてしまうこと。 (実際のActions)

この例だとバージョンの違いでCIが失敗することが分かりますが、その境界が分かりません。これは困る。

ドキュメントを良く見ると、この動作を変える方法が載ってたので紹介する次第です。

方法

ドキュメントの該当箇所は 「Workflow syntax for GitHub Actions - jobs.<job_id>.strategy.fail-fast」です。

strategy.fail-fast-false を指定するだけです。全体はこんな感じ。(実際のActions)

name: Main workflow
on:
  push: {branches: [master]}
  pull_request: {branches: [master]}

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - uses: purcell/setup-emacs@master
      with:
        version: '26.3'
    - uses: conao3/setup-keg@master
    - name: Run lint
      run: 'make lint'

  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        emacs_version:
          - '24.1'
          - '24.2'
          - '24.3'
          - '24.4'
          - '24.5'
          - '25.1'
          - '25.2'
          - '25.3'
          - '26.1'
          - '26.2'
          - '26.3'
          - 'snapshot'
        include:
          - emacs_version: 'snapshot'
            allow_failure: true
    steps:
    - uses: actions/checkout@v1
    - uses: purcell/setup-emacs@master
      with:
        version: ${{ matrix.emacs_version }}
    - uses: conao3/setup-keg@master

    - name: Run tests
      if: matrix.allow_failure != true
      run: 'make test'

    - name: Run tests (allow failure)
      if: matrix.allow_failure == true
      run: 'make test || true'

この設定をすることで以下の結果を得ることができました。 境界はEmacs-24.3とEmacs-24.4の間でしたね。

まとめ

マシンを貸し出す側からしたらさっさとジョブを中止したいのだと思いますが、 ユーザーからすると strategy.fail-fastfalse に設定するのは必須かもしれません。

パブリックレポジトリでのActionsは無料なのでどんどん使ってバグをmasterに入れないようにしましょう!

参考

DONE Elispでplist-getをsetfに対応させる方法

背景

setf という便利な関数があります。

これは汎変数に対する setq と説明できるのですが、この短かい紹介では表せないほどの汎用性と利便性を提供してくれます。

汎変数についてはkawabataさんの神記事(Qiita - Emacs Lispの汎変数(とその他))があるので、まずそちらを参照していただければと思います。

さて、問題は alist-get は汎変数として定義されているのに、なぜか plist-get の汎変数が定義されていないことです。

alistplist もlispの基本構造で、本質的に同じなので alist だけ使えて plist だけ使えないというのは 無意味な非対称性を感じます。

今回は plist に対して setf を使えるようにすることを目的とします。

汎変数の定義

magit などの大きな(そして注意深く実装されている)パッケージを読んでいると、いきなり gv- から始まるS式を見ることがあります。

これが汎変数の定義であり、標準で提供されていない汎変数を独自に定義することができます。

汎変数の定義についてもkawabataさんの記事(Qiita - Emacs Lispの汎変数(とその他)#汎変数の定義方法)に記載があります。

ここでは gv.el の実装を参考に、汎変数を定義する方法を解説します。

  • gv-define-simple-setter

    最も簡単なのは gv-define-simple-setter を使用できる場合です。 セッターを組み立てる際、 get <-> set / put の対応が取れている関数が用意されている場合で、単に関数シンボルを替えるだけで良い場合に使用できます。

    (gv-define-simple-setter aref aset)
    (gv-define-simple-setter car setcar)
    (gv-define-simple-setter cdr setcdr)
    (gv-define-simple-setter get put)
    (gv-define-simple-setter default-value set-default)
    (gv-define-simple-setter process-get process-put)
    

    setfrotatef はこのように展開され、意図通りにリストが変更されます。

    (ppp-macroexpand-all
     (let ((target '(a b c)))
       (setf (car target) 'modify)
       target))
    ;;=> (let ((target '(a b c)))
    ;;     (let* ((v target))
    ;;       (setcar v 'modify))
    ;;     target)
    
    (let ((target '(a b c)))
      (setf (car target) 'modify)
      target)
    ;;=> (modify b c)
    
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    
    (ppp-macroexpand-all
     (let ((target '(a b c)))
       (cl-rotatef (car target) (cadr target))
       target))
    ;;=> (let ((target '(a b c)))
    ;;     (let* ((v target)
    ;;            (--cl-rotatef-- (car v)))
    ;;       (progn
    ;;         (setcar v
    ;;                 (let* ((v target))
    ;;                   (prog1 (car (cdr v))
    ;;                     (setcar
    ;;                      (cdr v)
    ;;                      --cl-rotatef--))))
    ;;         nil))
    ;;     target)
    
    (let ((target '(a b c)))
      (cl-rotatef (car target) (cadr target))
      target)
    ;;=> (b a c)
    
  • gv-define-setter

    gv-define-setter はセッターを組み立てる際に、単に関数シンボル置き換えでは実現できない場合に使用します。

    基本的にはこのマクロを使うことになるかと思います。 なお、先程の gv-define-simple-settergv-define-setter を生成するマクロとして定義されていました。

    (gv-define-setter caar (val x) `(setcar (car ,x) ,val))
    (gv-define-setter cadr (val x) `(setcar (cdr ,x) ,val))
    (gv-define-setter cdar (val x) `(setcdr (car ,x) ,val))
    (gv-define-setter cddr (val x) `(setcdr (cdr ,x) ,val))
    (gv-define-setter elt (store seq n)
      `(if (listp ,seq) (setcar (nthcdr ,n ,seq) ,store)
         (aset ,seq ,n ,store)))
    (gv-define-setter gethash (val k h &optional _d)
     `(puthash ,k ,val ,h))
    

    gv-define-setter の使用方法は眺めていると分かってきますが、 第1引数に汎変数を定義する関数シンボル、第2引数に関数の引数、第3引数にマクロ展開部を指定します。 第2引数は先頭に変更先の値を受け取って、その後に関数の引数を並べることになっているようです。

    setfrotatef はこのように展開され、意図通りにリストが変更されます。

    (ppp-macroexpand-all
     (let ((target '(a b c)))
       (setf (cadr target) 'modify)
       target))
    ;;=> (let ((target '(a b c)))
    ;;     (let* ((v target))
    ;;       (setcar (cdr v) 'modify))
    ;;     target)
    
    (let ((target '(a b c)))
      (setf (cadr target) 'modify)
      target)
    ;;=> (a modify c)
    
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    
    (ppp-macroexpand-all
     (let ((target '(a b c)))
       (cl-rotatef (cadr target) (caddr target))
       target))
    ;;=> (let ((target '(a b c)))
    ;;     (let* ((v target)
    ;;            (--cl-rotatef-- (car (cdr v))))
    ;;       (progn
    ;;         (setcar
    ;;          (cdr v)
    ;;          (let* ((v (cdr (cdr target))))
    ;;            (prog1 (car v)
    ;;              (setcar v --cl-rotatef--))))
    ;;         nil))
    ;;     target)
    
    (let ((target '(a b c)))
      (cl-rotatef (cadr target) (caddr target))
      target)
    ;;=> (a c b)
    
  • gv-define-expander

    gv-define-expander はセッターを生成する際にセッターを使いたい場合など、特別な処理を要する関数に使用します。

    alist-get の汎変数はこのマクロで定義されています。

    なお、先程の gv-define-settergv-define-expander を生成するマクロとして定義されていました。

    (gv-define-expander alist-get
      (lambda (do key alist &optional default remove testfn)
        (macroexp-let2 macroexp-copyable-p k key
          (gv-letplace (getter setter) alist
            (macroexp-let2 nil p `(if (and ,testfn (not (eq ,testfn 'eq)))
                                      (assoc ,k ,getter ,testfn)
                                    (assq ,k ,getter))
              (funcall do (if (null default) `(cdr ,p)
                            `(if ,p (cdr ,p) ,default))
                       (lambda (v)
                         (macroexp-let2 nil v v
                           (let ((set-exp
                                  `(if ,p (setcdr ,p ,v)
                                     ,(funcall setter
                                               `(cons (setq ,p (cons ,k ,v))
                                                      ,getter)))))
                             `(progn
                                ,(cond
                                 ((null remove) set-exp)
                                 ((or (eql v default)
                                      (and (eq (car-safe v) 'quote)
                                           (eq (car-safe default) 'quote)
                                           (eql (cadr v) (cadr default))))
                                  `(if ,p ,(funcall setter `(delq ,p ,getter))))
                                 (t
                                  `(cond
                                    ((not (eql ,default ,v)) ,set-exp)
                                    (,p ,(funcall setter
                                                  `(delq ,p ,getter))))))
                                ,v))))))))))
    

    alist-get の汎変数の定義がこのように複雑になっているのは、「alistに存在しないキーに対して setf したときにコンスセルごと追加する」という処理があるからです。

    (ppp-macroexpand-all
     (let ((target '((a ."a") (b . "b") (c . "c"))))
       (setf (alist-get 'd target) "modify")
       target))
    ;;=> (let ((target '((a . "a") (b . "b") (c . "c"))))
    ;;     (let* ((p (if (and nil (not (eq nil 'eq)))
    ;;                   (assoc 'd target nil)
    ;;                 (assq 'd target))))
    ;;       (progn
    ;;         (if p
    ;;             (setcdr p "modify")
    ;;           (setq target (cons (setq p (cons 'd "modify")) target)))
    ;;         "modify"))
    ;;     target)
    
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    
    (let ((target '((a ."a") (b . "b") (c . "c"))))
      (setf (alist-get 'a target) "modify")
      target)
    ;;=> ((a . "modify") (b . "b") (c . "c"))
    
    (let ((target '((a ."a") (b . "b") (c . "c"))))
      (setf (alist-get 'd target) "modify")
      target)
    ;;=> ((d . "modify") (a . "a") (b . "b") (c . "c"))
    

    「alistに存在しないキーに対して setf したときにコンスセルごと追加する」という処理が必要ない場合、 gv-define-setter を使って簡単に定義することができます。 (さらに簡単のため、 alist-getdefaultremovetestfn 引数も無視します。)

    (gv-define-setter alist-get (val key alist &optional _default _remove _testfn)
      `(setcdr (assq ,key ,alist) ,val))
    ;;=> ...
    
    (ppp-macroexpand-all
     (let ((target '((a ."a") (b . "b") (c . "c"))))
       (setf (alist-get 'a target) "modify")
       target))
    ;;=> (let ((target '((a . "a") (b . "b") (c . "c"))))
    ;;     (let* ((v target))
    ;;       (setcdr (assq 'a v) "modify"))
    ;;     target)
    
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    
    (let ((target '((a ."a") (b . "b") (c . "c"))))
      (setf (alist-get 'a target) "modify")
      target)
    ;;=> ((a . "modify") (b . "b") (c . "c"))
    
    (let ((target '((a ."a") (b . "b") (c . "c"))))
      (setf (alist-get 'd target) "modify")
      target)
    ;;=> Debugger entered--Lisp error: (wrong-type-argument consp nil)
    ;;     setcdr(nil "modify")
    ;;     (let* ((v target)) (setcdr (assq 'd v) "modify"))
    ;;     (setf (alist-get 'd target) "modify")
    ;;     (let ((target '((a . "a") (b . "b") (c . "c")))) (setf (alist-get 'd target) "modify") target)
    ;;     (progn (let ((target '((a . "a") (b . "b") (c . "c")))) (setf (alist-get 'd target) "modify") target))
    ;;     eval((progn (let ((target '((a . "a") (b . "b") (c . "c")))) (setf (alist-get 'd target) "modify") target)) t)
    

    このように簡単に定義できますが、alistに存在しないキーに対して作用させようとするとエラーになってしまいます。 alist-get の汎変数はこのような要件を満たすために複雑な定義になっています。

    なお、 gv-define-expandergv-define-setter から生成されるので、 gv-define-setter がどのような式を生成するかを眺めることでも使用法を予想することができます。

    (ppp-macroexpand
      (gv-define-setter cddr (val x) `(setcdr (cdr ,x) ,val)))
    ;;=> (gv-define-expander cddr
    ;;     (lambda (do &rest args)
    ;;       (declare-function gv--defsetter "gv"
    ;;                         (name setter do args &optional vars))
    ;;       (gv--defsetter 'cddr
    ;;                      (lambda (val x) `(setcdr (cdr ,x) ,val))
    ;;                      do args)))
    
    • alist-getの汎変数余談

      gv-define-setter は分かりやすいので、どうにかして alist-get の汎変数を gv-define-setter で定義できないでしょうか。 まず思いつくのはこのような定義です。

      (macroexp-let2 は第1引数が nil のときに、第2引数の名前で make-symbol したものに第3引数の値をlet束縛するものです。

      これはマクロ展開のイディオムを簡略化するものであり、望まない変数捕捉を避けるものです。(マクロの変数捕捉についてはOnlisp(Onlisp - 変数捕捉)参照))

      (gv-define-setter alist-get (val key alist &optional _default _remove _testfn)
        (macroexp-let2 nil p `(assq ,key ,alist)
          `(if ,p
               (setcdr ,p ,val)
             (setq ,alist (cons (cons ,key ,val) ,alist)))))
      ;;=> ...
      
      (ppp-macroexpand-all
       (let ((target '((a ."a") (b . "b") (c . "c"))))
         (setf (alist-get 'a target) "modify")
         target))
      ;;=> (let ((target '((a . "a") (b . "b") (c . "c"))))
      ;;     (let* ((v target)
      ;;            (p (assq 'a v)))
      ;;       (if p
      ;;           (setcdr p "modify")
      ;;         (setq v (cons (cons 'a "modify") v))))
      ;;     target)
      ;;   nil
      
      ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
      
      (let ((target '((a ."a") (b . "b") (c . "c"))))
        (setf (alist-get 'a target) "modify")
        target)
      ;;=> ((a . "modify") (b . "b") (c . "c"))
      
      (let ((target '((a ."a") (b . "b") (c . "c"))))
        (setf (alist-get 'd target) "modify")
        target)
      ;;=> ((a . "a") (b . "b") (c . "c"))
      

      しかし上手く動いてないようです。マクロ展開結果を注意深く見れば分かるのですが、letで一時的に束縛された変数にsetqしてしまっています。 これではグローバルの値を変更できていません。

      先程確認したように gv-define-settergv--defsetter を展開し、これに含まれる macroexp-let2 によりalistをlet束縛してしまいます。 gv-define-expander を直接使うことで macroexp-let2 を展開せず、 gv-letplace でグローバル値のセッターを得ることができます。

      (defun gv--defsetter (name setter do args &optional vars)
        "Helper function used by code generated by `gv-define-setter'.
      NAME is the name of the getter function.
      SETTER is a function that generates the code for the setter.
      NAME accept ARGS as arguments and SETTER accepts (NEWVAL . ARGS).
      VARS is used internally for recursive calls."
        (if (null args)
            (let ((vars (nreverse vars)))
              (funcall do `(,name ,@vars) (lambda (v) (apply setter v vars))))
          ;; FIXME: Often it would be OK to skip this `let', but in general,
          ;; `do' may have all kinds of side-effects.
          (macroexp-let2 nil v (car args)
            (gv--defsetter name setter do (cdr args) (cons v vars)))))
      
      (ppp-macroexpand
       (gv-define-setter alist-get (val key alist &optional _default _remove _testfn)
         (gv-letplace (getter setter) alist
           (macroexp-let2 nil p `(assq ,key ,alist)
             `(if ,p
                  (setcdr ,p ,val)
                ,(funcall setter `(cons (cons ,key ,val) ,getter)))))))
      ;;=> (gv-define-expander alist-get
      ;;     (lambda (do &rest args)
      ;;       (declare-function gv--defsetter "gv"
      ;;                         (name setter do args &optional vars))
      ;;       (gv--defsetter 'alist-get
      ;;                      (lambda (val key alist &optional _default _remove _testfn)
      ;;                        (gv-letplace (getter setter) alist
      ;;                          (macroexp-let2 nil p `(assq ,key ,alist)
      ;;                            `(if ,p
      ;;                                 (setcdr ,p ,val)
      ;;                               ,(funcall setter `(cons (cons ,key ,val)
      ;;                                           ,getter))))))
      ;;                      do args)))
      ;;   nil
      
      (ppp-macroexpand
       (gv-define-expander alist-get
         (lambda (do key alist &optional _default _remove _testfn)
           (gv-letplace (getter setter) alist
             (macroexp-let2 nil p `(assq ,key ,alist)
               (funcall do '_getter
                        (lambda (val)
                          `(if ,p
                               (setcdr ,p ,val)
                             ,(funcall setter `(cons (cons ,key ,val) ,getter))))))))))
      ;;=> (function-put 'alist-get 'gv-expander
      ;;                 (lambda (do key alist &optional _default _remove _testfn)
      ;;                   (gv-letplace (getter setter) alist
      ;;                     (macroexp-let2 nil p `(assq ,key ,alist)
      ;;                       (funcall do '_getter
      ;;                                (lambda (val)
      ;;                                  `(if ,p
      ;;                                       (setcdr ,p ,val)
      ;;                                     ,(funcall setter `(cons (cons ,key ,val)
      ;;                                                 ,getter)))))))))
      ;;   nil
      

      次に示すように、 gv-define-setter では上手くいかず、 gv-define-expander を使うと上手くいきます。

      (gv-define-setter alist-get (val key alist &optional _default _remove _testfn)
        (gv-letplace (getter setter) alist
          (macroexp-let2 nil p `(assq ,key ,alist)
            `(if ,p
                 (setcdr ,p ,val)
               ,(funcall setter `(cons (cons ,key ,val) ,getter))))))
      ;;=> ...
      
      (ppp-macroexpand-all
       (let ((target '((a ."a") (b . "b") (c . "c"))))
         (setf (alist-get 'd target) "modify")
         target))
      ;;=> (let ((target '((a . "a")
      ;;                   (b . "b")
      ;;                   (c . "c"))))
      ;;     (let* ((v target)
      ;;            (p (assq 'd v)))
      ;;       (if p
      ;;           (setcdr p "modify")
      ;;         (setq v (cons (cons 'd "modify") v))))
      ;;     target)
      ;;   nil
      
      (let ((target '((a ."a") (b . "b") (c . "c"))))
        (setf (alist-get 'd target) "modify")
        target)
      ;;=> ((a . "a") (b . "b") (c . "c"))
      
      ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
      
      (gv-define-expander alist-get
        (lambda (do key alist &optional _default _remove _testfn)
          (gv-letplace (getter setter) alist
            (macroexp-let2 nil p `(assq ,key ,alist)
              (funcall do '_getter
                       (lambda (val)
                         `(if ,p
                              (setcdr ,p ,val)
                            ,(funcall setter `(cons (cons ,key ,val) ,getter)))))))))
      ;;=> ...
      
      (ppp-macroexpand-all
       (let ((target '((a ."a") (b . "b") (c . "c"))))
         (setf (alist-get 'd target) "modify")
         target))
      ;;=> (let ((target '((a . "a")
      ;;                   (b . "b")
      ;;                   (c . "c"))))
      ;;     (let* ((p (assq 'd target)))
      ;;       (if p
      ;;           (setcdr p "modify")
      ;;         (setq target (cons (cons 'd "modify") target))))
      ;;     target)
      ;;   nil
      
      (let ((target '((a ."a") (b . "b") (c . "c"))))
        (setf (alist-get 'd target) "modify")
        target)
      ;;=> ((d . "modify") (a . "a") (b . "b") (c . "c"))
      

まとめ

setf はとても便利なので、ぜひ使いこなして頂ければと思います。

今回、汎変数を定義する方法についてまとめたので、自作パッケージでも汎変数を定義しておいてあげるとユーザーが便利に使えるパッケージになると思います。

なお、今回の目的である plist-get の汎変数については gv-define-setter を使って、次のように定義しました。

後でemacs-develにも投稿しておきたいと思います。 本家に取り入れられればこのパッチなしで plit-get に対する setf を使用できるようになります。

(gv-define-expander plist-get
  (lambda (do plist prop)
    (macroexp-let2 macroexp-copyable-p key prop
      (gv-letplace (getter setter) plist
        (macroexp-let2 nil p `(plist-member ,getter ,key)
          (funcall do
                   `(cadr ,p)
                   (lambda (val)
                     `(if (plist-member ,plist ,key) (setcar (cdr (plist-member ,plist ,key)) ,val)
                        ,(funcall setter `(cons ,key (cons ,val ,getter)))))))))))
;;=> ...

(ppp-macroexpand-all
 (let ((target '(:a "a" :b "b" :c "c")))
   (setf (plist-get target :a) "modify")
   target))
;;=> (let ((target '(:a "a" :b "b" :c "c")))
;;     (let* ((p (plist-member target :a)))
;;       (if (plist-member target :a)
;;           (setcar
;;            (cdr (plist-member target :a))
;;            "modify")
;;         (setq target (cons :a (cons "modify" target)))))
;;     target)
;;   nil

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(let ((target '(:a "a" :b "b" :c "c")))
  (setf (plist-get target :a) "modify")
  target)
;;=> (:a "modify" :b "b" :c "c")

(let ((target '(:a "a" :b "b" :c "c")))
  (cl-rotatef (plist-get target :a) (plist-get target :c))
  target)
;;=> (:a "c" :b "b" :c "a")

(let ((target '(:a "a" :b "b" :c "c")))
  (setf (plist-get target :d) "modify")
  target)
;;=> (:d "modify" :a "a" :b "b" :c "c")

参考

TODO Emacsにおける.dir-locals.elについて

背景

個々のファイルを汚さないためにも .dir-locals.el を活用していきたいものです。 しかし、歴史的な経緯により、 .el でサフィックスされているのに文法はElispではなく .dir-locals.el 独自のものとなっています。 それが .dir-locals.el をよく分からないものにしている一因んだと思われます。 この記事は様々な .dir-locals.el の具体例を示し、 .dir-locals.el の効果的な利用方法を見つけて頂く一助となればと思うものです。

.dir-locals.elの仕組み

.dir-locals.el はEmacs本体の lisp/files.el で処理されています。

なんと .dir-locals.el のファイル名は変数で管理されており、変更することができます(おすすめできませんが)。(GitHub - emacs-mirror/emacs - lisp/files.el)

(defconst dir-locals-file ".dir-locals.el"
  "File that contains directory-local variables.
It has to be constant to enforce uniform values across different
environments and users.

A second dir-locals file can be used by a user to specify their
personal dir-local variables even if the current directory
already has a `dir-locals-file' that is shared with other
users (such as in a git repository).  The name of this second
file is derived by appending \"-2\" to the base name of
`dir-locals-file'.  With the default value of `dir-locals-file',
a \".dir-locals-2.el\" file in the same directory will override
the \".dir-locals.el\".

See Info node `(elisp)Directory Local Variables' for details.")

変数のドキュメントに記載があるように .dir-locals.el.dir-locals-2.el が同時にディレクトリに存在する場合、後者が優先されるようですね。(知らなかった) 使用法としては、これも変数のドキュメントに記載のとおり、gitで .dir-locals.el が共有されていて、気に入らない値が設定されているとき .dir-locals.el を置くことで前者を無視することができるということです。 そういうユースケースなので、 .dir-locals-2.el はglobalの.gitignoreでignoreしておくといいのかもしれません。

dir-locals-file を使っている dir-locals--all-files に適当にデバッガを仕込んでバックトレースを取ると次の情報が得られました。

dir-locals--all-files("~/dev/dir-locals-test/")
locate-dominating-file("/home/conao/dev/dir-locals-test/" dir-locals--all-files)
dir-locals-find-file("/home/conao/dev/dir-locals-test/.dir-locals.el")
hack-dir-local-variables()
hack-local-variables(no-mode)
run-mode-hooks(emacs-lisp-mode-hook)
emacs-lisp-mode()
set-auto-mode-0(emacs-lisp-mode nil)
set-auto-mode()
normal-mode(t)
after-find-file(t t)
find-file-noselect-1(#<buffer .dir-locals.el> "~/dev/dir-locals-test/.dir-locals.el" nil nil "~/dev/dir-locals-test/.dir-locals.el" nil)
find-file-noselect("/home/conao/dev/dir-locals-test/.dir-locals.el" nil nil nil)
find-file("/home/conao/dev/dir-locals-test/.dir-locals.el")

つまり、 find-file でファイルを開いたり、 revert-buffer で開き直せば normal-mode が呼ばれるので、 .dir-locals.el の設定内容を反映できるようですね。 revert-buffer で反映できるのは .dir-locals.el のデバッグをするときには便利なので覚えておいて頂ければと思います。

TODO Lispとは何なのか。LISt Processerの力

背景

この記事はMomiji-LT#1の発表の草稿です。 Lispを触ったことのない人に「Lispとは何か」を紹介し、魅力を伝えようとするものです。

まとめ

参考

TODO Lispとは何なのか

はじめに

  • 自己紹介

    • 山下 直哉 (@conao3)
    • 学業
      • 広島大学先進理工系科学研究科
        パターン認識研究室 (栗田・宮尾研究室)
        • 機械学習を用いた単眼動画像の深度予測
    • 趣味
  • GitHubの草

  • GitHub解析結果 (forkwell)

  • MELPA (Emacsパッケージ)

  • 今回の目的

    • LISP(概念)とは何か
    • なぜLispが「強い」のか
    • どこでLispの「強さ」が生きるのか
    • なぜEmacsを選ぶのか

LISP(概念)とは何か

  • Lispの歴史と影響の広がり1

  • John McCarthy (1927 - 2011)2

    • 「人工知能; Artificial Intelligence」の生みの親
    • John McCarthy, Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I, 1960
      • LISP - LISt Interpreter
      • 言語のインタプリタ(eval)
        を記述できる(最小の)言語
      • 万能チューリングマシンの言い換え
      • 特に「純」LISPと呼ぶ
    • 5つの関数と4つのスペシャルフォーム
      • 関数 (atom, eq, car, cons)
      • スペシャルフォーム
        (if(cond), quote, lambda, define)
  • 純LISPの関数
    関数説明記号的説明
    atom値がatomなら T を返すatom [(A B)] → nil
    atom [nil] → T
    eq同一なら T を返すeq [A ; A] → T
    carpairの左値をとり出すcar [(A . B)] → A
    cdrpairの右値をとり出すcdr [(A . B)] → B
    conspairを作るcons [A;B] → (A . B)
  • 純LISPのスペシャルフォーム
    名前説明
    if条件分岐(if (< a b) 'yes 'no)
    quoteそのまま返す(quote yes), 'yes
    lambda無名関数を定義する(lambda (e) (+ 2 e))
    define無名関数に名前を与える(define '2+ (lambda (e) (+ 2 e)))
  • Lisp族

    • 純LISPはとても小さいのですぐに実装できる
    • LISPの仲間が大量発生
      • Scheme (Guile, Gauche, Racket,,,)
      • EmacsのLisp: Emacs-lisp
      • ANSIのLisp: CommonLisp
      • JVMのLisp: Clojure
      • PythonのLisp: Hy
    • Lispの入門としてEmacs-lisp!
      • 処理系はEmacsをインストールするだけ
      • カスタマイズはElispなので、
        いつのまにか覚えている
      • 以後、例示のコードはElisp

なぜLispが「強い」のか

  • 同図像性が「強い」

    • データとプログラムが同じ形式
    • プログラムがデータを作り、データがプログラムを作る
    '(+ 1 2 3 4 5 (+ 1 2 3 4 5))
    ;; => (+ 1 2 3 4 5 (+ 1 2 3 4 5))
    
    `(+ 1 2 3 4 5 ,(+ 1 2 3 4 5))
    ;; => (+ 1 2 3 4 5 15)
    
    (eval
     `(+ 1 2 3 4 5 ,(+ 1 2 3 4 5)))
    ;; => 30
    
    `(+ (+ ,@(number-sequence 1 5))
        (+ ,@(number-sequence 11 15))
        (+ ,@(number-sequence 21 25)))
    ;; => (+ (+ 1 2 3 4 5) (+ 11 12 13 14 15) (+ 21 22 23 24 25))
    
    (eval
     `(+ ,@(mapcar (lambda (elm)
                     `(+ ,@(number-sequence (car elm) (cdr elm))))
                   '((1 . 5) (11 . 15) (21 . 25)))))
    ;; => 195
    
  • マクロが「強い」 1/2

    • 構文木に対するマクロ
      • C言語のマクロは「プログラム文字列」のマクロ
      • 不整合エラーが発生しやすく、大域で扱い辛い
    • マクロの場合、引数の評価前に変換、評価される
    (defalias '2+
      (lambda (arg) (+ 2 arg)))
    ;;=> 2+
    
    (2+ 5)
    ;;=> 7
    
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    
    ;; 関数の定義には defalias/lambda を使う
    ;; これはイディオムなので、何回も書くのは面倒。。
    ;; defalias/lambda を展開するマクロを書いてしまえ
    
    (defmacro defun (name arglist &rest body)
      `(defalias ',name (lambda ,arglist ,@body)))
    ;;=> defun
    
  • マクロが「強い」 2/2

    ;; macro-expandで展開形を得ることができる
    (ppp-macroexpand
     (defun 2+ (arg)
       (+ 2 arg)))
    ;;=> (defalias '2+
    ;;     (lambda (arg) (+ 2 arg)))
    
    (defun 2+ (arg)
      (+ 2 arg))
    ;;=> 2+
    
    ;; defunマクロで定義した 2+ もちゃんと動く
    (2+ 5)
    ;;=> 7
    
  • カッコが「強い」 1/2

    • Lispはカッコで構文木を表現する
    • 機械フレンドリーである
    • よって、エディタの支援が得られやすい
    • インデントを間違えることがない
      (C++などの言語では闇の力によって実現されている。。)
    # with torch.no_grad(): を入れたいなぁ。。
    total, correct, loss = 0, 0, 0.0
    for testdata in testloader:
        images, labels = testdata
        outputs = net(images)
        ...
        loss += criterion.item()
    
    loss /= total
    
    # この条件分岐いらないな。。(then節だけでいいな。。)
    if os.path.exists("./cifer_net.pth"):
        torch.save(net.state_dict(), "./cifer_net.pth")
    else:
        print("file already exist.  abort!")
    
  • カッコが「強い」 2/2

    ;; | はポインタを示す
    
    ;; ワンキーでS式を囲む
    |(dolist (testdata testloader)
      ...)
    (|(dolist (testdata testloader)
      ...))
    
    ;; ワンキーでS式を取り出す
    (if (file-exists-p "./cifer_net.pth")
        |(save (state_dict net) "./cifer_net.pth")
      (print "file already exist.  abort!"))
    |(save (state_dict net) "./cifer_net.pth")
    
    ;; 「末尾まで削除」してもカッコの対応が崩れない
    (if (file-exists-p "./cifer_net.pth")
        (save (state_dict net) "./cifer_net.pth")
      |(print "file already exist.  abort!"))
    (if (file-exists-p "./cifer_net.pth")
        (save (state_dict net) "./cifer_net.pth")
      |)
    

どこでLispの「強さ」が生きるのか

  • どこでLispの「強さ」が生きるのか

    • どこでも。
    • データを自由な形式で記述したい
    • プログラムを自由な形式で記述したい
    • イディオムを簡単に隠蔽したい
    • プログラムを動的に生成したい
    • つまり、どこでも。
    • ただ、まともなプログラムを作ろうとすると依存管理、
      ライブラリ検索などで闇を見る可能性。
    • Emacsなら、閉じているのでElispを使い放題
    • CommonLispでMLできるらしい。。
    • HyならPythonになるのでPyTorchの全機能が使えるが、
      補完、シンボルジャンプなどがないエクストリーム環境
    • CommonLispからTorchのC++ APIが見えるらしい。。

まとめ

  • まとめ

    • Lispの歴史と多言語への影響
    • 純LISPの構成要素と内容
    • Lispの特徴である、
      • 「同図像性」・「マクロ」・「カッコ」
    • Lispの活用場所

    今回、時間の関係で挙げられなかったLispとその周辺、
    インタプリタやコンパイラ、コンパイラ最適化、
    継続(call/cc)、などなどLispからたくさん学べます。

    Lispは楽しい! みんなも触ってみよう!

DONE ピクシブ夏インターンシップ「PIXIV SUMMER BOOT CAMP 2020」体験記

はじめに

ピクシブさんのインターンシップ「PIXIV SUMMER BOOT CAMP 2020」へ参加してきました!

期間は8日間という短い期間でしたが、短い期間ながら、とても濃い経験が出来たと思います。

インターン最終日に発表した資料はこちら。

感想

アウトプットはピクシブ百科事典のオレオレフレームワークからPSR-7/15/17ベースのPHPへ(一部のクラスだが)移行したこと。 触らせて頂いたgitレポジトリのヒストリーだと、cvsからgitへ移行した最初のコミットが9年前、dispatcherやrouter、index.phpには多少手が入ることはあっても抜本的に手が入るのはこれが初めてでした。

最終発表ではこの「9年前」ということに対して、「いやもっと前。。。」というコメントもあったので、長らく「いつか改善したいなぁ」とピクシブのエンジニアさんが思っていたところを作業でき、そのコードに貢献できたのはとても嬉しいことでした。 技術的なことと、その変更をしているときに詰まったところなどは発表資料を参照して頂ければと。

メンターをして頂いたtadsanさんからのコメントです。 インターンで取り組んだ課題の背景などもスレッドで投稿されています。

ノベルティもたくさん頂きました。 現地で参加できれば、このパーカーを着て作業していたらしいですが。。とても残念!

世界的な疫病の拡大により、インターンに係る全ての作業はリモートで行なわれ、始めてのインターンではあるものの不思議な感覚でした。。

そもそも広島からのリモートでのインターン参加を認めて下さったこと、さらにはインターネットの不調により急遽モバイルルータを手配して頂くなど、手厚いサポートを頂きました。

このピクシブのインターンで「圧倒的猛者」になれました!! 本当にありがとうございました!

TODO org-modeドキュメントからZenn Flavored Markdownを生成するox-zennの使い方

tl;dr

  • ox-zennというorgからZenn flavored markdownを生成するパッケージを書きました。
  • Zennマークダウンは独自記法があるので、orgファイルとの対応について少し注意することがある。

org-modeとは

  • 概要

    org-modeとはEmacsのメジャーモードのひとつです」という説明は正しいのですが、org-modeの力を説明するには全く言葉が足りません。

    org-modeの日本語プロジェクトページには

    Org-mode はノートの保存、TODOリストの管理、プロジェクト計画、文書編集のためのモードです。高速で効率的なプレーンテキストのシステムを使ってファイルを編集します。

    と説明されています。

    この説明でとても重要なのはorgドキュメントが「プレーンテキスト」であるという点です。

    「プレーンテキスト」はロックインを避けるための究極の選択肢です。

    「org-mode」にロックインしてるじゃないか。という手痛い指摘が飛んできそうですが、それには当たりません。なぜならorg-modeが編集の対象にしているのは「プレーンテキスト」であり、もしorg-modeが明日なくなったとしても普通のエディタで編集や読み書きができ、様々なUnixコマンドによる処理が容易できるからです。

  • 万能のドキュメントソース

    org-modeはとても大きく、私も全容を把握できていません。 しかしつまみ食いをするだけでもとても大きなメリットがある機能があります。 それがorgのエクスポート機能とコード実行機能1です。

    このようにorgドキュメントを中心として、HTMLやLaTeX、Wordドキュメント生成や、実行可能なソースコードを生成する。ドキュメントの中で任意の言語のコード実行を伴なうこともできます。

    LSP(Language Server Protocol Support)にも似た思想を感じます。 LSPも各言語に対するプログラミング支援環境を各エディタにそれぞれ実装するのは無駄が多いからという理由で現代では主流といっても良い、勢いとシェアがあります。

    私がorgドキュメントとorg-modeを使う理由も同じです。 もうHTMLを、LaTeXを、Wordを、PowerPointをそれぞれのファイルフォーマットで編集する時代は終わっているのです。 私が編集するドキュメント形式はorgドキュメントだけであり、orgドキュメントに情報を集約することで情報の再利用性と利便性が向上するのです。

ox-zenn

orgはorgドキュメントをパースし、それぞれの「意味」でどのような「出力」にするのか制御することでエクスポート先のファイルを生成します。

つまり我々がorgドキュメントから新しいフォーマットで出力したい場合はorgが解釈した「意味」からどのような出力にするのかというエクスポートバックエンドのみを作成すれば良いことになります。 それがこちら「ox-zenn」です。

ox-zennの作り方については別の記事にするとして、この記事では使い方を説明します。

  • org-modeとox-zennのインストール

    leafを使ってインストールします。 また、orgはEmacsに標準添付されているのですが、upstreamがとても活発に改善されているので、この手順に従い、最新版を入れることをおすすめします。

    現状、ox-zennがまだcelpaにしか入っていないので、celpaのURLを package-archives に追加する必要があることに注意する必要があります。

    (eval-and-compile
      (customize-set-variable
       'package-archives '(("gnu" . "https://elpa.gnu.org/packages/")
                           ("melpa" . "https://melpa.org/packages/")
                           ("celpa" . "https://celpa.conao3.com/packages/")
                           ("org" . "https://orgmode.org/elpa/")))
      (package-initialize)
      (unless (package-installed-p 'leaf)
        (package-refresh-contents)
        (package-install 'leaf))
    
      (leaf leaf-keywords
        :ensure t
        :config
        (leaf-keywords-init)))
    
    (leaf org
      :ensure org-plus-contrib
      :custom ((org-startup-indented . t)
               (org-return-follows-link . t)
               (org-src-window-setup . 'other-window)
               (org-highlight-latex-and-related . '(latex script entities))))
    
    (leaf ox-zenn
       :ensure t
       :after org
       :require t)
    
  • 基本のorg記法

    orgのプロジェクトページにorg-syntaxのページがあるので参照して頂ければと。

  • ox-zenn拡張記法

    Zenn公式によるMarkdownの解説記事があります。 Zennは基本的に今時のマークダウン、つまりGFM(GitHub Flavored Markdown)を受け取るように見えます。 さらにZenn独自の記法として独自拡張のブロック要素コンテンツ埋め込み用のショートコードをサポートしています。

    残念ながらorgには対応している記法が存在しないのでox-zennが特別に解釈する文法を追加しました。 もちろん、他のバックエンドは解釈しないのでドキュメントの可搬性が落ちることに留意する必要があります。

    • frontmatter

      マークダウンの上にYAMLのfrontmatterが書けるのは、どのマークダウンが始めたのか分かりませんが、Zennのマークダウンもfrontmatterでタイトルなどのメタ要素を指定することができます。

      Zennはfrontmatterで次の要素を指定することができます。

      • title
      • topics
      • emoji
      • type
      • published

      orgドキュメントでは次のように指定すると、

      #+title: ZennのMarkdown記法
      
      #+gfm_tags: markdown zenn
      #+gfm_custom_front_matter: :emoji 👩‍💻 :type tech
      #+gfm_custom_front_matter: :published true
      

      次のようにマークダウンに出力されます。

      ---
      author: Naoya Yamashita
      title: "ZennのMarkdown記法"
      last_modified: 2020-09-24
      emoji: 👩‍💻
      type: tech
      topics: [markdown, zenn]
      published: true
      ---
      

      gfm_tags は空白区切りです。 gfm_custom_front_matter はplist風に空白区切りで指定します。 それぞれ、一行が長くなる場合、複数行に分けて指定しても正しく解釈されます。

      authorlast_modified は自動で追加されるので、それらは options で無効化できます。

      #+title: ZennのMarkdown記法
      #+options: broken-links:mark toc:nil author:nil last-modified:nil
      
      #+gfm_tags: markdown zenn
      #+gfm_custom_front_matter: :emoji 👩‍💻 :type tech
      #+gfm_custom_front_matter: :published true
      

      ZennではサイドバーにToCが表示されているので、 toc:nil を指定しておくのも便利です。 :topics の指定において、空白が含まれる値を設定する都合上、若干トリッキーになっているので注意する必要があります。

    • ブロック要素

      ブロック要素はブロッククオートにhtml属性を付けることで対応しました。 このように定義することで他のバックエンドで出力した際にも多少は意味を保てると思います。

      message : ブロッククオートに #+attr_html:x-typemessage を指定します。 org #+attr_html: :x-type message #+begin_quote メッセージをここに #+end_quote これは次のようにマークダウンに変換され、 markdown :::message メッセージをここに ::: 次のように表示されます。

       <div x-type="message">
      
       > メッセージをここに
       </div>
      

      alert : ブロッククオートに #+attr_html:x-typealert を指定します。 org #+attr_html: :x-type alert #+begin_quote メッセージをここに #+end_quote これは次のようにマークダウンに変換され、 markdown :::alert メッセージをここに ::: 次のように表示されます。

       <div x-type="alert">
      
       > メッセージをここに
       </div>
      

      details : ブロッククオートに #+attr_html:x-typedetails を指定します。summaryを指定したい場合は x-summary にサマリを指定します。 org #+attr_html: :x-type details :x-summary タイトル #+begin_quote 表示したい内容 #+end_quote これは次のようにマークダウンに変換され、 markdown :::details タイトル 表示したい内容 ::: 次のように表示されます。

       <div x-type="details" x-summary="タイトル">
      
       > 表示したい内容
       </div>
      
       なお、 `x-summary` 要素を省略した場合は `details` をサマリに指定したと解釈されます。
      
    • 埋め込みショートコード

      ショートコードは悩んだ結果、それぞれのサービス名をschemeとして指定するリンクを特別に解釈することにしました。

      つまりorgで次のように記述した場合、

      [[tweet://https://twitter.com/conao_3/status/1308747297790898176]]
      [[youtube://jNa3axo40qM]]
      [[codepen://https://codepen.io/alphardex/pen/poyOMgr]]
      [[slideshare://866aBtwVYswXvu]]
      [[speakerdeck://3491c9ab20ef40938119aecadb06f1c6]]
      [[jsfiddle://https://jsfiddle.net/chrisvfritz/50wL7mdz]]
      

      次のようにマークダウンで出力され、

      @[tweet](https://twitter.com/conao_3/status/1308747297790898176)
      @[youtube](jNa3axo40qM)
      @[codepen](https://codepen.io/alphardex/pen/poyOMgr)
      @[slideshare](866aBtwVYswXvu)
      @[speakerdeck](3491c9ab20ef40938119aecadb06f1c6)
      @[jsfiddle](https://jsfiddle.net/chrisvfritz/50wL7mdz)
      

      次のように表示されます。

      [BROKEN LINK: tweet://https://twitter.com/conao_3/status/1308747297790898176]

      [BROKEN LINK: youtube://jNa3axo40qM]

      [BROKEN LINK: codepen://https://codepen.io/alphardex/pen/poyOMgr]

      [BROKEN LINK: slideshare://866aBtwVYswXvu]

      [BROKEN LINK: speakerdeck://3491c9ab20ef40938119aecadb06f1c6]

      [BROKEN LINK: jsfiddle://https://jsfiddle.net/chrisvfritz/50wL7mdz]

      なお、この拡張は他のバックエンドで動きません。 またデフォルトの状態ではこのリンクは未定義要素へのリンクとして解釈されるため、他のドキュメントへのエクスポートが途中で止まってしまいます。。

      これは未定義要素へのリンクを無視するように指定することで回避できます。 orgドキュメントの上の方に #+options: broken-links:mark と記述します。

    • 画像の拡張記法

      orgにおいて、画像を表示するにはdescriptionなしのリンクで表現します。

      さらにZennは独自の記法により画像の横幅を指定できるようになっています。 この機能については #+attr_htmlalt 属性と width 属性を指定すると適切に出力するようにしました。

      つまりこのorgフォーマットは

      [[https://raw.githubusercontent.com/conao3/files/master/blob/headers/png/ox-zenn.el.png]]
      
      #+attr_html: :alt image
      [[https://raw.githubusercontent.com/conao3/files/master/blob/headers/png/ox-zenn.el.png]]
      
      #+attr_html: :alt image :width 250px
      [[https://raw.githubusercontent.com/conao3/files/master/blob/headers/png/ox-zenn.el.png]]
      

      このように変換され、

      ![](https://raw.githubusercontent.com/conao3/files/master/blob/headers/png/ox-zenn.el.png)
      ![image](https://raw.githubusercontent.com/conao3/files/master/blob/headers/png/ox-zenn.el.png)
      ![image](https://raw.githubusercontent.com/conao3/files/master/blob/headers/png/ox-zenn.el.png =250x)
      

      このように表示されます。

      image
      image

      注意点は画像っぽいリンク、正確に言えば org-html-inline-image-rules にマッチするリンクでないとインライン表示されず、単なるリンクになってしまいます。

      org-html-inline-image-rules
      ;;=> (("file" . "\\.\\(jpeg\\|jpg\\|png\\|gif\\|svg\\)\\'")
      ;;    ("attachment" . "\\.\\(jpeg\\|jpg\\|png\\|gif\\|svg\\)\\'")
      ;;    ("http" . "\\.\\(jpeg\\|jpg\\|png\\|gif\\|svg\\)\\'")
      ;;    ("https" . "\\.\\(jpeg\\|jpg\\|png\\|gif\\|svg\\)\\'"))
      
  • org-publishサポート

    この章はオプショナルです。

    ox-zennはorg-publishをサポートしているので、若干の設定が必要になりますが、こちらの方が便利だという向きもあるかもしれません。

    (leaf ox-zenn
      :ensure t
      :after org
      :require t ox-publish
      :defun zenn/f-parent org-publish
      :defvar org-publish-project-alist
      :preface
      (defvar zenn/org-dir "~/dev/repos/zenn-src/org")
    
      (defun zenn/org-publish (arg)
        "Publish zenn blog files."
        (interactive "P")
        (let ((force (or (equal '(4) arg) (equal '(64) arg)))
              (async (or (equal '(16) arg) (equal '(64) arg))))
          (org-publish "zenn" arg force async)))
    
      :config
      (setf
       (alist-get "zenn" org-publish-project-alist nil nil #'string=)
       (list
        :base-directory (expand-file-name "" zenn/org-dir)
        :base-extension "org"
        :publishing-directory (expand-file-name "../" zenn/org-dir)
        :recursive t
        :publishing-function 'org-zenn-publish-to-markdown)))
    

    こうすれば次のようなディレクトリ構成にした上で、orgディレクトリ以下のorgドキュメントを一括でarticlesディレクトリとbooksディレクトリにマークダウンに変換できます。(1記事/1orgドキュメント)

    ~/dev/repos/zenn-src/
    ├── articles/
    │   ├── (ox-zenn-usage.md)
    │   ├── (ox-publish-usage.md)
    │   ├── (ox-odt-usage.md)
    │   └── (ox-html-usage.md)
    ├── books/
    │   └── ox-zenn-book/
    │       ├── (1.md)
    │       ├── (2.md)
    │       ├── (3.md)
    │       ├── (config.yml)
    │       └── (cover.png)
    ├── org/
    │   ├── articles/
    │   │   ├── ox-zenn-usage.org
    │   │   ├── ox-publish-usage.org
    │   │   ├── ox-odt-usage.org
    │   │   └── ox-html-usage.org
    │   └── articles
    │       └── ox-zenn-book/
    │           ├── 1.org
    │           ├── 2.org
    │           ├── 3.org
    │           ├── config.yml
    │           └── cover.png
    └── README.org
    

    ()で囲われたファイルが生成対象のファイル群です。 orgディレクトリでのディレクトリ構造のままファイルがpublishされていることが分かります。

  • subtreeスタイル

    この章もオプショナルです。

    私はこの方法でZennコンテンツを管理することにしました。

    ディレクトリ構造はこのようになります。

    ~/dev/repos/zenn-src/
    ├── articles/
    │   ├── (ox-zenn-usage.md)
    │   ├── (ox-publish-usage.md)
    │   ├── (ox-odt-usage.md)
    │   └── (ox-html-usage.md)
    ├── books/
    │   └── ox-zenn-book/
    │       ├── (1.md)
    │       ├── (2.md)
    │       ├── (3.md)
    │       ├── config.yml
    │       └── cover.png
    ├── main.org
    └── README.org
    

    ()で囲われたファイルが生成対象のファイル群です。

    main.orgに全てのコンテンツを集中させる方法(多記事/1orgドキュメント)です。 ox-hugoのような積極的なサポートではないですが、org標準の機能を使ってもこのように記事を管理できます。

    main.orgは次のような形です。

    #+title: Zenn blog source
    #+author: conao3
    #+date: <2020-09-24 Thu>
    #+options: ^:{} toc:nil
    
    * articles
    ** org-modeドキュメントからZenn Flavored Markdownを生成するox-zennの使い方
    :PROPERTIES:
    :EXPORT_FILE_NAME: articles/ox-zenn-usage
    :EXPORT_GFM_TAGS: markdown zenn
    :EXPORT_GFM_CUSTOM_FRONT_MATTER: :emoji 👩‍💻 :type tech
    :EXPORT_GFM_CUSTOM_FRONT_MATTER+: :published true
    :END:
    
    *** org-modeとは
    **** 概要
    「[[https://orgmode.org/][org-mode]]とはEmacsのメジャーモードのひとつです」という説明は正しいのですが、org-modeの力を説明するには全く言葉が足りません。
    ...
    

    単一orgドキュメントで管理するようになるので、各記事は第2レベルをトップレベルとするようになり、記事ごとのオプションはプロパティドロワで設定するようになります。

    プロパティには EXPORT_ が前置されることに注意してください。

    エクスポートは記事タイトル(つまり第2レベルの見出し)にポイントを乗せた上で、 C-c C-e C-s z z という呪文()を入力すれば出力できます。

まとめ

ox-zennを使うことでorgドキュメントからZenn拡張の含まれたマークダウンを出力できるようになりました。 ぜひみなさんもこのバックエンドを使ってorgからZennの記事を書いてもらえればと思います。

気に入ってもらえたら、ぜひzennかpatreonでサポートを頂ければと思います!

TODO org-modeから始めるEmacs入門

はじめに

  • 自己紹介

    • 山下 直哉 (@conao3)
    • 学業
      • 広島大学先進理工系科学研究科
        パターン認識研究室 (栗田・宮尾研究室)
        • 機械学習を用いた単眼動画像の深度予測
    • 趣味

界隈の最近

  • 2020年代のEmacs入門3

  • Emacsに一生入門できねえ20204

  • 風になりたい奴だけがEmacsを使えばいい 20205

  • 最近のEmacsJP

    • 事実として、他コミュニティのような流量はない
      • vim-jp, Ruby-jp, Rust-jp,
      • プログラミング言語処理系が好きな人の集まり,,,
    • しかし、数多くの心強いつよつよハッカー達が集まっている
      • ぜひ気軽に質問をしてください!
      • 「Emacs-jp」で検索!
    • Windows周辺の情報のまとめ、現在進行中!

それはそれとして

  • Zenn

    • 概要
      • Qiita的な技術情報ブログサイト
      • Note的な投げ銭システム
      • とても簡単な「本」の出版 (公開)
      • ローカルプレビューのためのcliの提供
      • GitHub連携
    • システム
      • 個人開発 (CatNoseさん)
        • Zenn needs help6
      • Next.js, Rails, GCP, Vercel

org-mode

  • org-mode

    • markdown的なstructuredプレーンテキスト
    • Emacsでプレーンテキストを扱うならこれ
    • org-tangle, org-export, org-publish, org-agenda, org-clock, org-capture, org-table 等々の便利機能
      • Emacs界のキラーパッケージ
      • (しかしでかすぎて全容を把握できない)
    • org-export周辺だけでも、とても便利!
    • 卒論をorgで書いた
      • org -> LaTeX -> pdf
    • ブログ(conao3.com)もorg (ox-hugo)
  • [markdown] / org-mode

    • 思想は同じ
      • structuredかつ簡単なスタイルを付けたプレーンテキスト
    • markdownの進化
      • 本来のmarkdownの仕様は小さく、(需要に対して)不十分だった
        • 言語名付きのソースブロックはGFM拡張
      • 最初の実装はperlスクリプト
      • 「俺のmarkdown」(方言)の大量発生
      • 基本的にCommonMarkに収束
      • 空行やインデントの扱いなど、多くの実装依存な部分
  • markdown / [org-mode]

    • org-modeの進化
      • Elispでの実装がずっとメンテナンスされている
        • 方言はない
      • Emacsで使うことを前提に、ノートテイキング、タスク管理などの機能
      • 文芸的プログラミングのさきがけ
        • jupyter notebookで有名に
      • 任意のフォーマットへの変換機能!
        • text, md, rst, LaTeX, beamer, html, odt(word)
        • このスライドもorg形式 -> reveal.js向けのhtml
  • org文書のイメージ7

  • 文芸的プログラミング

    • Donald Ervin Knuth (1938-) 提唱
    • (狭義)ドキュメントとコードを同じファイルに書く
    • 狭義では単に埋め込みドキュメントも該当してしまう
      • コード断片の評価及び返り値参照
      • コード断片のマクロ展開
        • 記述順に依存しない
  • orgの文芸的プログラミング

    org-tangle: org文書からプログラムを出力

  • ox-zenn

    • orgからzenn形式のmarkdownへ変換
    • 今日! MELPAにマージ (23パッケージ目)
    • ox-zennの開発期間は2日 (開発1日、修正1日)
    • なぜそんなに早く開発できたのか
      • 中間表現はすでに提供されている
      • 多くの関連パッケージの存在
    • 使い方はzennで公開8
    • 作り方も公開予定
  • ox-zenn

まとめ

  • まとめ

    • org-modeの紹介
      • org-tangle, org-export, org-publish
    • zennの紹介とox-zennを作った話

    今回、時間の (そして私の知識の) 関係で、
    orgの他の大部分の機能 (特にタスク管理)
    については紹介できませんでした。

    しかし、orgのドキュメント変換機能は単体でもとても便利です。

    私はorg-modeを使いたくてEmacsに入門しました。
    ぜひ皆さんもorg-modeを使ってみて、そのパワフルさを感じてください!

    Emacsに入門する際にはEmacsJPで支えますので、
    ぜひSlackに入ってみてください!
    https://emacs-jp.slack.com

DONE dioxus-tui入門

はじめに

この記事はRustアドベントカレンダー16日目の記事です。 昨日はyukinaritさんの「PGOでRustで書かれた広告サーバを早くする」でした。

RustでTUIアプリケーションを作りたいなーっと思っていろいろ探していると dioxus-tui というライブラリを見つけたので、使ってみた記事を書きます。

dioxusとは

TUIアプリケーション専用ではなく、仮想DOMを使ってWebフロントエンド、デスクトップアプリ、モバイルアプリを書き出すクレート。 日本語記事ではここらへんを参照

この記事ではdioxusを使ってTUIアプリを作ってみます。

Getting Started

適当なRustプロジェクトを作って遊んでみます。

mkdir rust-dioxus-tui-sample
cd rust-dioxus-tui-sample
cargo init --name dioxus-tui-sample

cargoで取ってきます。

cargo add dioxus dioxus-tui

src/main.rs を以下で置き換えます。

use dioxus::prelude::*;

fn main() {
    dioxus_tui::launch(app);
}

fn app(cx: Scope) -> Element {
    cx.render(rsx! {
        div {
            width: "100%",
            height: "10px",
            background_color: "red",
            justify_content: "center",
            align_items: "center",
            "Hello world!"
        }
    })
}

cargoで起動します。

cargo run

起動できたら成功です! 🎉

rsx!マクロ

  • rsx

    JSXのようなインターフェースを提供するマクロ。次のようなサンプルが動きます。

    use dioxus::prelude::*;
    
    fn main() {
        dioxus_tui::launch(app);
    }
    
    fn app(cx: Scope) -> Element {
        let val = 42;
        let name = if val == 42 { "Jack" } else { "Bob" };
    
        cx.render(rsx! (
            div {
                "hello world"
                "hello {val}"
                "hello {name}"
            }
        ))
    }
    

    なおJSXのように {} の中に任意の式を書くことはできないようです。一旦rsxマクロの外で計算しておくのがおすすめとのこと。

  • rsx - bool.then()

    一方でこういうのはできるようです。

    let show_title = true;
    rsx!(
        div {
            // Renders nothing by returning None when show_title is false
            show_title.then(|| rsx!{
                "This is the title"
            })
        }
    )
    
  • rsx - Option.map()

    Option に対する map 風のAPIも使えます。

    let user_name = Some("bob");
    rsx!(
        div {
            // Renders nothing if user_name is None
            user_name.map(|name| rsx!("Hello {name}"))
        }
    )
    
  • rsx - iter

    動的に rsx! を作ることもできます。

    let names = ["jim", "bob", "jane", "doe", "a"];
    
    cx.render(rsx! (
        ul {
            names.iter().map(|name| rsx!{
                li { "{name}" }
            })
        }
    ))
    

    が、tuiのレンダラは上手く表示できないようです。(改行せずに上書き表示されてる?)

  • rsx - Components

    Componentの実装

    #![allow(non_snake_case)]
    
    use dioxus::prelude::*;
    
    #[derive(Props)]
    struct TitleCardProps<'a> {
        title: &'a str,
    }
    
    fn TitleCard<'a>(cx: Scope<'a, TitleCardProps<'a>>) -> Element {
        cx.render(rsx!{
            h1 { "{cx.props.title}" }
        })
    }
    
    fn main() {
        dioxus_tui::launch(app);
    }
    
    fn app(cx: Scope) -> Element {
        cx.render(rsx! (
            TitleCard {
                title: "hello world"
            }
        ))
    }
    

hooks

React風のhookが利用できます。

  • useState

    use_state はReactと全く同じです。

    let count = use_state(&cx, || 0);
    
    cx.render(rsx! (
        div {
            "count: {count}"
        }
    ))
    

    本来ならボタンを追加して値の更新をするのですが。。dioxus-tuiにはまだボタンが実装されていなく確かめられませんでした。

  • useFuture

    dioxusの拡張。async関数を取って、非同期で実行してくれます。

    use dioxus::prelude::*;
    
    fn main() {
        dioxus_tui::launch(app);
    }
    
    fn app(cx: Scope) -> Element {
        let count = use_state(&cx, || 0);
    
        use_future(&cx, (), move |_| {
            let count = count.to_owned();
            let update = cx.schedule_update();
            async move {
                loop {
                    count.with_mut(|f| *f += 1);
                    tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
                    update();
                }
            }
        });
    
        cx.render(rsx! {
            div { width: "100%",
                div {
                    width: "50%",
                    height: "5px",
                    background_color: "blue",
                    justify_content: "center",
                    align_items: "center",
                    "Hello {count}!"
                }
                div {
                    width: "50%",
                    height: "10px",
                    background_color: "red",
                    justify_content: "center",
                    align_items: "center",
                    "Hello {count}!"
                }
            }
        })
    }
    

    良く見るとこのサンプル、背景赤の場所に表示されるべき文字が左に出てますね。。

Events

on* を使ってイベントを処理できる。。らしい。が、dioxus-tui(0.2.2)ではpanicしてしまった。

let log_event = move |event: Event| {
    events.write().push(event);
};


cx.render(rsx! {
    div {
        width: "100%",
        height: "100%",
        flex_direction: "column",
        div {
            width: "80%",
            height: "50%",
            border_width: "1px",
            justify_content: "center",
            align_items: "center",
            background_color: "hsl(248, 53%, 58%)",

            onmousemove: move |event| log_event(Event::MouseMove(event.data)),
            onclick: move |event| log_event(Event::MouseClick(event.data)),
            ondblclick: move |event| log_event(Event::MouseDoubleClick(event.data)),
            onmousedown: move |event| log_event(Event::MouseDown(event.data)),
            onmouseup: move |event| log_event(Event::MouseUp(event.data)),

            onwheel: move |event| log_event(Event::Wheel(event.data)),

            onkeydown: move |event| log_event(Event::KeyDown(event.data)),
            onkeyup: move |event| log_event(Event::KeyUp(event.data)),
            onkeypress: move |event| log_event(Event::KeyPress(event.data)),

            onfocusin: move |event| log_event(Event::FocusIn(event.data)),
            onfocusout: move |event| log_event(Event::FocusOut(event.data)),

            "Hover, click, type or scroll to see the info down below"
        },
        div {
            width: "80%",
            height: "50%",
            flex_direction: "column",
            events_rendered,
        },
    },
})

https://github.com/DioxusLabs/dioxus/blob/fc2aaa7df5/packages/tui/examples/tui_all_events.rs レポジトリに置いてあるHEADのサンプルも同じくpanicしてしまうのでどこかのコミットで壊れてそのままになってる。。かなにか動かし方を間違えてるようです。 動かせた人は教えてくれると嬉しいです。

まとめ

dioxusという意欲的なプロジェクトを見つけたのでちょっと動かしてみました。 TUIのレンダラはまだまだ発展途上のようですが、React文化をTUIに持ってくるのは普通に便利だと思うので期待してます。 (ユーザーとしては素直にnodeのinkを使った方がいいかもしれない。ただ俺はRustでTUIを作りたいんだ。。!)

doc

TODO leaf.el - Flexible, declarative and modern init.el package configuration

leaf

TODO oj.el - Competitive programming tools client for AtCoder, Codeforces

Description

Competitive programming tools(oj, oj-template) client for AtCoder, Codeforces, and more!

Now support below online judges and feature.

(defvar oj-online-judges
  '((aoj                . ((name . "Aizu Online Judge")        (url . "https://onlinejudge.u-aizu.ac.jp/")))
    (aoj2               . ((name . "Aizu Online Judge (Beta)") (url . "https://onlinejudge.u-aizu.ac.jp/courses/")))
    (anrchy-golf        . ((name . "Anarchy Golf")             (url . "http://golf.shinh.org/p.rb?")))
    (atcoder            . ((name . "AtCoder")                  (url . "https://atcoder.jp/contests/")))
    (codeforces         . ((name . "Codeforces")               (url . "https://codeforces.com/contests/")))
    (cs-academy         . ((name . "CS Academy")               (url . "https://csacademy.com/contests/")))
    (facebook           . ((name . "Facebook Hacker Cup")      (url . "https://www.facebook.com/hackercup/")))
    (hackerrank         . ((name . "HackerRank")               (url . "https://www.hackerrank.com/challenges/")))
    (hackerrank-contest . ((name . "HackerRank Contest")       (url . "https://www.hackerrank.com/contests/")))
    (kattis             . ((name . "Kattis")                   (url . "https://open.kattis.com/problems/")))
    (poj                . ((name . "PKU JudgeOnline")          (url . "http://poj.org/problem?id=")))
    (topcoder           . ((name . "Topcoder")                 (url . "https://www.topcoder.com/challenges/")))
    (toph               . ((name . "Toph (Problem Archive)")   (url . "https://toph.co/p/")))
    (codechef           . ((name . "CodeChef")                 (url . "https://www.codechef.com/problems/")))
    (soj                . ((name . "Sphere online judge")      (url . "https://www.spoj.com/problems/")))
    (yukicoder          . ((name . "yukicoder")                (url . "https://yukicoder.me/problems/no/")))
    (yukicoder-contest  . ((name . "yukicoder Contest")        (url . "https://yukicoder.me/contests/")))
    (library-checker    . ((name . "Library Checker")          (url . "https://judge.yosupo.jp/problem/"))))
  "Supported online judges.

- Download sample cases
  (aoj-arena aoj anrchy-golf atcoder codeforces
   cs-academy facebook hackerrank kattis poj toph
   codechef soj yukicoder library-checker)

- Download system cases
  (aoj yukicoder)

- Submit solution source code
  (atcoder codeforces topcoder hackerrank toph)

NOTE: online-judge symbol MUST NOT include slash (\"/\").")

Install

(leaf oj
  :ensure t
  :custom ((oj-default-online-judge . 'codeforces)))

Usage

  • oj-install-package

    This package requires oj, oj-template, selenium.

    You can install these tools via M-x oj-install-packages.

    (defun oj-install-packages ()
      "Install `oj', `oj-template', `selenium' pip package via `pip3'."
      (interactive)
      (unless (yes-or-no-p "Install `oj', `oj-template', `selenium' via pip3?")
        (error "Abort install"))
      (dolist (elm '(("oj" . "online-judge-tools")
                     ("oj-template" . "online-judge-template-generator")
                     ("selenium" . "selenium")))
        (unless (executable-find (car elm))
          (unless (executable-find "python3")
            (error "Missing `python3'.  Please ensure Emacs's PATH and the installing"))
          (unless (executable-find "pip3")
            (error "Missing `pip3'.  Please ensure Emacs's PATH and the installing"))
          (oj--exec-script (format "pip3 install %s" (cdr elm))))))
    

    But if you want to install via this funciton, you can install them by your command-line shell.

    pip3 install online-judge-tools
    pip3 install online-judge-template-generator
    pip3 install selenium
    

    And then, make sure that the Emacs’s PATH is set up properly. M-! which oj returns some path to oj executable, your Emacs can use it. If you get no output, please see Q&A or consider to use exec-path-from-shell.

  • Edit your code

    Template files are generated in oj-home-dir, open file and edit it.

    $ tree codeforces/
    codeforces/
    └── 1349
        ├── A
        │   ├── a.out
        │   ├── geckodriver.log
        │   ├── generate.py
        │   ├── main.cpp
        │   ├── main.py
        │   └── test
        │       ├── sample-1.in
        │       ├── sample-1.out
        │       ├── sample-2.in
        │       ├── sample-2.out
        │       ├── sample-3.in
        │       └── sample-3.out
        ...
        └── F2
            ├── generate.py
            ├── main.cpp
            ├── main.py
            └── test
                ├── sample-1.in
                ├── sample-1.out
                ├── sample-2.in
                ├── sample-2.out
                ├── sample-3.in
                └── sample-3.out
    
    $ cat codeforces/1349/A/main.cpp
    #include <bits/stdc++.h>
    
    using namespace std;
    
    #define ll long long
    #define ld long double
    #define v vector
    
    #define rep(i, n)      for (int i = 0; i < (int)(n); ++i)
    #define rep3(i, m, n)  for (int i = (m); i < (int)(n); ++i)
    #define rrep(i, n)     for (int i = (int)(n)-1; i >= 0; --i)
    #define rrep3(i, m, n) for (int i = (int)(n)-1; i >= (m); --i)
    #define all(x) x.begin(), x.end()
    #define rall(x) x.end(x), x.begin()
    
    #define endl '\n'
    
    
    ll gcd(ll a, ll b) { return b ? gcd(b, a % b) : a; }
    
    ll solve(int ebd, const vector<ll> & zdf) {
        // TODO: edit here
    }
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(nullptr);
    
        int ebd;
        cin >> ebd;
        vector<ll> zdf(ebd);
        rep (i, ebd) {
            cin >> zdf[i];
        }
    
        auto ans = solve(ebd, zdf);
        cout << ans << endl;
    
        return 0;
    }
    
  • oj-test

    M-x oj-test do compile and test your code.

    If your code pass testcases, get below output in *oj* buffer.

    [*] 3 cases found
    [!] GNU time is not available: time
    
    [*] sample-1
    [x] time: 0.001666 sec
    [+] AC
    
    [*] sample-2
    [x] time: 0.002213 sec
    [+] AC
    
    [*] sample-3
    [x] time: 0.001923 sec
    [+] AC
    
    [x] slowest: 0.002213 sec  (for sample-2)
    [+] test success: 3 cases
    

    Compiler command is automatically detected using the quickrun package. You may use quickrun-add-command to add or override commands.

  • oj-submit

    M-x oj-submit submit your code to online judge. (The first time, you need M-x oj-login per online judges.)

Customize

  • Variables

    oj-shell-program : A path to shell executable used *oj* buffer. (default shell-file-name (bash or some customized shell))

    oj-home-dir : A path for generate files. (default "~/.emacs.d/oj/")

    oj-default-online-judge : A online-judge used for guessing. (default 'codeforces)

    oj-compiler-c : Compiler name to submit for C/C++. (default gcc)

     If you want to also use Clang local, please add your init.el below.
     ```emacs-lisp
     (quickrun-set-default "c" "c/clang")
     (quickrun-set-default "c++" "c++/clang++")
     ```
    

    oj-compiler-python : Compiler name to submit for Python. (default cpython)

    oj-login-args : Args for oj login. (default nil) ```text usage: oj login [-h] [-u USERNAME] [-p PASSWORD] [–check] [–use-browser {always,auto,never}] url

     positional arguments:
       url
    
     optional arguments:
       -h, --help            show this help message and exit
       -u USERNAME, --username USERNAME
       -p PASSWORD, --password PASSWORD
       --check               check whether you are logged in or not
       --use-browser {always,auto,never}
                             specify whether it uses a GUI web browser
                             to login or not  (default: auto)
     ```
    

    oj-prepare-args : Args for oj-prepare. (default nil) ```text Args for `oj-prepare’.

     usage: oj-prepare [-h] [-v] [-c COOKIE] [--config-file CONFIG_FILE] url
    
     positional arguments:
       url
    
     optional arguments:
       -h, --help            show this help message and exit
       -v, --verbose
       -c COOKIE, --cookie COOKIE
       --config-file CONFIG_FILE
                  default: ~/.config/online-judge-tools/prepare.config.toml
     ```
    

    oj-test-args : Args for oj test. (default nil) ```text Args for oj-test'. Note that the runtime command (-c’) is detected automatically.

     usage: oj test [-h] [-c COMMAND] [-f FORMAT] [-d DIRECTORY] [-m
                    {simple,side-by-side}] [-S] [--no-rstrip]
                    [--rstrip] [-s] [-e ERROR] [-t TLE] [--mle MLE]
                    [-i] [-j N] [--print-memory] [--gnu-time GNU_TIME]
                    [--no-ignore-backup] [--ignore-backup] [--json]
                    [--judge-command JUDGE] [test [test ...]]
    
     positional arguments:
       test                  paths of test cases. (if empty: globbed from --format)
    
     optional arguments:
       -h, --help            show this help message and exit
       -c COMMAND, --command COMMAND
                             your solution to be tested.  (default: "./a.out")
       -f FORMAT, --format FORMAT
                             a format string to recognize the relationship of
                             test cases.  (default: "%s.%e")
       -d DIRECTORY, --directory DIRECTORY
                             a directory name for test cases (default: test/)
       -m {simple,side-by-side}, --display-mode {simple,side-by-side}
                             mode to display an output with the correct answer
                             (default: simple)
       -S, --side-by-side    display an output and the correct answer with
                             side byside mode
                             (equivalent to --display-mode side-by-side)
       --no-rstrip
       --rstrip              rstrip output before compare (default)
       -s, --silent          don't report output and correct answer even if not AC
                             (for --mode all)
       -e ERROR, --error ERROR
                             check as floating point number: correct if its absolute
                             or relative error doesn't exceed it
       -t TLE, --tle TLE     set the time limit (in second) (default: inf)
       --mle MLE             set the memory limit (in megabyte) (default: inf)
       -i, --print-input     print input cases if not AC
       -j N, --jobs N        specifies the number of jobs to run simultaneously
                             (default: no parallelization)
       --print-memory        print the amount of memory which your program used,
                             even if it is small enough
       --gnu-time GNU_TIME   used to measure memory consumption (default: "time")
       --no-ignore-backup
       --ignore-backup       ignore backup files and hidden files
                             (i.e. files like "*~", "\#*\#" and ".*")
                             (default)
       --json
       --judge-command JUDGE
                             specify judge command instead of default diff judge.
                             See https://online-judge-tools.readthedocs.io/en/
                                   master/introduction.en.html
                                   #test-for-special-forms-of-problem for details
    
     format string for --format:
       %s                    name
       %e                    extension: "in" or "out"
       (both %s and %e are required.)
    
     tips:
       You can do similar things with shell: e.g.
      `for f in test/*.in ; do echo $f ; diff <(./a.out < $f) ${f/.in/.out} ; done`
     ```
    

    oj-submit-args : Args for oj submit. (default '("-y")) ```text Args for `oj-submit’.

     usage: oj submit [-h] [-l LANGUAGE] [--no-guess] [-g]
                      [--no-guess-latest] [--guess-cxx-latest]
                      [--guess-cxx-compiler {gcc,clang,all}]
                      [--guess-python-version {2,3,auto,all}]
                      [--guess-python-interpreter {cpython,pypy,all}]
                      [--format-dos2unix] [--format-rstrip] [-G]
                      [--no-open] [--open] [-w SECOND] [-y] [url] file
    
     positional arguments:
       url                   the URL of the problem to submit.
                             if not given, guessed from history of download command.
       file
    
     optional arguments:
       -h, --help            show this help message and exit
       -l LANGUAGE, --language LANGUAGE
                             narrow down language choices if ambiguous
       --no-guess
       -g, --guess           guess the language for your file (default)
       --no-guess-latest
       --guess-cxx-latest    use the lasest version for C++ (default)
       --guess-cxx-compiler {gcc,clang,all}
                             use the specified C++ compiler if both of GCC and
                             Clang are available (default: gcc)
       --guess-python-version {2,3,auto,all}
                             default: auto
       --guess-python-interpreter {cpython,pypy,all}
                             use the specified Python interpreter if both of CPython
                             and PyPy are available (default: cpython)
       --format-dos2unix     replace CRLF with LF for given file
       --format-rstrip       remove trailing newlines from given file
       -G, --golf            now equivalent to --format-dos2unix --format-rstrip
       --no-open
       --open                open the result page after submission (default)
       -w SECOND, --wait SECOND
                             sleep before submitting
       -y, --yes             don't confirm
     ```
    
  • Template file

    In oj, you can use template file for auto generate source code.

    If you want use your customize template, you save like below file as ~/.config/online-judge-tools/template/template-ext.cpp.

    <%!
        import onlinejudge_template.generator.cplusplus as cplusplus
        import onlinejudge_template.generator.about as about
    %>\
    <%
        data['config']['rep_macro'] = 'rep'
        data['config']['using_namespace_std'] = True
        data['config']['long_long_int'] = 'll'
    %>\
    #include <iostream>
    #include <string>
    #include <vector>
    #include <algorithm>
    #include <utility>
    #include <tuple>
    #include <cstdint>
    #include <cstdio>
    #include <map>
    #include <queue>
    #include <set>
    #include <stack>
    #include <deque>
    #include <unordered_map>
    #include <unordered_set>
    #include <bitset>
    #include <cctype>
    
    using namespace std;
    
    #define ll long long
    #define ld long double
    #define v vector
    
    #define rep(i, n)      for (int i = 0; i < (int)(n); ++i)
    #define rep3(i, m, n)  for (int i = (m); i < (int)(n); ++i)
    #define rrep(i, n)     for (int i = (int)(n)-1; i >= 0; --i)
    #define rrep3(i, m, n) for (int i = (int)(n)-1; i >= (m); --i)
    #define all(x) x.begin(), x.end()
    #define rall(x) x.end(x), x.begin()
    
    #define endl '\n'
    ${cplusplus.declare_constants(data)}
    
    ll gcd(ll a, ll b) { return b ? gcd(b, a % b) : a; }
    
    ${cplusplus.return_type(data)} solve(${cplusplus.formal_arguments(data)}) {
        // TODO: edit here
    }
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(nullptr);
    
    ${cplusplus.read_input(data)}
    
        auto ${cplusplus.return_value(data)} = solve(${cplusplus.actual_arguments(data)});
    ${cplusplus.write_output(data)}
    
        return 0;
    }
    

    And save below config toml as ~/.config/online-judge-tools/prepare.config.toml.

    [templates]
    "main.py" = "main.py"
    "main.cpp" = "template-ext.cpp"
    "generate.py" = "generate.py"
    

TODO oj.el - ja - Competitive programming tools client for AtCoder, Codeforces

Intro

競技プログラミングコミュニティが年々大きくなっています。 日本ではAOJAtCoder、世界ではTopCoderCodeforcesなどのオンラインジャッジが人気です。

オンラインジャッジはコンパイル、実行を行い、あらかじめ用意されているテストケースをパスするかどうか確認してくれます。

プログラミング初心者のプログラミング導入にも最適ですし、プログラマの力試し/筋力向上としても、とても良い教材です。

ただ、サンプルケースをダウンロードしたり、解答をいい感じに管理したりと定型作業も多いです。

ojoj-templateというツールで、その定型作業を自動化できます。 そのojをEmacsと連携させた oj というパッケージを紹介します。

なお、このojは私が作ったパッケージなので、技術的な話は「」に書いてあります。

Install

(leaf oj
  :doc "Competitive programming tools client for AtCoder, Codeforces"
  :req "emacs-26.1" "quickrun-2.2"
  :tag "convenience" "emacs>=26.1"
  :url "https://github.com/conao3/oj.el"
  :emacs>= 26.1
  :ensure t
  :custom ((oj-compiler-c . "clang")
           (oj-compiler-python . "cpython")
           (oj-default-online-judge . 'atcoder)))

Usage

  • oj-install-package

    このパッケージはoj, oj-template, seleniumに依存しています。

    これらのソフトは M-x oj-install-packages でインストールできます。

    (defun oj-install-packages ()
      "Install `oj', `oj-template', `selenium' pip package via `pip3'."
      (interactive)
      (unless (yes-or-no-p "Install `oj', `oj-template', `selenium' via pip3?")
        (error "Abort install"))
      (dolist (elm '(("oj" . "online-judge-tools")
                     ("oj-template" . "online-judge-template-generator")
                     ("selenium" . "selenium")))
        (unless (executable-find (car elm))
          (unless (executable-find "python3")
            (error "Missing `python3'.  Please ensure Emacs's PATH and the installing"))
          (unless (executable-find "pip3")
            (error "Missing `pip3'.  Please ensure Emacs's PATH and the installing"))
          (oj--exec-script (format "pip3 install %s" (cdr elm))))))
    

    もしこの関数によってインストールしたくない場合は、ターミナルで次のコマンドでインストールします。

    pip3 install online-judge-tools
    pip3 install online-judge-template-generator
    pip3 install selenium
    

    さらにEmacsの PATH が適切に設定されていることを確認してください。 M-! which ojoj へのパスを返せば、Emacsからojを利用できます。 出力がない場合は、Q&Aを参照するか、exec-path-from-shellを導入することを検討して下さい。

  • Edit your code

    oj-home-dir にファイルが生成されるので、ファイルを開いて編集して下さい。

    $ tree codeforces/
    codeforces/
    └── 1349
        ├── A
        │   ├── a.out
        │   ├── geckodriver.log
        │   ├── generate.py
        │   ├── main.cpp
        │   ├── main.py
        │   └── test
        │       ├── sample-1.in
        │       ├── sample-1.out
        │       ├── sample-2.in
        │       ├── sample-2.out
        │       ├── sample-3.in
        │       └── sample-3.out
        ...
        └── F2
            ├── generate.py
            ├── main.cpp
            ├── main.py
            └── test
                ├── sample-1.in
                ├── sample-1.out
                ├── sample-2.in
                ├── sample-2.out
                ├── sample-3.in
                └── sample-3.out
    
    $ cat codeforces/1349/A/main.cpp
    #include <bits/stdc++.h>
    
    using namespace std;
    
    #define ll long long
    #define ld long double
    #define v vector
    
    #define rep(i, n)      for (int i = 0; i < (int)(n); ++i)
    #define rep3(i, m, n)  for (int i = (m); i < (int)(n); ++i)
    #define rrep(i, n)     for (int i = (int)(n)-1; i >= 0; --i)
    #define rrep3(i, m, n) for (int i = (int)(n)-1; i >= (m); --i)
    #define all(x) x.begin(), x.end()
    #define rall(x) x.end(x), x.begin()
    
    #define endl '\n'
    
    
    ll gcd(ll a, ll b) { return b ? gcd(b, a % b) : a; }
    
    ll solve(int ebd, const vector<ll> & zdf) {
        // TODO: edit here
    }
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(nullptr);
    
        int ebd;
        cin >> ebd;
        vector<ll> zdf(ebd);
        rep (i, ebd) {
            cin >> zdf[i];
        }
    
        auto ans = solve(ebd, zdf);
        cout << ans << endl;
    
        return 0;
    }
    
  • oj-test

    M-x oj-testcompiletest を行います。

    もしテストケースをパスしたら、 *oj* バッファに次のように出力されます。

    [*] 3 cases found
    [!] GNU time is not available: time
    
    [*] sample-1
    [x] time: 0.001666 sec
    [+] AC
    
    [*] sample-2
    [x] time: 0.002213 sec
    [+] AC
    
    [*] sample-3
    [x] time: 0.001923 sec
    [+] AC
    
    [x] slowest: 0.002213 sec  (for sample-2)
    [+] test success: 3 cases
    

    コンパイルコマンドは自動的に quickrun パッケージの定義から推測されます。 quickrun-add-command を使用することで、追加や上書きができます。

  • oj-submit

    オンラインジャッジに提出するには M-x oj-submit を実行します。 (初回にはオンラインジャッジごとに M-x oj-login が必要です)

Customize

  • Variables

    oj-shell-program : *oj* バッファで使われるシェル (default shell-file-name (bash or some customized shell))

    oj-home-dir : ファイルが生成されるパス (default "~/.emacs.d/oj/")

    oj-default-online-judge : 規定のオンラインジャッジ (default 'codeforces)

    oj-compiler-c : C/C++の提出で使われるコンパイラ (default gcc)

     Clangをローカルで使いたい場合は次のスニペットをinit.elに追加します。
     ```emacs-lisp
     (quickrun-set-default "c" "c/clang")
     (quickrun-set-default "c++" "c++/clang++")
     ```
    

    oj-compiler-python : Pythonの提出で使われるコンパイラ (default cpython)

    oj-login-args : oj login のオプション引数 (default nil) ```text usage: oj login [-h] [-u USERNAME] [-p PASSWORD] [–check] [–use-browser {always,auto,never}] url

     positional arguments:
       url
    
     optional arguments:
       -h, --help            show this help message and exit
       -u USERNAME, --username USERNAME
       -p PASSWORD, --password PASSWORD
       --check               check whether you are logged in or not
       --use-browser {always,auto,never}
                             specify whether it uses a GUI web browser
                             to login or not  (default: auto)
     ```
    

    oj-prepare-args : oj-prepare のオプション引数 (default nil) ```text Args for `oj-prepare’.

     usage: oj-prepare [-h] [-v] [-c COOKIE] [--config-file CONFIG_FILE] url
    
     positional arguments:
       url
    
     optional arguments:
       -h, --help            show this help message and exit
       -v, --verbose
       -c COOKIE, --cookie COOKIE
       --config-file CONFIG_FILE
                  default: ~/.config/online-judge-tools/prepare.config.toml
     ```
    

    oj-test-args : oj test のオプション引数 (default nil) ```text Args for oj-test'. Note that the runtime command (-c’) is detected automatically.

     usage: oj test [-h] [-c COMMAND] [-f FORMAT] [-d DIRECTORY] [-m
                    {simple,side-by-side}] [-S] [--no-rstrip]
                    [--rstrip] [-s] [-e ERROR] [-t TLE] [--mle MLE]
                    [-i] [-j N] [--print-memory] [--gnu-time GNU_TIME]
                    [--no-ignore-backup] [--ignore-backup] [--json]
                    [--judge-command JUDGE] [test [test ...]]
    
     positional arguments:
       test                  paths of test cases. (if empty: globbed from --format)
    
     optional arguments:
       -h, --help            show this help message and exit
       -c COMMAND, --command COMMAND
                             your solution to be tested.  (default: "./a.out")
       -f FORMAT, --format FORMAT
                             a format string to recognize the relationship of
                             test cases.  (default: "%s.%e")
       -d DIRECTORY, --directory DIRECTORY
                             a directory name for test cases (default: test/)
       -m {simple,side-by-side}, --display-mode {simple,side-by-side}
                             mode to display an output with the correct answer
                             (default: simple)
       -S, --side-by-side    display an output and the correct answer with
                             side byside mode
                             (equivalent to --display-mode side-by-side)
       --no-rstrip
       --rstrip              rstrip output before compare (default)
       -s, --silent          don't report output and correct answer even if not AC
                             (for --mode all)
       -e ERROR, --error ERROR
                             check as floating point number: correct if its absolute
                             or relative error doesn't exceed it
       -t TLE, --tle TLE     set the time limit (in second) (default: inf)
       --mle MLE             set the memory limit (in megabyte) (default: inf)
       -i, --print-input     print input cases if not AC
       -j N, --jobs N        specifies the number of jobs to run simultaneously
                             (default: no parallelization)
       --print-memory        print the amount of memory which your program used,
                             even if it is small enough
       --gnu-time GNU_TIME   used to measure memory consumption (default: "time")
       --no-ignore-backup
       --ignore-backup       ignore backup files and hidden files
                             (i.e. files like "*~", "\#*\#" and ".*")
                             (default)
       --json
       --judge-command JUDGE
                             specify judge command instead of default diff judge.
                             See https://online-judge-tools.readthedocs.io/en/
                                   master/introduction.en.html
                                   #test-for-special-forms-of-problem for details
    
     format string for --format:
       %s                    name
       %e                    extension: "in" or "out"
       (both %s and %e are required.)
    
     tips:
       You can do similar things with shell: e.g.
      `for f in test/*.in ; do echo $f ; diff <(./a.out < $f) ${f/.in/.out} ; done`
     ```
    

    oj-submit-args : oj submit のオプション引数 (default '("-y")) ```text Args for `oj-submit’.

     usage: oj submit [-h] [-l LANGUAGE] [--no-guess] [-g]
                      [--no-guess-latest] [--guess-cxx-latest]
                      [--guess-cxx-compiler {gcc,clang,all}]
                      [--guess-python-version {2,3,auto,all}]
                      [--guess-python-interpreter {cpython,pypy,all}]
                      [--format-dos2unix] [--format-rstrip] [-G]
                      [--no-open] [--open] [-w SECOND] [-y] [url] file
    
     positional arguments:
       url                   the URL of the problem to submit.
                             if not given, guessed from history of download command.
       file
    
     optional arguments:
       -h, --help            show this help message and exit
       -l LANGUAGE, --language LANGUAGE
                             narrow down language choices if ambiguous
       --no-guess
       -g, --guess           guess the language for your file (default)
       --no-guess-latest
       --guess-cxx-latest    use the lasest version for C++ (default)
       --guess-cxx-compiler {gcc,clang,all}
                             use the specified C++ compiler if both of GCC and
                             Clang are available (default: gcc)
       --guess-python-version {2,3,auto,all}
                             default: auto
       --guess-python-interpreter {cpython,pypy,all}
                             use the specified Python interpreter if both of CPython
                             and PyPy are available (default: cpython)
       --format-dos2unix     replace CRLF with LF for given file
       --format-rstrip       remove trailing newlines from given file
       -G, --golf            now equivalent to --format-dos2unix --format-rstrip
       --no-open
       --open                open the result page after submission (default)
       -w SECOND, --wait SECOND
                             sleep before submitting
       -y, --yes             don't confirm
     ```
    
  • Template file

    ojにおいて、自動生成されるソースコードにはテンプレートファイルを使うことができます。

    テンプレートをカスタマイズするには、下記の様なファイルを ~/.config/online-judge-tools/template/template-ext.cpp として保存します。

    <%!
        import onlinejudge_template.generator.cplusplus as cplusplus
        import onlinejudge_template.generator.about as about
    %>\
    <%
        data['config']['rep_macro'] = 'rep'
        data['config']['using_namespace_std'] = True
        data['config']['long_long_int'] = 'll'
    %>\
    #include <iostream>
    #include <string>
    #include <vector>
    #include <algorithm>
    #include <utility>
    #include <tuple>
    #include <cstdint>
    #include <cstdio>
    #include <map>
    #include <queue>
    #include <set>
    #include <stack>
    #include <deque>
    #include <unordered_map>
    #include <unordered_set>
    #include <bitset>
    #include <cctype>
    
    using namespace std;
    
    #define ll long long
    #define ld long double
    #define v vector
    
    #define rep(i, n)      for (int i = 0; i < (int)(n); ++i)
    #define rep3(i, m, n)  for (int i = (m); i < (int)(n); ++i)
    #define rrep(i, n)     for (int i = (int)(n)-1; i >= 0; --i)
    #define rrep3(i, m, n) for (int i = (int)(n)-1; i >= (m); --i)
    #define all(x) x.begin(), x.end()
    #define rall(x) x.end(x), x.begin()
    
    #define endl '\n'
    ${cplusplus.declare_constants(data)}
    
    ll gcd(ll a, ll b) { return b ? gcd(b, a % b) : a; }
    
    ${cplusplus.return_type(data)} solve(${cplusplus.formal_arguments(data)}) {
        // TODO: edit here
    }
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(nullptr);
    
    ${cplusplus.read_input(data)}
    
        auto ${cplusplus.return_value(data)} = solve(${cplusplus.actual_arguments(data)});
    ${cplusplus.write_output(data)}
    
        return 0;
    }
    

    さらに下記の様なtomlを ~/.config/online-judge-tools/prepare.config.toml として保存します。

    [templates]
    "main.py" = "main.py"
    "main.cpp" = "template-ext.cpp"
    "generate.py" = "generate.py"
    

emacs


  1. IBM Developer - プログラミング言語の進化を追え 第1回 ↩︎

  2. Wikipedia - John McCarthy ↩︎

  3. 2020年代のEmacs入門 - https://emacs-jp.github.io/tips/emacs-in-2020.html ↩︎

  4. Emacsに一生入門できねえ2020 - https://anond.hatelabo.jp/20200921040234 ↩︎

  5. 風になりたい奴だけがEmacsを使えばいい 2020 - https://blog.tomoya.dev/posts/only-those-who-want-to-be-the-wind-should-use-emacs-2020/ ↩︎

  6. Zenn needs help - https://catnose99.com/zenn-needs-help/ ↩︎

  7. Eric Schulte, Dan Davison, Thomas Dye, et.al., “A Multi-Language Computing Environment for Literate Programming and Reproducible Research” https://www.jstatsoft.org/article/view/v046i03 ↩︎

  8. ox-zennの使い方 - https://zenn.dev/conao3/articles/ox-zenn-usage ↩︎