Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

(更新あり)AWS LambdaでCommon Lispを動かす

[追記]IPv6を無効化することによりうまく動作させることができました.詳細は記事下方を参照してください.

いやー来ましたね.AWS Lambdaで新言語・・・あれ??

aws.amazon.com

ナンダテメッコラー!!Lambdaの名前を冠しておきながらAWS LambdaでCommon Lispが動かないとはどういうことなんだ. 幸いにもLambdaにはCustom Runtimeという仕組みがあり,自由な言語を動かすことができるようになりました. というわけでAWS Lambda上で動くCommon Lispプログラムを実装してみました. 俺だってLambdaを冠したブログやってるからな.責任がありますよ責任. Let Lambda over Lambda over Lambdaじゃん.

docs.aws.amazon.com

というわけで後学のためにもLambdaでCommon Lispを動かす実験を記録することにします.

TL;DR

結論から言うと微妙なところで動いたり動かなかったりする状態になった動いたぞ!!!.コードは以下にある. roswellがあればビルド可能だ.makeって叩いたらout.zipができるので,これをLambdaに入れてやればOKだ. ビルド系をDockerfileにしたらよかったけど,眠くてあきらめたDocker化したぞ!!!

github.com

予習: Lambdaで動かすために必要なもの

bootstrapという実行ファイル(動けばよいのでシェルスクリプトでもよい)を入れたzipファイルが必要.これさえあれば動く.

bootstrapを作る

bootstrapでやらなければならない仕事は以下の通り.

  • 環境変数を受け取る
    • _HANDLER
      • Lambdaのユーザが指定したハンドラ名が入っている.呼びたい関数名だと思ってもらえればさしつかえない
    • LAMBDA_TASK_ROOT
      • ここにchdirする必要がある,理由は知らん
    • AWS_LAMBDA_RUNTIME_API
      • ここを叩いて外界とやりとりする
  • イベントループを回す
    • _HANDLERをハンドルするなら,これを使って呼び出すべき処理を決定しておく
      • 決め打ちでもよい
    • AWS_LAMBDA_RUNTIME_API以下の特定のエンドポイントにGETすると処理するべきコンテンツがもらえるので,これを処理する
    • 処理ができたらAWS_LAMBDA_RUNTIME_API以下の特定のエンドポイントにPOSTする
      • これで処理ができたことになる

bootstrap.rosを用意して,これをビルドしてbootstrapを生成する作戦で行きましょう.以下のコマンドでbootstrap.rosを生成する.

ros init bootstrap

bootstrap.rosの内容はこんな感じ.

#!/bin/sh
#|-*- mode:lisp -*-|#
#|
exec ros -Q -- $0 "$@"
|#
(progn ;;init forms
  (ros:ensure-asdf)
;;; ライブラリをインストールする
;; dexadorはHTTP  ライブラリ
;; uiopはOSまわりを扱う汎用ユーティリティ
  #+quicklisp (ql:quickload '(:dexador :uiop) :silent t)
  )

(defpackage :ros.script.bootstrap.3758360166
  (:use :cl))
(in-package :ros.script.bootstrap.3758360166)

;;; 環境変数を入れる大域変数を用意しておく
(defvar *handler*)
(defvar *lambda-task-root*)
(defvar *aws-lambda-runtime-api*)

;; 環境変数ロードする君
(defun load-envvar ()
  (setf *handler*
        (uiop:getenv "_HANDLER"))
  (setf *lambda-task-root*
        (uiop:getenv "LAMBDA_TASK_ROOT"))
  (setf *aws-lambda-runtime-api*
        (uiop:getenv "AWS_LAMBDA_RUNTIME_API")))

;; メインループ.
(defun main-loop (handler)
  (declare (ignorable handler))
  (let ((next-endpoint
;; ここを叩くとLambda呼び出し時のコンテンツがもらえる
          (format nil "http://~A/2018-06-01/runtime/invocation/next" *aws-lambda-runtime-api*)))
;; 実際にdex:getで叩く
    (loop for (body status headers . nil) = (multiple-value-list (dex:get next-endpoint))
          as request-id = (gethash "Lambda-Runtime-Aws-Request-Id" headers)
          as response = "Hello, Lambda World from Yet Another Lambda World!"
;; ここを叩いてリクエスト完了とする
          as response-endpoint = (format nil "http://~A/2018-06-01/runtime/invocation/~A/response" *aws-lambda-runtime-api* request-id)
;; 実際に叩く
          do (dex:post response-endpoint :content response))))

;; entrypoint
;; roswellはmain関数をエントリポイントとしてバイナリを作るので,ここから実行される
(defun main ()
  (load-envvar)
  (uiop:chdir *lambda-task-root*)
  (main-loop *handler*))
;;; vim: set ft=lisp lisp:

bootstrap.rosができたのでビルドする.

ros build bootstrap.ros

ZIPを固める

基本的にはさっきのbootstrapをzipで固めれば完成だ.

このほか,HTTPライブラリのdexadorがいくつか共有ライブラリ(libcrypto, libssl)を要求するので,いっしょに同梱しておく.

この手順は面倒なのでMakefileにした.

.PHONY: default

default: out.zip

libcrypto.so.1.0.0: /lib64/libcrypto.so.1.0.0
    cp /lib64/libcrypto.so.1.0.0 .

libssl.so.1.0.0: /lib64/libssl.so.1.0.0
    cp /lib64/libssl.so.1.0.0 .

