八発白中

はてなブログに引越しました。

Shibuya.lisp TT #8 で「Redesigning Common Lisp」という発表をしました

先日8/30(土)、Shibuya.lisp Tech Talk #8 が開催されました。

イベント概要

イベントではTechnical Talkが5つ、Lightning Talkが9つありました。

参加者は全部で57名。よく使うLisp方言のアンケートを取ったら、だいたいCommon LispSchemeClojureが1/3ずつくらいばらけていました印象です。コミュニティ内に多様性があるのは非常に良いですね。

各発表も話題が多様でした。

とても内容の濃い一日だったと思います。

途中@nobkzさんの発表で、「弊社、Lisperを募集しています!」という求人募集もありました。いいですね、こういうの。

発表しました

僕もTechnical Talkで「Redesigning Common Lisp」と題して40分程度の発表を行いました。

新しくLispエイリアン色のプレゼンテーマを作って使ってみました。

内容は僕が今年始めたプロジェクトの一つの「CL21」についてです。

質問や意見も多くいただきました。ありがとうございました。

話さなかったこと: CL21のこれから

時間の関係と少し話がそれてしまうという理由で削ってしまったことについて補足します。

CL21はこれからCommon Lispの再整理を行い、より小さな言語カーネルを目指す予定です。

f:id:nitro_idiot:20140901113406p:plain

Richard GabrielのWorse Is Betterという記事に、言語を4層に分離するという方法が提案されています。

言語は少なくとも4つの層に分割されるべきだ:

  1. 言語カーネル。この層は単純で実装しやすいものとなる。どんなケースにおいても、動的な再定義は再考を加えて、このレベルでのサポートが必要かどうかを判断するべきだ。私は再定義可能なものがカーネル内に必要だとは思わない。
  2. 言語を肉付けする言語学的な層。この層は実装上の困難が若干伴うかもしれない。そしてここには多分、カーネルで実装するには高価すぎるが割愛するには重要すぎる動的な側面が含まれることになる。
  3. ライブラリCommon Lispにあるものの大半はこの層に置かれる。
  4. 環境として提供されるエピ言語的機能

1番目の層に私は、条件式、関数呼び出し、すべての基礎データ構造、マクロ、単値、非常に基礎的なオブジェクト指向のサポートを含める。

2番目の層に私は、多値とより進んだオブジェクト指向サポートを含める。2番目の層は、環境が提供するに任せるにはあまりに重要すぎる、難易度が高いプログラミング上の構造、しかしそれでいて正確な定義を正当化するだけの意味論的な重要性が十分にある、そういうもののためにある。何らかの形の再定義機能はここに置かれるかもしれない。

3番目の層に私は、シーケンス関数、手の込んだ入出力関数、その他1番目の層と2番目の層に単純に実装できなかったものすべてを含める。これらの関数はリンク可能であるべきだ。

4番目の層に私は、環境が提供でき、またそうすべきであるが、標準化されなければならない機能を含める。典型的な例はCLOSのdefmethodだ。CLOSでは、総称関数はメソッドから成っており、各メソッドは特定のクラスに適用可能である。1番目の層には完全な総称関数のための定義式――つまり総称関数およびその全メソッドの定義が一箇所に集まったもの――がある(第1層のコンパイラがそれらをどう見たいかを反映している)。名前を総称関数に結び付ける手段も用意される。しかしながら、システムを発展させていく途上で、クラスはさまざまな場所で定義され、関連した(適用可能な)メソッドがクラスの隣に見えたほうが道理にかなっている。メソッドを作るその仕組みはdefmethodで、defmethod式は他の定義式の中のどこにでも置ける。

この方法には2つのメリットがあります。

  • 何をどこのレイヤーに入れるべきか、または入れないべきかの議論の基準が明確になる
  • CL処理系の実装が楽になる

CL21における議論で、「この機能を追加してよ」というものはかなり多いですが、それがどの層に属するのか、同じレイヤーの他の部品と比較して汎用性は十分にあるか、などより具体的に話し合うことができます。

もう一つのメリットである処理系の実装を簡単にするのは、実行環境が(再び)多様化しつつある昨今で、実行環境が生き残っていくために必要かなと考えています。

