TOYOKUMO Tech Blog

トヨクモ株式会社の開発本部のブログです。

nil punningが難しい(seq vs empty?)

これはClojure Advent Calendar 2019 - Qiitaの二日めの記事です。nil punningが難しいなあと個人的に感じているので、なぜリッチヒッキーさんはnil punningを選択したのだろうか、nil punningが優れている点は何か、ということを調べて書こうと思います。調べていくうちに、いろんな考えがあるなあ、というような気持になることはできました。nil punningにも一長一短があり、実際リッチヒッキーさんも「確かに複雑ではある」とは言っていることはわかりました。オチのない記事になってしまいましたが大目に見てください。

nil punningとは何か

"pun"とは日本語では「 (同音意義語を利用した)だじゃれ」と訳される ようで、Lispでいえば、nilが異なるコンテキストでは別の意味を持って用いられること(=同音異義語)、のことを指します。特にClojureでは、ifの条件の部分ではnilは偽として扱われること、nilは空のリストや空のhash map、空のvectorとして多相的に扱われること、これらのことを指してnil punningといいます。

参考: https://lispcast.com/what-is-nil-punning/

調べようと思ったきっかけ

Clojureのスタイルガイド を見ていたら、nil punningの節で違和感を感じました。

;; good
(defn print-seq [s]
  (when (seq s)
    (prn (first s))
    (recur (rest s))))

;; bad
(defn print-seq [s]
  (when-not (empty? s)
    (prn (first s))
    (recur (rest s))))

「goodとbadが逆では?」という気持ちになりました。私が初めて触れたLispはSchemeであったこと(参考: Schemeではnilはfalsyな値ではない。(if () 1 2) => 1 になる)も要因の1つではあるかもしれないですが、文芸的プログラミングの観点から見てもempty?のほうが意思が感じられてよさそうだなと思いました。実際、私と同じように感じる人も少なからず存在するようで、nil punning confusing? · Issue #44 · bbatsov/clojure-style-guide · GitHubのようなissueがstyle guideのリポジトリに立てられていました。これに対して、リポジトリのオーナーのbbatsovさんは 「リッチヒッキーがnil punningを推奨している」というようなコメントをしています。このseq vs empty?に関しては、nil punningの問題が背後に潜んでいるとみることができます。seq派の人は、nil punningを推進しているので、whenなどの条件分岐にnilを利用することを好みます。一方empty?派の人(私もその一人)は、nilを多義的に扱うことを好まないので、whenであったりifの条件部にnilの値が紛れ込まないよう、empty?でbooleanにわざわざ変換します。empty?は(not (seq coll))として実装されているからなのでは 、という意見もあるようですが、ちょっとくらい効率が落ちても分かりやすく書いたほうが嬉しいのではと思っているので、あまり納得がいきませんでした。

以後、google groupsにあるnil punningの推進派、反対派の意見を整理していきたいと思います。

nil punning反対派のpuzzlerさん

https://groups.google.com/d/msg/clojure/gWvXoHa7-t4/rPY-nWpt4zQJ

puzzlerさんはnilが存在しないこと、偽、空を表す3つの意味で使われていることに一貫性が感じられないと言っています。「多くのケースでは動くが、nilを要素に含むlist/map/vectorを渡すと動かない」関数を多く見てきたそうです。具体的にどういうことなのか別のコメントを見てみましょう。

https://groups.google.com/d/msg/clojure/gWvXoHa7-t4/6Qw3yIq0BEAJ

(filter identity s)(keep identity s) を例として挙げています。前者はnilとfalseを消すのに対し、keepはnilだけを削除するのがconfusingだと言っていますね。filterとkeepの使い分けが生じて、バグを生む原因になってしまったということなのでしょう。 参考

user=> (keep identity [true false 1 nil])
(true false 1)
user=> (filter identity [true false 1 nil])
(true 1)

nil punning推進派のリッチヒッキーさん

google groupでnil punningについて議論が盛り上がっている中、リッチヒッキー本人がコメントを残しています。

https://groups.google.com/d/msg/clojure/gWvXoHa7-t4/HjdiQu3dqbwJ

The is no doubt nil punning is a form of complecting.

nil punningが複雑さを生むことは認めていますね。(文頭のTheはThereのtypoだと思われる)

But you don't completely remove all issues merely by using empty collections and empty?, you need something like Maybe and then things get really gross (IMO, for a concise dynamic language)

empty?を使ってもすべての問題が解決するわけではない、と言っていますね。Maybe(Optional)のようなものは動的言語には似つかわしくないという考えのようです。

I like nil punning, and find it to be a great source of generalization and reduction of edge cases overall, while admitting the introduction of edges in specific cases. I am with Tim in preferring CL's approach over Scheme's, and will admit to personal bias and a certain comfort level with its (albeit small) complexity.