out.zip: bootstrap libcrypto.so.1.0.0 libssl.so.1.0.0
    zip out.zip bootstrap libcrypto.so.1.0.0 libssl.so.1.0.0

bootstrap: bootstrap.ros
    ros build bootstrap.ros

これでmake一発でout.zipを生成できるようになった.

make

実行

AWS Lambdaのコンソールで, カスタムランタイムとしてout.zipをアップロード(10MBくらいになった)して試しにtest実行してみた結果がこれ.

START RequestId: cae88257-a707-49ee-8cd0-8e28ae8b92a7 Version: $LATEST
Unhandled SIMPLE-ERROR in thread #<SB-THREAD:THREAD "main thread" RUNNING
                                    {1002D5CFA3}>:
  Syscall poll(2) failed: Operation not permitted

Backtrace for: #<SB-THREAD:THREAD "main thread" RUNNING {1002D5CFA3}>
0: (SB-DEBUG::DEBUGGER-DISABLED-HOOK #<SIMPLE-ERROR "Syscall poll(2) failed: ~A" {1003108703}> #<unused argument>)
1: (SB-DEBUG::RUN-HOOK SB-EXT:*INVOKE-DEBUGGER-HOOK* #<SIMPLE-ERROR "Syscall poll(2) failed: ~A" {1003108703}>)
2: (INVOKE-DEBUGGER #<SIMPLE-ERROR "Syscall poll(2) failed: ~A" {1003108703}>)
3: (ERROR "Syscall poll(2) failed: ~A" "Operation not permitted")
4: (SB-UNIX:UNIX-SIMPLE-POLL 3 :INPUT -1)
5: (SB-SYS:WAIT-UNTIL-FD-USABLE 3 :INPUT NIL NIL)
6: (SB-IMPL::REFILL-INPUT-BUFFER #<SB-SYS:FD-STREAM for "socket 127.0.0.1:40116, peer: 127.0.0.1:9001" {1003106693}>)
7: (SB-IMPL::INPUT-UNSIGNED-8BIT-BYTE #<SB-SYS:FD-STREAM for "socket 127.0.0.1:40116, peer: 127.0.0.1:9001" {1003106693}> NIL NIL)
8: (READ-BYTE #<SB-SYS:FD-STREAM for "socket 127.0.0.1:40116, peer: 127.0.0.1:9001" {1003106693}> NIL NIL)
9: (DEXADOR.BACKEND.USOCKET::READ-UNTIL-CRLF*2 #<SB-SYS:FD-STREAM for "socket 127.0.0.1:40116, peer: 127.0.0.1:9001" {1003106693}>)
10: (DEXADOR.BACKEND.USOCKET::READ-RESPONSE #<SB-SYS:FD-STREAM for "socket 127.0.0.1:40116, peer: 127.0.0.1:9001" {1003106693}> T NIL T)
11: (DEXADOR.BACKEND.USOCKET:REQUEST #<unavailable argument> :METHOD :GET)
12: (MAIN-LOOP #<unused argument>)
13: (MAIN)
14: (ROSWELL::ENTRY "ROS.SCRIPT.BOOTSTRAP.3758360166::MAIN")
15: (ROSWELL:RUN ((:ENTRY "ROS.SCRIPT.BOOTSTRAP.3758360166::MAIN")))
16: ((FLET "WITHOUT-INTERRUPTS-BODY-26" :IN SB-EXT:SAVE-LISP-AND-DIE))
17: ((LABELS SB-IMPL::RESTART-LISP :IN SB-EXT:SAVE-LISP-AND-DIE))

unhandled condition in --disable-debugger mode, quitting
END RequestId: cae88257-a707-49ee-8cd0-8e28ae8b92a7
REPORT RequestId: cae88257-a707-49ee-8cd0-8e28ae8b92a7  Duration: 448.42 ms Billed Duration: 500 ms     Memory Size: 128 MB Max Memory Used: 80 MB  
RequestId: cae88257-a707-49ee-8cd0-8e28ae8b92a7 Error: Runtime exited with error: exit status 1
Runtime.ExitError

死んでんじゃねえか!ふざけんな!

とはいえCommon Lisp自体は動いているようだ!やったぜ.

しかしpoll(2)の実行に失敗しており,リクエストコンテンツの取得には至らなかったようだ.

これには同様の報告がいくつか存在しており,Lambdaのクセのようだ.

[http://y2q-actionman.hatenablog.com/entry/2018/12/06/AWS_Lambda%E3%81%AE_Custom_Runtime%E3%81%A8%E3%81%97%E3%81%A6_Common_Lisp(sbcl)%E3%82%92%E4%BD%BF%E3%81%86:title]

blog.marshallbrekka.com

後者については,Erlang処理系での記事なのでそのまま適用できるかは分からないが,IPv6サポートを切れば動くのではという示唆を与えてくれた.しかしIPv6サポートの切り方がわからなかったので諦めた.IPv6サポートを切ることができ,動作に成功したので後述する.

ひとまずCommon Lispが動いたのでめでたい.よかったですね.

まとめ

  • LambdaでCommon Lisp(SBCL, ClozureCL)は動作する
  • 動作するがソケットまわりの不具合で完動しない
  • もう一声なのでこれからに期待

参考にした記事

[http://y2q-actionman.hatenablog.com/entry/2018/12/06/AWS_Lambda%E3%81%AE_Custom_Runtime%E3%81%A8%E3%81%97%E3%81%A6_Common_Lisp(sbcl)%E3%82%92%E4%BD%BF%E3%81%86:title]

blog.marshallbrekka.com

www.m3tech.blog

qiita.com

追記 2019-02-07

この挙動はIPv6が正常に動作していないからではないかと推測していましたが,この推測はかなり当たっているようすです.同様のトラブルをまた見付けました.

github.com

さて,処理に問題が発生しているのはHTTPクライアントであるdexadorが使用している汎用ソケットライブラリusocketです.このコードを読み解いていると,以下のようなfeatureの切り替えコードがあるのを発見しました.

github.com

まず軽く*feature*について説明しましょう.*feature*とは特殊なスペシャル変数で,処理系が利用することができる機能がキーワードの形で格納されています. よくある利用方法としては処理系の判別に使われます.処理系が何であるかも,処理系が利用することができる機能として表しているのですね. 例えばsbclでのみ実行したい処理,あるいは実行したくない処理はリーダマクロ#+/#-を使って以下のように表現できます.

#+sbcl
(print "only sbcl")

#-sbcl
(print "all but sbcl")

これは*feature*:SBCLが格納されているかどうかで,読み取り時(実行時ではない)に式を評価するかどうか決定しているのです.

さて,ClozureCLという処理系ではIPv6をサポートするかどうかを:IPV6というfeatureで表現しています(featureにどういう名前を使うかは処理系依存で,可搬性はなさそうです).そしてusocketもこのフラグを利用してIPv6を使うかを判定しているようです. したがって,強制的に:IPV6*feature*から取り上げた後でライブラリを読み込ませれば,IPv6サポート無しのusocketをインストールできます.

IPv6サポートを外すために,処理系をClozureCLに変更し,スクリプトに手を入れます.

まずDockerfileを修正します.eshamsterさんのベースイメージをお借りします.

FROM eshamster/cl-base

ENV libs 'openssl-dev'
RUN apk update \
  && apk add ${libs}

# ClozureCLを使う
RUN ros install ccl-bin/1.11
RUN ros use ccl-bin/1.11

# Assuming whole application directory is mounted as /app
WORKDIR /app/

CMD /bin/sh

そしてroswellスクリプトを修正します. roswellスクリプトでライブラリがインストールされるタイミングは(ql:quickload)のタイミングなので,それより前に:IPV6を取り除きます.

(progn ;;init forms
  (ros:ensure-asdf)
  (setf *features* (delete :ipv6 *features*))
  #+quicklisp (ql:quickload '(:dexador :uiop) :silent nil)
  )

これでmakeします.

make

こうして得られたzipファイルをアップロードして動作を確認したところ………

やったぜ.SBCLでも同じことができれば同様の結果を得られそうです.

Smart::Argsのパーサを書いた

この記事は,はてなエンジニアAdvent Calendar 2018の17日目の記事です.昨日はid:aerealさんによるTheSchwartzの失敗したjobとかerrorがいつどのように消えていくのか - Sexually Knowingでした.

今日ははてなでもよく使われているSmart::Argsのパーサを書いた話をしようと思います.

俺たちのPerl 5

はてなではPerl 5がしばしばプロダクトの開発に使われています.そしてPerlで動的な引数型チェックをできるようにするSmart::Argsというライブラリがあり,よく使われています.Smart::Argsを使うと,以下のような体裁で引数の型チェックを実行時に行うことができます.

package Dummy;
use Smart::Args qw(args);
# クラス定義略
sub foo {
  args my $class => 'ClassName',
          my $arg1 => 'Int',
          my $arg2 => { isa => 'HashRef', default => +{} };
}

このサブルーチンを呼び出すには,Dummy->foo(arg1 => 1, arg2 => +{ bar => "buzz" });というふうに,ハッシュの形で引数をとります. Common LispやRubyに存在するキーワード引数に似ていますね.

またハッシュではなく通常のサブルーチン同様に引数をとるためのargs_posもあります.これはサブルーチン定義で利用しているargsargs_posに変えることで,上述のサブルーチンをDummy->foo(1, { bar => "buzz" });というふうに呼ぶことができるようになります.

Smart::Argsの詳細については,CPANのページをごらんください.

ちなみにこれを改良したSmart::Args::TypeTinyid:akiymさんが作られています.

貧弱な開発環境

さて,便利な型チェックライブラリが存在する一方で,エディタやIDEがPerlのために提供する支援はあまり充実してはいません. 他の言語には用意されているような,自動的にメソッド名や引数を補完するといった機能を提供するプラグインはほぼありません.このような状況になっているのは,柔軟な文法が静的な解析を困難にしていることに遠因があるようです.この状況はSmart::Argsでも同様で,Smart::Argsを使って定義されたサブルーチンの引数を的確にサポートできる開発環境を,わたしは存じません.

まずはパーサから

そこで今回私はSmart::Argsを使って定義されたサブルーチンの引数をサポートできるようにするための橋頭堡として,まずはSmart::Argsの文法をパースするためのライブラリを作成しました.

github.com

roswellを使ってcommon lisp処理系をインストールした状態で,rosコマンドを使ってサンプルperl moduleファイルをパースしてみましょう.

# Roswellをセットアップする
# In OS X
$ sudo brew install roswell
$ ros setup
$ git clone git@github.com:windymelt/smart-args-parser.git
$ cd smart-args-parser
$ ros -S . roswell/parse-smart-args.ros tests/Test.pm

すると,tests/Test.pmをパースした結果が表示されます.

(("sub2" SMART-ARGS-PARSER::ARGS
  ((SMART-ARGS-PARSER::ONEARG (VARIABLE . "class")
    SMART-ARGS-PARSER::SIMPLEARGTYPE TYPE . "ClassName")
   (SMART-ARGS-PARSER::ONEARG (VARIABLE . "arg2")
    SMART-ARGS-PARSER::SIMPLEARGTYPE TYPE . "Int")
   (SMART-ARGS-PARSER::ONEARG (VARIABLE . "arg3")
    SMART-ARGS-PARSER::SIMPLEARGTYPE TYPE . "ArrayRef")))
 ("sub1" SMART-ARGS-PARSER::ARGS
  ((SMART-ARGS-PARSER::ONEARG (VARIABLE . "class")
    SMART-ARGS-PARSER::SIMPLEARGTYPE TYPE . "ClassName")
   (SMART-ARGS-PARSER::ONEARG (VARIABLE . "arg1")
    SMART-ARGS-PARSER::SIMPLEARGTYPE TYPE . "Str"))))

今のところ機能はこれだけですが,これからパッケージとサブルーチンとの紐付け・Language Server Protocolの作成などを行おうと思っています.

どんな仕組みでパースしているの

当初はSmart::Argsの構文を正規表現でパースしようと思っていましたが,コーディング途中で到底これではパースできないと思いPEG を用いたパースに置き換えました.argsargs_posの構文には,型の指定・デフォルト値の指示等の目的でHashRefが登場し,これがネストする可能性があるためです.何重にもネストする構文を正規表現でパースすることは難しいのです.

PEG

args/args_posの構文は,あらかじめ定義されたPEG文法ファイルをもとにパースされます.

github.com

Common Lispでは,ESRAPライブラリを使うことでpackrat parserを作成することができ,ESRAP-PEGライブラリを使うことで,PEG文法ファイルをもとに,ESRAPを使ってPEGパーサを組み立てることができます. このツールでもESRAP/ESRAP-PEGを使ってargs/args_posのパーサを作成し,またパース結果を評価して「変数」や「文字列」といった意味のある単位に再編するためのコードを実装して,最終的に上掲のS式を出力させています.

実装の詳細ですが,処理の流れは以下のようになっています.

  1. PEG文法ファイルからpackratパーサが構築される
  2. ヘルパーメソッドがファイルからサブルーチン部分を抜き出す(ここは正規表現で行う)
  3. 抜き出されたサブルーチンの冒頭部分(セミコロンが出現するまで)を順次packratパーサに渡していく
    1. トークナイズとある程度の構造化が行われる
  4. パースされた結果をさらに適切な形に変形させる
    1. 必要ないトークンを削除し,文字の連続を文字列に変換する,などを行う
  5. 変換結果を印字する

ここがきつかったです

PEGの文法に慣れていなかったため,うっかり無限に再帰させてしまったり,パースしにくい表現にしてしまったりしました.

ここがおもしろかったです

パースに失敗するとどこで失敗したかが表示されるので,自分の入力ミスだったことがすぐに分かって面白かったです.

(esrap:parse 'smart-args-parser::hashref "{ foo -> 1 }")
At

  { foo -> 1 }
        ^ (Line 1, Column 6, Position 6)

In context HASHPAIR:

While parsing COMMA. Expected:

     the character , (COMMA)

While parsing FATCOMMA. Expected:

     the string "=>"

While parsing peg-derived character class rule with clauses (32 9 10) . Problem:

  The production
  
    #\-
  
  does not satisfy the predicate SMART-ARGS-PARSER::|peg-derived semantic-checker for character class with clauses (32 9 10) |.

まとめ

今回は,Smart::ArgsをCommon Lispでパースしたお話をしました.

明日はid:taraoさんです!

Smart::Argsパーサを書いている

2018年も終わろうとしているし平成ももうすぐ終わるのだが,いまだに俺の開発環境(emacs)ではperlをうまく補完することができない.

とはいえdabbrevの類を使っているのでそこそこの補完は動いているのでそこはいいのだが,Smart::Argsの引数をよく間違えて時間をとられる,といった事がよくあった.

Emacs上に,これから呼ぼうとしているサブルーチンの引数をうまく表示してやれたらいいなと思い,ひとまず今はS::Argsを使ったサブルーチンをパースするコードを書いている.

言語

Common Lispを使っている.言語処理に使われた実績があるし,パーサも豊富だ.そもそも俺の好みだというのが一番大きそう.

今回はESRAP-PEGパッケージを使って,PEG文法ファイルからPEGパーサジェネレータを介してパーサを作成し,これに読み取らせようとしている. なにやら大袈裟かもしれないが敢えて正規表現を使わないのは,正規表現だと複雑になりすぎてメンテナンスできそうもないと思ったからだ.

コードのサンプルとして以下のサイトを参考にした.

owmya.com

PEG文法ファイル

こういった内容になっている.このファイルをESRAP-PEGに食わせると,ESRAPが理解可能なPEG文法のインスタンスが作成され,ESRAPがパーサを生成する. このファイルを使わない場合は,ESRAPに直にS式でPEG文法を指示する必要があり,ちょっと面倒.

alpha <- [a-zA-Z]
number <- [0-9]
colon <- ':'
semicolon <- ';'
comma <- ','
spaces <- [ \t\n]+
fatcomma <- '=>'
leftbracket <- '['
rightbracket <- ']'
leftbrace <- '{'
rightbrace <- '}'
dollar <- '$'
string <- (["] (!["] .)* ["]) / (['] (!['] .)* ['])

...

argsdefinition <- spaces? (argsheading / argsposheading) spaces? (onearg spaces? comma spaces?)* (onearg spaces?)? semicolon spaces?

argsheading <- 'args'
argsposheading <- 'args_pos'
my <- 'my'
onearg <- my spaces? variable spaces? fatcomma spaces? argtype
variable <- dollar (alnumsign)+
alnumsign <- alpha / number / underscore
underscore <- '_'
argtype <- simpleargtype / argtypeobject
simpleargtype <- '\'' type '\''
type <- (alpha / colon colon / leftbracket / rightbracket)+
argtypeobject <- leftbrace spaces? (argtypedefinition spaces? comma spaces?)* argtypedefinition spaces? rightbrace
argtypedefinition <- alpha+ spaces? fatcomma spaces? (string / number+)

パースするコード

まだ未完なので完動するコードはまだないが,完成したら動くものをどこかに公開したい.以下に示しているのは,ファイルのストリームからサブルーチンをとりだそうとしているコード.きたねーッ!!

(defun extract-subs-from-stream (s)
  (let ((sub nil)
        (lines nil)
        (sub-string-map nil))
    (flet ((after-sub-p () sub)
           (pack-lines ()
             (setf sub-string-map
                   (acons sub (format nil "~{~A~^
~}" (take-to-last-argument (reverse lines))) sub-string-map))
             (setf lines nil))
           (wipe-lines () (setf lines nil))
           (enter-sub (sub-now) (setf sub sub-now))
           (append-line (line) (setf lines (cons line lines))))
      (iter (initially line nil)
        (for line next (read-line s nil :eof nil))
        (when (eq line :eof)
          (when (after-sub-p)
            (pack-lines))
          (return-from extract-subs-from-stream sub-string-map))
        (let ((subname-or-nil (extract-sub-heading-line line)))
          (if subname-or-nil
              (progn
                (if (after-sub-p)
                    (pack-lines)
                    (wipe-lines))
                (enter-sub subname-or-nil))
              (append-line line)))))))

想い

lispで何か書いてもあまり注目されないんだよね・・・

まあそもそもパーサ書いてもそんな注目されなくて,サービスを作って公開するのがいいのかなって思うんだよね・・・

フォロワーのポートフォリオ見てビビっちゃったよ・・・俺25なのに特に実績とかなくてちょっとヘコんでるんだよね・・・

Scala風に穴開き関数リテラルが書けるライブラリ「cl-punch」のご紹介

こういうのを作りました.

github.com

このライブラリを使うと,(lambda (x y z) (foo x y z)) と書いていたところを ^(foo _ _ _) と書けるようになります.

ちなみに名前は穴が開いたパンチカードから来ています.任意の式に穴を開けられるのでそういう名前にしました.

動機

Common Lispは関数が一級市民なので,かなりlambda式を多用します.ソートの基準やちょっとした文法の調整だとかで,いったんlambda式に焼き直して使いやすい形に変換してしまう,ということがよくあります.

しかし頻出するわりには特別なフォームといったものも特に用意されておらず,素朴に(lambda (x y) ...)といった書き方をしていました.

ここでScalaの無名関数リテラルを見てみましょう.引数が入る位置に_を配することで,うまく無名関数を書いています.

_ + _ // (x) => (y) => x + y

これは部分適用の構文とか,プレースホルダという名前で呼ばれています.(正式には何と呼ぶのでしょう)

ScalaでできてCommon Lispでできないのもくやしいので*1,サクッと実装してみたところ案外よく動くので公開したというわけです.

変換ルール

とりたててルールというべきものもありませんが,以下の通りに式が変換されます.

^()

これでくくった式は最終的に(lambda)に変換されます.このカッコの中で使われているアンダースコアが引数として使われます.

_

アンダースコアです.配置した順に仮引数がつけられていきます.例えば^(* _ _ _)としたとき,(lambda (x y z) (* x y z))という順序で展開されます.展開するときの仮引数の名前は(gensym)で生成しているため,内部の式と衝突することはありません.

基本的にこれが全部です.

補助ルール

これだけだと実用上(同じ変数を2度使いたいときなどに)不便なので,いくつかのルールを追加しています.

<_

引数リストの末尾にある引数を再利用します.例えば^(* _ _ <_)(lambda (x y) (* x y y))と同様です. したがって最初に使うことができません.

このプレースホルダは引数を増やしません.

必要に応じて<<_(2つ遡る)や<<<_(3つ遡る)も実装できますが,今のところこれででいいんじゃないかという理由で<_だけ実装しています.

_!

通常であれば_は書かれた順に引数リストを構成する----つまり引数リストの最後に自分自身を挿入しますが,_!は引数リストの先頭に自分自身を挿入します.これは引数の順序を入れ替えたいときなどに便利です.まず_!を右から順に引数リストに追加し,その後で_を左から引数リストに追加する,という見え方になります.

(^(list _ _! _! _) 1 2 3 4)
;; => '(3 2 1 4)

これより複雑なルールが必要になったら,おとなしくlambdaと書いたほうが良さそうです.

また今後便利なルールが必要になったら,随時追加していこうと思います.こういうルールもあったほうがいいんじゃないの,といったご意見お待ちしております.

まとめ

拙作のCommon Lispライブラリcl-punchを紹介しました.今のところquicklispには登録していませんが,手順も難しくないようなので登録してみようと思います.

*1:Scalaも大好きな言語です

シグナルを使って木構造をインタラクティブに探索する

prologのバックトラックのように,木構造といった構造を探索して,条件にマッチしたらsignalを飛ばして,その値を採用するか・採用せずに次の値を探すか,という決定を上にやってもらう,ということをやりたい場合はどうしたらよいのだろう.

試しに,木構造を探索して,葉にぶつかったらコンディションをシグナリングして,それを採用するかしないかをデバッガに選んでほしいときのコードを考えてみた.

(ql:quickload :iterate)
(use-package :iterate)

(defstruct tr title children)

(define-condition found-leaf-condition ()
  ((leaf :reader leaf :initarg :leaf))
  (:report (lambda (c s) (format s "found leaf: ~A" (leaf c)))))

(define-condition leaf-accepted-condition () ((leaf :reader leaf :initarg :leaf)))

(defun first-leaf (tr)
  (handler-bind ((leaf-accepted-condition #'(lambda (c) (return-from first-leaf (leaf c)))) ;; 受諾された場合のコンディションハンドラ
                 (t #'(lambda (c) (invoke-debugger c)))) ;; 候補が見付かった場合のコンディションハンドラ
      (first-leaf% tr)))

(defun first-leaf% (tree)
  (if (null (tr-children tree))
    (restart-case (signal (make-condition 'found-leaf-condition :leaf tree))
      (next () nil)
      (accept () (signal (make-condition 'leaf-accepted-condition :leaf tree)))))
    (iterate:iter (for child-tree in (tr-children tree))
      (first-leaf% child-tree)))

(first-leaf
 (make-tr
  :title "root"
  :children
  (list (make-tr
         :title "a"
         :children
         (list (make-tr
                :title "b"
                :children nil)
               (make-tr
                :title "c"
                :children (list (make-tr
                                 :title "ca"
                                 :children nil)))
               (make-tr
                :title "d"
                :children nil))))))

ここでは2つのコンディションが使われている.条件に合致する値(葉)を発見したときにこれを伝えるためのfound-leaf-conditionと,デバッガが値を受諾したときに,その値を返り値として関数から脱出させるためのleaf-accepted-conditionだ.こういうふうに二段構えにシグナルを活用することで,条件に合致する値の発見と受諾をインタラクティブに行うことができた.

より一般化すると,木構造以外にも適用できて面白いかもしれない.

Common Lispでリードマクロしてみた

common lispではマクロによって式を変形することでプログラムを柔軟に書くことができる.今回はリードマクロを使って,便利なデバッグプリントを実装したい.

柔軟なマクロ,それがリードマクロ

一般的なマクロは通常の関数呼び出しと同じように(マクロ名 引数 ...)といった形をとるが,より柔軟に式を表現したいときもあろう.リードマクロというものを使うと,ある特定の文字をトリガーとして,文字列を扱うように自由に式を書き換えることができる.

  • 一般的なマクロ
    • S式を受け取ってS式を生成する
  • リードマクロ
    • 入力ストリームを好きなだけ消費しながらS式を生成する

以下のコードを見てほしい.

#?x
; => (let ((a x)) (fresh-line *error-output*) (format *error-output* "DEBUG: ~S~%" a) a)

今回はこのような式変形を行いたい.formatによってデバッグプリントを行いつつ,式自体はその値を返すといったものだ. またここで敢えてxaに代入するのは,xが二度評価されないようにするためである.マクロを作るときは,評価されていない生のシンボルを触っていることに注意する必要がある.

定義方法が2種類あります

Common Lispでリードマクロを定義するには,set-macro-characterset-dispatch-macro-characterのどちらかを選ぶことができる.前者は特定の1文字をトリガーとすることができ,後者は特定の2文字(トリガー文字1つと,数字もしくは数字でない文字1つ)をトリガーにすることができる.

  • set-macro-character
    • リードマクロを定義する
    • 1文字がトリガー
    • 例: 'x => (quote x)
    • 2引数の関数を割り当てる
      • stream, 割り当て文字が渡される
  • set-dispatch-macro-character
    • ディスパッチング・リードマクロを定義する
    • 特殊な形式
      • 文字2つと中置パラメータ(数値のみ,オプショナル)1つをとる形式にできる
    • 3引数の関数を割り当てる
      • stream, 割り当て文字2, 中置パラメータ(ない場合はNIL)が渡される
    • 例: #P"foo" => (pathname "foo").ここで1文字目は#\#,2文字目は#\P,中置パラメータはNIL
    • 例2: #2A() => rank=2の配列.ここで1文字目は#\#,2文字目は#\A,中置パラメータは2.

今回は#?をトリガーにしたいので,2文字使うことができるset-dispatch-macro-characterを選ぼう.1文字目は#であり,2文字目は?である.中置パラメータは使わない. また,ここから先はディスパッチング・リードマクロを含む概念である広義のリードマクロと,ディスパッチング・リードマクロでない方の,という意味の,狭義のリードマクロという言葉を区別せずに使う. あまり混乱しないように書いているつもりだが,難しかったら教えてほしい.

リードマクロの動作を理解するためにも,実際にリードマクロを割り当てる前に,まずはリードマクロの割り当て先となる関数を定義しよう.

難しい引数の説明は以下の通りだが,まずは次節以降でひととおりリードマクロを作成してみると意味がわかるようになるだろう.

  • set-macro-characterでリードマクロを定義するとき
    • (lambda (stream trigger-char) ...) の形式にする
    • 第一引数には,マクロがトリガされた地点より先のソースコードが格納されたstreamが渡される
    • 第二引数には,マクロをトリガした文字が渡される
  • set-dispatch-macro-characterでディスパッチング・リードマクロを定義するとき
    • (lambda (stream trigger-char-2 n) ...) の形式にする
    • 第一引数には,set-macro-character同様にstreamが渡される
    • 第二引数として,マクロをトリガした2文字目が渡される
    • 第三引数として,中置パラメータが渡される.中置パラメータがないときは,NILが渡される.

普通の関数をマクロの本体に仕上げていこう

リードマクロの定義といっても怖いことはない.

まずは標準入力を読み込んで,そこからなんらかのS式を生成するような関数を考えてほしい.標準入力のかわりに文字列の入力ストリーム(input string stream)を使うように書き換えれば,それがそのままリードマクロとして動くようにできる.

まずは何も考えず,標準入力から式を読み込んで,意図した式に変形してくれる関数を考えてみよう.

(defun debug-print ()
  (let ((expression (read)))
    (fresh-line *error-output*)
    (format *error-output* "DEBUG: ~S~%" expression)
    expression))

実際に実行するとこういう感じだ.

* (debug-print)
12345 ; 入力
DEBUG: 12345 ; エラー出力

12345 ; 返り値

うまく動いている.これから,この関数をディスパッチング・リードマクロに適した形に変形していこう.

streamをとるようにする

リードマクロは標準入力のかわりにstreamを受け取るので,任意のstreamで処理可能にしよう.ここでやるべきことは,関数が任意のstreamを受け取るようにし,readの引数を変更することだ.

(defun debug-print (stream)
  (let ((expression (read stream t nil t))) ; error-on-eof: t, eof-value: nil, recursive-p: t
    (fresh-line *error-output*)
    (format *error-output* "DEBUG: ~S~%" expression)
    expression))

今やこの関数は任意のstreamで動くようになった.いくつかreadの引数が増えているが,これは

  • eofに到達したらエラーにする
  • eofに到達した場合の値はnilにする(その前にエラーになるが)
  • 再帰的なreadであることを表明する

という意味.

トリガー文字をとるようにする

リードマクロにはトリガーとなる文字が必要だが,リードマクロはそれ自体がどの文字をトリガーとして呼び出されたか,引数を通じて知ることができる. このため複雑な挙動をするリードマクロを考えたりできるが,今回はトリガーが1通りしかないので単に無視する. リードマクロの要求に対応するため,関数の引数を増やそう.

(defun debug-print (stream char2 n) ; 3引数に変化
  (declare (ignorable char2 n)) ; 使わないので無視するようにする
  (let ((expression (read stream t nil t))) ; error-on-eof: t, eof-value: nil, recursive-p: t
    (fresh-line *error-output*)
    (format *error-output* "DEBUG: ~S~%" expression)
    expression))

基本骨格はもうここで完成した.あとは,関数を呼び出したときに実際に動作するのではなく,動作するべきS式を返すようにすればよい.

S式を返すようにする

S式を返すという動作自体は一般的なマクロと同じなので,一般的なマクロの知識をここで使うことができる.ここでは,

  • 読み取ったS式が二度評価されないように,評価結果を一時変数に束縛するような式を生成する
    • リードマクロはコンパイル前に動作する.リードマクロがreadしたS式はまだ評価されていない状態にある
    • リードマクロがreadしたS式をそのまま変換結果のS式に複数個埋め込むと,複数回評価される
    • 副作用が生じる関数だった場合危険!
  • リードマクロがreadした式の内部と,一時変数のシンボル名が干渉しないように,安全なシンボルを動的に生成する
    • gensym関数を利用して,衝突しないことが保証されたシンボルを生成する

というテクニックを使っている.最終的にどのようなS式が欲しいかをイメージしながらマクロを書くとよい.

(defun debug-print (stream char2 n)
  (declare (ignorable char2 n))
  (let ((expression (read stream t nil t)) ; 入力から式を1つ読み取る.1や(quote 1 2 3),(f x y)などが入っている(評価すると副作用を生じるかもしれない!)
         (expression-evaluated (gensym))) ; 変数名は衝突の危険があるので,gensym関数で安全に生成する.
     ;; ここから生成されるS式の記述が始まる.quasiquote(`)を使ってテンプレートライクに記述する.
    `(let ((,expression-evaluated ,expression)) ; expressionを何度も使うとその都度評価されるような式になってしまい危険なので,いちど内部で一時変数に束縛する.
      (fresh-line *error-output*)
      (format *error-output* "DEBUG: ~S~%" ,expression-evaluated) ; 一時変数をデバッグ出力する.
      ,expression-evaluated))) ; 一時変数を返す.

これが呼び出されると,以下のようなS式が構築される.

* (debug-print *standard-input* nil nil)
(f x) ; 標準入力

(LET ((#:G444 (F X)))
  (FRESH-LINE *ERROR-OUTPUT*)
  (FORMAT *ERROR-OUTPUT* "DEBUG: ~S~%" #:G444)
  #:G444)

生成されたコードは,

  • まず(f x)を評価して#:G444というシンボルに束縛する
  • *error-output*の行を改める
  • エラー出力に#:G444の値を出力する
  • #:G444を評価した値を返却する

といったものになっていることがわかる.これにより(f x)は1度だけ評価され,マクロを適用した式本来の動作を妨げない. もし一時変数への束縛を怠ると,以下のような式を使わなければならない.

(PROGN
  (FRESH-LINE *ERROR-OUTPUT*)
  (FORMAT *ERROR-OUTPUT* "DEBUG: ~S~%" (F X)) ; ここでいちど評価される
  (F X)) ; また評価される

このようなコードは,リードマクロが読み込んだS式が数値リテラルや純粋な関数なら問題ないが,副作用のある関数でこのようなことが発生すると不都合だし,純粋な関数でも計算コストが単純に二倍になって嬉しくないということがわかるはずだ.

ディスパッチング・リードマクロとして定義する

さて中身が完成したところで,実際にリードマクロとして使えるように定義しよう. set-dispatch-macro-characterは以下のようにして使う.

(set-dispatch-macro-character
  #\# ; 第一のトリガー文字
  #\? ; 第二のトリガー文字
  #'debug-print) ; 割り当てる関数

こうすると,トリガー文字# ?を読み取ったさいに#'debug-printを実行した結果で読み替えるべし,といったことが処理系のリードテーブルreadtableに格納される.

処理系は式を読み取る(パースする)さいにこのリードテーブルを利用している.この動作をカスタムしようというのがリードマクロが果たす機能である.

処理系のリードテーブルはスペシャル変数*readtable*に格納されている.これがいじられることで読み取り動作が変化するという仕掛けだ.

最終的な形を以下に示そう.

; これが評価されると,以降のコードは#?記法が有効化される.
(defun %enable-debug-print ()
  (setf *readtable* (copy-readtable))
  (set-dispatch-macro-character #\# #\? #'debug-print))

; (enable-debug-print)をコードに書いておくと,コンパイル時に#?記法が有効化される.
; いきなりリードマクロを有効化するのは行儀が悪い.
(defmacro enable-debug-print ()
  '(eval-when (:compile-toplevel :load-toplevel :execute)
    (%enable-debug-print)))

この形は id:m2ym さんの

github.com

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

なんで*read-table*を複製するのか

http://clhs.lisp.se/Body/f_cp_rdt.htm CLHSによれば,

(setq *readtable* (copy-readtable)) replaces the current readtable with a copy of itself. This is useful if you want to save a copy of a readtable for later use, protected from alteration in the meantime.

ちょっとよくわからなかったので,ここは宿題にしたい.

完成

(defun debug-print (stream char2 n)
  (declare (ignorable char2 n))
  (let ((expression (read stream t nil t))
         (expression-evaluated (gensym)))
    `(let ((,expression-evaluated ,expression))
      (fresh-line *error-output*)
      (format *error-output* "DEBUG: ~S~%" ,expression-evaluated)
      ,expression-evaluated)))

(defun %enable-debug-print ()
  (setf *readtable* (copy-readtable))
  (set-dispatch-macro-character #\# #\? #'debug-print))

(defmacro enable-debug-print ()
  '(eval-when (:compile-toplevel :load-toplevel :execute)
    (%enable-debug-print)))

これを使ったサンプルコードを以下に示す.

(defun factorial (n)
  (if (= #?n 0)
      1
      (* n (factorial (- n 1))) ) )

実行すると以下のようにデバッグメッセージを出力する.

* (factorial 10)
DEBUG: 10
DEBUG: 9
DEBUG: 8
DEBUG: 7
DEBUG: 6
DEBUG: 5
DEBUG: 4
DEBUG: 3
DEBUG: 2
DEBUG: 1
DEBUG: 0

3628800

ここから,デバッグプリント対象の式そのものも表示してあげる,などの改良ができそうだ.

(defun debug-print (stream char1 char2)
  (declare (ignorable char1 char2))
  (let ((expression (read stream t nil t))
         (expression-evaluated (gensym)))
    `(let ((,expression-evaluated ,expression))
      (fresh-line *error-output*)
      (format *error-output* "DEBUG: ~S => ~S~%" ',expression ,expression-evaluated)
      ,expression-evaluated)))

追記(2018/11/13)

set-dispatch-macro-characterについて誤解している旨の指摘があったため,修正します.

set-dispatch-macro-characterは3引数の関数を取りますが,その関数には

  • stream
  • 2文字目のトリガ文字
  • 中置パラメータもしくはnil

が渡されるのであって,「streamと1文字目と2文字目が渡される」という説明はまちがっていました.このため記述を修正しました.

Dockerでqlotを使うときに必要なもの

qlotはライブラリを管理してくれる便利なやつだが,dockerで使うとなると忘れがちな点がいくつかあり,よくハマるのでメモしておく.

qlfile

まあこれは忘れないだろう.これがないとqlot installできないので,COPYする必要がある.

# Dockerfile
COPY qlfile /app/qlfile

.asdファイル

これ忘れがちなのでちゃんとCOPYすること!!!!!これがないと,いくらqlfileに依存性が定義されていても,qlotはsystemをインストールしない.

# Dockerfile
COPY foobar.asd /app/foobar.asd
COPY foobar-test.asd /app/foobar-test.asd

まとめ

ふだんdocker-composeをつかっていると,必要なものはvolume-mountすればいいでしょ,といった具合に,アプリケーションディレクトリぜんぶを/app/とかにマウントする脳筋解決法をとりがちだが,コンテナビルドに必要なものは当然ながらDockerfileの中でコピーしなければならない.

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?