たとえばJSCLというJavaScriptで実装されたCL処理系がありますが、JavaScriptという独特の難しさを抱えており、CLの仕様をすべて満たしきれないでいます。この視点での言語の4層分離は必要そうです。

CL21の現状

こういう話を踏まえて、CL21の現在のステータスを話すと、「約束できることは何もありません」。

まだ議論も続いていて、何もFixできる状態でなく、早々の安定化を目指すよりも本来の目的である「Experiment (実験)」することの意義を優先しています。

何か良いアイデアをお持ちの方はGitHub Issuesでの議論に参加してください。

まとめ

Shibuya.lisp Tech Talk楽しかったですね。運営スタッフの方々お疲れ様でした。会場を貸していただいた株式会社mixiの鈴木さん、ありがとうございました。

また、Shibuya.lispは月一のMeetUpイベントも開催しています。

次回は来月下旬に開催予定なので、興味がある方はぜひ参加を検討ください。

プロジェクトのCommon Lispライブラリ管理ツール「qlot」を作りました

想像してください。

Common Lispで開発しているアプリケーションが手元にありますよね。

それを他の環境、たとえば他の開発者のPC、CI環境やWebサーバなどで動かしたいというときに起こる問題はなんですか。

大きな問題は処理系やライブラリのバージョンが違うために、自分の環境では動くのに他の環境に持っていくと動かないということです。

処理系に関してはCIMを使えば固定できますが、Common Lispにはライブラリのバージョンを固定する方法は現状ほとんど無いために問題になります。

こういった、Common Lispプロジェクトの可搬性を上げるツールとして「qlot」を作りました。

Quicklispの何が問題か

Common LispにはQuicklispという偉大なライブラリインスーラ及びライブラリの中央リポジトリがあります。依存ライブラリも含めてインストール・ロードしてくれるので、Common Lispでの開発が非常に楽になりました。

けれど、Quicklispにはいくつか問題があります。

  • 更新が月一しかない
  • 特定のライブラリのみ違うバージョンを使うことができない

更新が月に一回しかないので、GitHubに上がっている最新のClackを使いたいみたいなことができません。

また、Quicklispはライブラリを毎月のアップデートごとに管理しているので、すべてのライブラリを同じ時期のものを使うしか選択肢がありません。つまりlog4clだけ古いバージョンを使うということもできません。

"local-projects/"やASDFの設定をいじればgit cloneしたライブラリを使うことはできますが、git cloneしたことを忘れてて更新があったあとも古いバージョンをロードしてしまうみたいなことないですか。僕はつい最近もnamed-readtablesでやらかしました。

とにかく、Quicklispは使うライブラリを細かく管理するという目的ではまったく使えないのです。

qlotとは

qlotは、Common Lispライブラリをプロジェクトローカルにインストール・ロードすることができるツールです。

(正確にはプロジェクトローカルにQuicklispをインストールします。)

使い方を見せたほうが早いのでとっとと使い方を紹介します。

使い方

qlfileを置く

プロジェクトで使うライブラリをqlfileに記述します。すべてを記述する必要は無く、書かれていないものであれば最新のQuicklispのdistからロードされます。

たとえばGitHubにある最新のClackとdataflyを使いたい場合は以下のようなqlfileを用意します。

git clack https://github.com/fukamachi/clack.git
git datafly https://github.com/fukamachi/datafly.git

詳しいqlfileの書き方はGitHubのREADMEを参照してください。

単純にquicklispをプロジェクトごとに分離したい場合は空のqlfileを用意してください。

ライブラリをインストールする

qlotを通常通りにql:quickloadでロードします。

(ql:quickload :qlot)

それからqlot:installをプロジェクト名と一緒に呼び出します。

(qlot:install <プロジェクト名>)
(qlot:install :myapp)

:myappであれば(qlot:install :myapp)みたいな感じです。

インストールはこれだけです。プロジェクトルートにquicklisp/ディレクトリとqlfile.lockファイルができているのが分かると思います。

qlfile.lockはインストールした内容をスナップショットとして記録したものです。このファイルがあるとqlot:installはlockを優先します。一度こうしてインストールしておけば、他の環境でqlot:installしたときにも全く同じバージョンのライブラリを使うことが保証されます。

quicklisp/VCSから無視されるようにし、qlfileqlfile.lockを追加しておきます。