慣れは必要だが、nil punningはより一般的なアプローチにつながると言っています。

However, it couldn't be retained everywhere. In particular, two things conspire against it. One is laziness. You can't actually return nil on rest without forcing ahead. Clojure old timers will remember when this was different and the problems it caused. I disagree with Mark that this is remains significantly complected, nil is not an empty collection, nil is nothing.

このコメントの文脈について補足します。 https://groups.google.com/d/msg/clojure/gWvXoHa7-t4/Ndf2EHynZdAJ puzzlerさんが (when s ...)と書いていたため、sが空のlazy-seqの時に(空のlazy-seqはtrueとして扱われる)変な動作になってしまった、というような体験を語っています。これは、nilがfalsyな値であるため、空のlazy-seqのもfalsyなのでは、と思った結果バグらせてしまった、これはnil punningが原因だ、というような趣旨のようです。

https://groups.google.com/d/msg/clojure/gWvXoHa7-t4/DLxx_dCoRkUJ それに対して、daly(=リッチヒッキーのコメントに出てくるTim)が、昔のClojureは空のlazy-seqもfalsyだった、すなわち、(when s ...) が透過的に (when (seq s) ...) と扱われていた、このアプローチのままだったらpuzzlerの言う問題は生じなかった、という指摘をしています。これ踏まえてコメントを見直してみましょう。

You can't actually return nil on rest without forcing ahead. Clojure old timers will remember when this was different and the problems it caused. I disagree with Mark that this is remains significantly complected, nil is not an empty collection, nil is nothing.

おそらく、透過的にseqを呼ぶと遅延リストのまま引きまわすことができないため問題が生じたことがあったのではないか、と解釈できそうです。nilと空のコレクションを混同するなと言っていますね。dalyのアプローチを採用しても依然として複雑さは残るだろうという考えのようです。

Second, unlike in CL where the only 'type' of empty collection is nil and cons is not polymorphic, in Clojure conj is polymorphic and there can only be one data type created for (conj nil ...), thus we have [], {}, and empty?. Were data structures to collapse to nil on emptying, they could not be refilled and retain type.

Common Lispと違ってmap, set, vector, listなど複数の種類のemptyな値があるということを言っています。nilを用意することで、map, set, vector, listなどに多相的に使うことのできる関数が書きやすくなったりするのでは、と思いましたが、具体的な例は思いつきませんでした。

At this point, this discussion is academic as nothing could possibly change in this area.

academicという言葉の解釈が難しくてわかりませんでした。pragmaticと対比する意味で使っているのかと思いましたが、リッチヒッキーがnil punningのメリットを感じる点があるならその解釈はおかしい気がしました。"nothing could possibly change in this area" は、nil punningかそうでないか、という議論に決着がつくことはないだろう、ということを言っているのでしょう。

The easiest way to think about is is that nil means nothing, and an empty collection is not nothing. The sequence functions are functions of collection to (possibly lazy) collection, and seq/next is forcing out of laziness. No one is stopping you from using rest and empty?, nor your friend from using next and conditionals. Peace!

最後に、nil punningに親しむための考えを紹介してくれています。nilとempty collectionは違うんだぞー、ということを意識しろと言ってます。でもempty?を使うのは君の自由だ!らしいです。

empty?の存在意義

最後に、上記のことを踏まえて、初めの seq vs empty?の問題に戻りたいと思います。nil punningを採用している以上、empty?という関数の存在意義は何だろうか、という疑問が浮かびます。これはclojureがJavaのライブラリを呼んだりする状況を考えれば腑に落ちるのではと考えます。clojureだけで記述している限り、nil punningの思想を徹底していればbooleanを表す値は不要ですが、Javaのライブラリがbooleanを期待しているところにnilを渡すわけにはいきません。そういう場合にsequentialな値をbooleanに変換する関数empty?が必要になってくるということなのでしょう。個人的にはjavaとの連携の部分にempty?を使い、それ以外はseqでnilかどうかを判定する方針で書いていこうかなと思いました。

書きたかったが書けなかったもの

  • coreの関数でbooleanを返す関数 contains? などの設計について書きたかったが、疲れたのでやめた。要素を含んでいるか調べる系の関数ではnilを返すことで「nilが要素として発見されたのか、それとも(条件を満たす)要素がなかったのか」が分かりづらくなるので、booleanを返すようになっていたりしないかなと適当に仮説を立てているが、調べてないので嘘かもしれない。

参考にしたもの

https://github.com/bbatsov/clojure-style-guide/issues/44

https://lispcast.com/what-is-nil-punning/

https://groups.google.com/forum/?fromgroups=#!topic/clojure/gWvXoHa7-t4

https://ejje.weblio.jp/content/pun