$ echo quicklisp/ >> .gitignore
$ git add qlfile qlfile.lock
$ git commit -m 'Start using qlot.'

プロジェクトをロードする

プロジェクトをロードするときはql:quickloadの代わりにqlot:quickloadを実行します。

(qlot:quickload :myapp)

これはプロジェクトローカルのQuicklispを使ってロードする関数です。

ライブラリをアップデートする

一度qlot:installしたあとqlfileを更新したり、ライブラリのバージョンを更新したいときなどはqlot:updateを実行します。

(qlot:update :myapp)

これでquicklisp/の内容とqlfile.lockが更新されます。

デプロイするとき

qlfile.lockさえ環境間で共有しておけば、qlot:installを実行するだけで同じQuicklisp環境が再現されます。

(qlot:install :myapp)

まとめ

使う関数はqlot:installqlot:quickloadqlot:updateの3つだけです。簡単でしょ?

今月のQuicklispアップデートで入る予定なので、試したい方は1、2週間くらい待つと使えるようになると思います。

ソースコードは例によってGitHubに上がっています。Starしてね!

余談: Lispイベント

今月は中旬にモントリオールでInternational Lisp Conferenceがあったり、月末にShibuya.lispのTech Talkがあったりでなかなか良い月になりそうですね。

Shibuya.lispテクニカル・トーク #8
日時: 8/30(土) 11:00 〜 18:30
場所: 株式会社ミクシィ

トークの応募は8月16日(土)までなので、何か話したい人は早めに準備して応募すると良いと思います。

僕も、qlotについてではないですが他のプロジェクトについて話すつもりでいます。みんな参加しましょう。

軽量なCommon LispのDBライブラリ「datafly」を作りました

Common Lispのデータベースライブラリというか、O/Rマッパーとしては3ヶ月前に僕が作ったIntegralがあります。

IntegralはCLOSやMOPなどのCommon Lispの魔術を余すこと無く使い、拡張性や高度なマイグレーション機能もあるライブラリとして他の追随を許しません。

ただ、すべてのアプリケーションでO/Rマッパーのような機能が必要なわけではないでしょう。抽象化レイヤーを薄く保って、極力コントローラブルにしたいという要望もあります。

今回紹介する「datafly」はそういった要求を満たす軽量なDBライブラリです。

dataflyの思想

一般的なO/Rマッパーでは、データベースの「テーブル」と、プログラム言語の「クラス定義」が一対一対応しています。この大きな前提のおかげでデータベースを抽象化でき、まるでクラス定義が(半)永続化しているように錯覚させてくれます。

ただし、そこにはトレードオフがあります。

O/Rマッパーはその性質上データベースやSQL発行を表向き見えなくするものなので、コストのかかるSQL発行が行われているときに気づきづらくなります。

その点、dataflyは逆の思想に基づいています。

dataflyでは暗黙のSQLの発行を行いません。マクロを除く黒魔術は使いません。透明性を重視し、アプリケーションごとの最適化を行いやすくコントローラブルな状態に保ちます。

機能

上述の通り、dataflyはO/Rマッパーではありません。たとえば、dataflyは以下のようなことはしません。

dataflyがやるのはこんなことです。

  • DBコネクション管理
  • CL-DBIをラップして扱いやすく
  • 結果を構造体(Structure)にマッピング
  • inflate

CLOSの標準クラスではなく構造体を使うのでいくらか効率も良いはずです。

クイックスタート

構造体(Structure)へのマッピング

dataflyではSQLの発行方法としてretrieve-oneretrieve-allexecuteの3つの関数があります。すべて引数としてSxQLのクエリオブジェクトを受け取ります。

たとえば、SELECT文を投げて、結果を1つ返して欲しいときはretrieve-oneを使います。

(retrieve-one
  (select :*
    (from :user)
    (where (:= :name "nitro_idiot"))))
;=> (:ID 1 :NAME "nitro_idiot" :EMAIL "nitro_idiot@example.com" :REGISTERED-AT "2014-04-14T19:20:13")

返り値はプロパティリストです。

キーワード引数の:asを指定すると、結果を指定した構造体(Structure)として返します。

(defstruct user
  id
  name
  email
  registered-at)

(retrieve-one
  (select :*
    (from :user)
    (where (:= :name "nitro_idiot")))
  :as 'user)
;=> #S(USER :ID 1 :NAME "nitro_idiot" :EMAIL "nitro_idiot@example.com" :REGISTERED-AT "2014-04-14T19:20:13")

(user-name *)
;=> "nitro_idiot"

この例ではテーブル名と構造体の名前が同じですが、同じである必要は全くありません。dataflyはテーブルとクラスが一対一対応ではないからです。

この自由さは、架空のテーブル――たとえばJOINした結果――などを構造体として扱いたいときなんかに便利です。

;; "user_bank"という名前のテーブルは存在しない。
(defstruct user-bank
  user-id
  name
  bank-balance)

(retrieve-one
  (select (:user_id
           :name
           (:as (:sum (:amount))
                :bank_balance))
    (from :user)
    (left-join :bank_transactions :on (:= :user.id :bank_transactions.user_id))
    (where (:= :name "nitro_idiot")))
  :as 'user-bank)
;=> #S(USER-BANK :USER-ID 1 :NAME "nitro_idiot" :BANK-BALANCE 200000)

いずれの例でも、結果として返ってきた構造体オブジェクトにsetfで変更を加えてもO/Rマッパーのようにデータベースに更新処理が行えるわけではありません。あくまでデータベースから構造体への一方向のマッピングだけを行います。

モデル定義としての構造体

少しずつ複雑な例を紹介していきます。

上の例では単なるCommon Lispの構造体を使いました。

これだけで十分な方も多いかもしれませんが、dataflyでは少し変わった構造体を定義する機能もあります。

使い方は簡単です。defstructの代わりにdefmodelというマクロを使います。

(defmodel user
  id
  name
  email
  registered-at)

アノテーションライブラリのcl-annotを使うと@modelと書くこともできます。

(annot:enable-annot-syntax)

@model
(defstruct user
  id
  name
  email
  registered-at)

以下では@modelを使うものとします。

inflate

@modelをつけると構造体定義にいくつかの特殊なオプションをつけることができます。

その一つが:inflateです。

@model
(defstruct (user (:inflate registered-at #'datetime-to-timestamp))
  id
  name
  email
  registered-at)

(:inflate <カラム> <関数>)を記述すると、オブジェクトを作るときに指定した<カラム>の値に<関数>を自動適用します。この例ではregistered-atというカラムをLOCAL-TIMEのTIMESTAMPオブジェクトに変換します。

(defvar *user*
  (retrieve-one
    (select :*
      (from :user)
      (where (:= :name "nitro_idiot")))
    :as 'user))

;; Returns a local-time:timestamp.
(user-registered-at *user*)
;=> @2014-04-15T04:20:13.000000+09:00

:inflateは複数つけることもできます。また、<カラム>の部分をリストにして複数のカラムを指定することもできます。

オブジェクトからデータベースにINSERT/UPDATE/DELETE文を発行する機能は無いので、反対の:deflateはありません。

:has-a と :has-many

他に指定できるオプションとして:has-a:has-manyがあります。これらはテーブルのカラムの関係性を定義することで、構造体にアクセサを追加する機能です。

@model
(defstruct (user (:inflate registered-at #'datetime-to-timestamp)
                 (:has-a config (select :* (from :config) (where (:= :user_id id))))
                 (:has-many (tweets tweet)
                  (select :*
                    (from :tweet)
                    (where (:= :user_id id))
                    (order-by (:desc :created_at)))))
  id
  name
  email
  registered-at)

(defstruct config
  id
  user-id
  timezone
  country
  language)

(defstruct tweet
  id
  user-id
  body
  created-at)

この例ではuser-configuser-tweetsというアクセサが自動で定義されます。

(defvar *user*
  (retrieve-one
    (select :*
      (from :user)
      (where (:= :name "nitro_idiot")))
    :as 'user))

(user-config *user*)
;=> #S(CONFIG :ID 4 :USER-ID 1 :TIMEZONE "JST" :COUNTRY "jp" :LANGUAGE "ja")

(user-tweets *user*)
;=> (#S(TWEET :ID 2 :USER-ID 1 :BODY "Is it working?" :CREATED-AT @2014-04-16T11:02:31.000000+09:00)
;    #S(TWEET :ID 1 :USER-ID 1 :BODY "Hi." :CREATED-AT @2014-04-15T18:58:20.000000+09:00))

:has-a:has-manyで定義されたアクセサを呼び出すとSELECT文が発行されることに注意してください。

結果は初回でキャッシュされるので、二度以上呼び出しても何回もクエリが発行されるわけではないので安心してください。キャッシュを消すにはclear-object-cachesが使えます。

おわりに

Integralと違ってブログポスト一つでほとんどの機能が紹介できてしまった。JSON吐くだけのWeb APIサーバとかならこの程度で十分ですね。

今回作ったdataflyはGitHubで公開しています。

また、来週の火曜の夜は渋谷でLisp Meetupがあります。興味がある方はどうぞご参加ください。

Lisp Meet Up presented by Shibuya.lisp #16 : ATND
日時: 4/22(火) 19:30 〜 21:30
場所: 渋谷マークシティ ウエスト13階 セミナールーム

参考

株式会社はてなを退職しました

二月末日で株式会社はてなを退職しました。二年半の間、大変お世話になりました。

理由。はてなで働き続けて得られる以上のことをしようと思ったから。

この一年くらい、僕は今の自分に何の価値も感じられず、今の自分に何の満足もできていない。それなのに、気を抜いたら現状に甘えて、一年後の自分が想像できる範囲の成長しかできなくなってる。

年末に一年間を振り返るとき「驚くべき進歩だ」と思えなかったら、きっと努力が足りてないんです。そして、一年後の自分が予想できるなら、今歩いている道は間違ってるんだと思う。

そんなことを考えつつ、ちょうど携わったサービスも終了したということもあって、居心地の良いはてなと大好きな京都を離れることにしました。

特に今後について現状で言えることは何もないですし、振り返るほどの立派な功績もないので、よくある退職エントリみたいにかっこいい文章は書けないですが、ご報告として。

こちらからは以上です。

誰向けかわからないCommon Lispでの関数型プログラミング入門とその未来

Lispと言えば関数型言語という印象を持つ人が多いようです。

まあ正直に言うと、Common Lispに関して言えば違うんですけどね。Common Lispは効率のためと言えばループも代入も使いまくるし、構造体もクラスもある。実際書かれたコードも関数型プログラミングとは程遠いことも多くて、たとえば僕が作ったClackのコードを見ればオブジェクト指向言語だって言っても信じると思います。

僕自身、繰り返しをわざわざ再帰で書くよりもloop使うことが多いです。最近loopに頼りすぎてて良くないなーと思うことが多く、Common Lispでも性能が重要でないところは関数型っぽく書く癖をつけないとなー、と思っていろいろ考えています。なんでもloopだと可読性が悪い。

特に、僕が今作っているCommon Lisp方言の「CL21」では関数プログラミングをもっとしやすくする機能を入れたいと思っています。

そういうわけで、最近はCommon Lispでの関数型プログラミングの方法について調べてCL21に取り込むことをしています。

だいたいまとまったので、Common LispとCL21で関数型プログラミングをするチュートリアルみたいなものを午前四時のローテンションで書いてみました。

以下の2章立てです。

今読み返してみると、しれっとScheme知ってる前提だったりして、これ誰向けだよ、みたいな感じですが、まあご容赦ください。というかCL21のほうが本題だったりするので入門っぽい話を読みたくなかったら2章までスクロールしてください。

Common Lispでの関数型プログラミングの現状

関数

Common Lispで関数を定義するにはdefunを使います。

(defun 関数名 (パラメータ*)
  "ドキュメント文字列 (任意)"
  本体*)

例えば、Schemeで良く使われるiotaは以下のように定義できます。iotastartから始まるn個のリストを返します。

(defun iota (n &optional (start 0))
  (if (zerop n)
      nil
      (cons start (iota (- n 1) (1+ start)))))

(iota 10)
;=> (0 1 2 3 4 5 6 7 8 9)

(iota 5 3)
;=> (3 4 5 6 7)

無名関数

Common Lispで無名関数を作るにはlambdaを使います。

(lambda (n)
  (and (zerop n)
       (integerp n)))
;=> #<Anonymous Function #x302002ECCE3F>

無名関数を単に呼び出すときは、通常の関数の位置にlambdaフォームを書けばいいだけです。

((lambda (n)
   (and (zerop n)
        (integerp n)))
 3)
;=> NIL

((lambda (n)
   (and (zerop n)
        (integerp n)))
 0)
;=> T

((lambda (n)
   (and (zerop n)
        (integerp n)))
 0.0)
;=> NIL

高階関数

高階関数とは、関数を引数として受け取ったり、返り値として(無名)関数を返すような関数のことです。

例えば、多くの言語ではmapという関数がありますね。関数とリストを受け取り、リストのそれぞれの要素について関数を適用するようなものです。

Common Lispではこのような機能はmapcarと呼ばれています。

(mapcar #'1+ '(1 2 3 4 5))
;=> (2 3 4 5 6)

#'という記号は他の言語では特殊なので説明が必要ですね。

まずCommon Lispでは関数と変数の名前空間が分かれています (LISP-2という分類)。たとえば、Common Lispには関数listがありますが、同時に同じ名前のlistという変数を定義して使うこともできます。

このとき気をつけなければいけないのは、単にlistと書いたときに、それが変数なのか関数なのかを区別する必要があるということです。Common Lispで通常listを値として評価した場合、変数として扱われます。

(defvar list '(1 2 3))

list
;=> (1 2 3)

listを関数として扱いたいときは、#'をつけます。

#'list
;=> #<Compiled-function LIST #x3000000B4D6F>

上のmapcarでは1+という変数を渡しているのではなく、関数1+を渡したいので、#'が必要なのです。

また、reduceもよく使われる高階関数です。reduceはリストの先頭から渡された関数に適用し、さらにその返り値とリストの次の値を適用することを繰り返して結果を返す関数です。reduceは他の言語ではfoldとかinjectと呼ばれることもあります。

;; (+ (+ 1 2) 3) と同じ
(reduce #'+ '(1 2 3))
;=> 7

高階関数の使い道

上述した高階関数の便利なところは、その汎用性です。引数として渡す関数によってさまざまな用途に使えます。小さく汎用的なパーツを組み合わせて段々と大きくしていく手法はボトムアッププログラミングとして知られていますね。

たとえば、reduceを使うと以下のような関数が簡単に定義できます。

(defun sum (list)
  "数字のリストを受け取り、その合計値を返す"
  (reduce #'+ list))

(defun factorial (n)
  "数字を一つ受け取り、その階乗を返す。 n! = 1 * 2 * 3 * ... * n"
  (reduce #'* (iota n 1)))

sumは数字のリストを受け取り、その合計値を返します。factorialは数字を一つ受け取り、その階乗を返します。

mapcarではzipという関数が定義できます。

(defun zip (&rest lists)
  (apply #'mapcar #'list lists))

applyは一番最後の引数をリストとして扱って関数を呼び出す方法です。

zipは複数のリストを受け取り、その要素一つ一つをまとめあげる関数です。

(zip '(1 2 3) '(a b c) '(松 竹 梅))
;=> ((1 A 松) (2 B 竹) (3 C 梅))

関数合成 (compose)

さて、高階関数を使って組み合わせることで多くの関数を定義することを説明しました。この章ではさらに「関数合成」というテクニックを紹介します。

たとえば、関数gの返り値を関数fに渡したいとき、(f (g x)) のように書けばいいのはわかりますね。

この処理を高階関数に渡すには、無名関数を作るのが最もナイーブな方法です。

(lambda (x)
  (f (g x)))

しかし、組み合わせる関数がもっと多くなったときはどうでしょうか。

(lambda (x)
  (f (g (h (i (j (k x)))))))

長くなりますしだんだんと読みづらくなってしまいます。

このようなとき便利なのが「関数合成」です。ここでは関数composeを使います。関数composeCommon Lispの仕様に含まれないため、Alexandriaのようなユーティリティライブラリを使うか、もしくは以下のようにreduceで簡単に定義できます。

(defun compose (fn &rest functions)
  (reduce (lambda (f g)
            (lambda (&rest args)
              (funcall f (apply g args))))
          functions
          :initial-value fn))

composeを使うとさき先ほどの例は以下のようになります。

(compose #'f #'g)

関数を引数で渡すだけなので括弧も少なく済みますね。

例として、リストの各要素の sin(n + 1) をリストとして返す処理は以下のように書けます。

(mapcar (compose #'sin #'1+) '(1 2 3 4 5))
;=> (0.9092974 0.14112 -0.7568025 -0.9589243 -0.2794155)

mapcarを2回使っても同じ結果が出ますが、リストを2回走査する必要があるし、ループのたびにリストを新しく作るためメモリ消費面でも良いコードではありません。

;; 良くない
(mapcar #'sin
        (mapcar #'1+ '(1 2 3 4 5)))

conjoin & disjoin

関数合成の例として、他にもconjoindisjoinという関数もよく使われます。

これらは各関数の返り値の真偽によって関数を複数実行する機能です。

たとえば、「ゼロかつ整数である」という条件の処理は以下のように書けます。

(lambda (n)
  (and (zerop n)
       (integerp n)))

この処理もcomposeのときのように、適用する関数が多い場合に煩雑になってしまいます。

このような処理を関数合成で解決するのがconjoinです。

(import 'alexandria:conjoin)

(funcall (conjoin #'zerop #'integerp) 0)
;=> T

(funcall (conjoin #'zerop #'integerp) 0.0)
;=> NIL

conjoinandで関数を繋げた無名関数を返します。一方でdisjoinorで関数を繋げます。

たとえば(disjoin #'plusp #'minusp)はプラスかマイナスの数値なら真を返します。つまりゼロではないという条件になります。

(funcall (disjoin #'plusp #'minusp) 100)
;=> T

(funcall (disjoin #'plusp #'minusp) 0)
;=> NIL

ちなみに余談ですが、ゼロかどうかはzeropで判断できるので、その返り値を反転させて返すほうが賢い実装ですね。

(funcall (lambda (n) (not (zerop n))) 0)
;=> NIL

このようなnotを加えるだけの呼び出しもよく使われるので、単に返り値の真偽を反転させる関数を返す関数complementというものもあります。

(funcall (complement #'zerop) 0)
;=> NIL

CL21での関数型プログラミングの未来

さて、ここからが実は本題です。

Common Lisp関数型プログラミングの機能をひと通り紹介しました。Common Lispでも十分に関数型プログラミングができますね。

しかし、僕が作っている新しいCommon Lisp方言の「CL21」では、より関数型プログラミングをしやすいようにするつもりです。たとえば、Common Lispには無いcomposeconjoindisjoincurryrcurryを含めました。

それだけでなく、さらにそれを簡易に使えるリーダマクロもあります。#'です。

Common Lisp#'はシンボルか、lambda式にしか使えなかったのですが、CL21ではこのリーダマクロに機能を追加したもので上書きしています。

実際のコードを見せたほうが早いかもしれません。

(funcall (conjoin #'zerop #'integerp) 0)
;=> T

;; ↑と同じ
(funcall #'(and zerop integerp) 0)
;=> T


(funcall (disjoin #'plusp #'minusp) 0)
;=> NIL

;; ↑と同じ
(funcall #'(or plusp minusp) 0)
;=> NIL

#'(and zerop integerp) と書くと conjoin に展開され、#'(or zerop integerp) と書くと disjoin に展開されます。

これは単に短いだけでなく、andorという一般的な単語を使うことで直感的です。

さらに、もう想像つくと思いますが、complementに対応するものはnotです。

(funcall (complement #'zerop) 0)
;=> NIL

;; ↑と同じ
(funcall #'(not zerop) 0)
;=> NIL

もちろん、これらは組み合わせて使うこともできます。

(remove-if-not #'(and integerp
                      (or (not evenp)
                          zerop))
               (iota 11))
;=> (0 1 3 5 7 9)

composeだけはそのままcomposeを使います。

(mapcar (compose #'sin #'1+) '(1 2 3 4 5))
;=> (0.9092974 0.14112 -0.7568025 -0.9589243 -0.2794155)

;; ↑と同じ
(mapcar #'(compose sin 1+) '(1 2 3 4 5))
;=> (0.9092974 0.14112 -0.7568025 -0.9589243 -0.2794155)

あまりルールを増やすのは良くないとは思いますが、こういう地味に省略されててかつ見た目もわかりやすいという機能はどんどん取り入れていきたいですね。

ちなみにCL21はGitHubで絶賛開発中で、意見募集や議論はIssuesで行っています。興味があればぜひご参加ください。

まとめ

JavaScriptで学ぶ関数型プログラミング

JavaScriptで学ぶ関数型プログラミング