見出し画像

Retro-gaming and so on

簡単な英文(肯定文)を疑問文にするプログラム

Web検索をしたら、次のような面白い問題を見つけた



多分この質問者はLisp初心者だ。「書いたコードがエラーを出してるんで解決法を」ってのが質問なんだけど、ここでは「これより良いコードを書くにはどうすればいい」ってのをテーマとする。
さて、どうするか。

見たら分かるが、このコードはLisp初心者にありがちな「破壊的変更」で埋め尽くされているコードとなっている。
恐らくこの人は他の言語に於けるプログラミング言語の経験者、ではあると思う。他の「代入を多用する」言語を扱ってるとこういうスタイルのLispプログラミングは良く行われる、んだ・・・Emacs Lispなんかはこういうコードに溢れている(笑)。
確かにこれだと「Lispは括弧がやたら多い」プログラミング言語、って印象にならざるを得ないよな。
どう見ても「括弧の多さ」に必然性があるように見えない。

さて、これもこの問題で見た通り、「リストを使ってリストを加工する」に類した問題、なんだ。
従って大枠としてはreduceを使う、ってのが方針となる。
また、argに与えられたリストに対して、個別にBe動詞、つまりam、is、are、was、wereを適用していこう、ってのも「Lispの考え方」じゃない。僕らにはmemberと言う関数があるんで、これを徹底活用すべきだ。
まず、第一段階、として考えられるANSI Common Lispでのコードは次のようになるだろう。

(defun question (arg)
 (let ((be (caar
     (remove nil
         (reduce #'(lambda (y x)
              (cons (member x arg :test #'eq) y))
             '(am is are was were) :initial-value nil)))))
  (when be
   `(,be ,@(remove be arg) ?))))

例えば、'(this is a simple lisp function)と言うリストに対してreduceを適用した心臓部を

CL-USER> (reduce #'(lambda (y x)
          (cons (member x '(this is a simple lisp function)
                 :test #'eq) y))
         '(am is are was were) :initial-value nil)
(NIL NIL NIL (IS A SIMPLE LISP FUNCTION) NIL)

と書くと、(NIL NIL NIL (IS A SIMPLE LISP FUNCTION) NIL)と言うリストを返す。
これは'(this is a simple lisp function)と言うリストの中にamarewaswereは見つからず(NILは見つからなかった、って意味だ)、二番目のisを発見した、と言う事で、結果、isを先頭とした部分リストを返している。
発見したのはサーチ対象の集合、'(am is are was were)の二番目だが、返り値は逆順になっている。これはreduceの「リストは左側から走査する」性質の為だ。
また、比較対象はあくまで全部シンボルなんで、Schemeだったらmemqで判別するトコだが、ANSI Common Lispの場合、memberにはキーワード引数があり、:testで等価判定関数をすげ替える事が出来る。ここではシンボル用に#'eqを等価判定関数にしてる。
いずれにせよ、返り値のリストの中のNILは邪魔なんでremoveで除外する。

CL-USER> (remove nil
         (reduce #'(lambda (y x)
              (cons (member x '(this is a simple lisp function)
                     :test #'eq) y))
            '(am is are was were) :initial-value nil))
((IS A SIMPLE LISP FUNCTION))

結果、「リストのリスト」が返り値になるんで、目標のisを取り出すにはcarcar、つまりcaarを取ればいい。

CL-USER> (caar
      (remove nil
          (reduce #'(lambda (y x)
               (cons (member x '(this is a simple lisp function)
                      :test #'eq) y))
               '(am is are was were) :initial-value nil)))
IS

あとは、isをフィルタリングした元リスト(arg)に対してisconsして、リストのケツに?を(ケツ故に)結合すれば終了、となる。

なお、Racketで書けばこんな風になるだろう。

(define (question arg)
 (let ((be (caar
     (filter (compose1 not false?)
        (foldl (lambda (x y)
           (cons (memq x arg) y))
          '() '(am is are was were))))))
  (when be
   `(,be ,@(remove be arg) ?))))

実はこのコード、ANSI Common Lisp版とほぼロジックは同じだけど、ANSI Common Lisp版程完璧じゃない。と言うのも引数として与えられたリストにamisarewaswereのどれ一つも含まれてない場合、エラーが起きるんだ(※1)。

> (question* '(C'est bien))
. . caar: contract violation
expected: (cons/c pair? any/c)
given: '()

これは何故か、と言うとANSI Common Lispでは空リストのcarを取ると空リストが返るが、Racketでは空リストのcarは取れない。

;;; ANSI Common Lisp
CL-USER> (car nil)
NIL
;;; Racket
> (car '())
. . car: contract violation
expected: pair?
given: '()

ただ、これは本筋じゃないんでほおっておく。
っつーか、ほおっておいても「他にもっと良い手がある」んでこの問題は結果、回避可能だ。
実は、上のANSI Common Lisp/Racketのコードを見ると、気づく人は気づくだろうが、ある種非効率的なコードとなってる。それはisが二番目に見つかるにも関わらず、結果、'(am is are was were)と言うリストを最後まで走査している辺りだ。
必要なのは、isが見つかった時点でreduce/foldlの機能を速攻終了して、isを持って脱出する事だ。
Racketではこれはcall/ccをもって実現出来る。

(define (question arg)
 (let ((be (call/cc
     (lambda (return)
      (foldl (lambda (x y)
          (if (memq x arg)
           (return x)
           y))
         #f '(am is are was were))))))
  (when be
   `(,be ,@(remove be arg) ?))))

そうすれば、条件に従ったブツが見つかった途端、foldlは妨害され、そのブツはbeに束縛される。そうじゃなかったら結果、beにはfoldlの計算結果である#fが束縛され、when節に渡される・・・結果、when内のスコープは実行されない。

とまぁ、Racketにはcall/ccがあるが、じゃあANSI Common Lispならどうなるだろう。
ANSI Common Lispにはcall/ccがない。代わりと言っちゃ何だが、ANSI Common Lispにはcatchthrowがある。
ANSI Common LispのcatchthrowはRacketのcall/ccと同じモノではない。言っちゃえばcall/ccよか非力なんだけど、それでもこの程度の「情報を持って脱出」くらいはやってのける。
それらはこうやって使う。

(defun question(arg)
 (let ((be (catch 'result
      (reduce #'(lambda (y x)
           (if (member x arg :test #'eq)
            (throw 'result x)
            y))
          '(am is are was were) :initial-value nil))))
  (when be
   `(,be ,@(remove be arg) ?))))

いずれにせよ、これにて終了、だ。
色んなパターンの問題を「定式化」、つまり「ワンパターンで解ける」ようになってる、のが如何に素晴らしいか分かってもらえただろうか。
Lispで「関数型プログラミング」の方策に従う限り、色んな事柄はラクチンに解けるんだ。

なお、いつもだったらPython版も紹介するんだけど、今回は止めておく。非効率ヴァージョンなら書くのは難しくないんだけど、Pythonで「高階関数を使って」、しかも条件を満たした時、「情報を持って高階関数から脱出する」と言う事をやるにはPythonは非力過ぎるから、だ(※2)。

※1: 他にもANSI Common LispはCase insensitiveな言語なんでシンボル操作前提だと大文字・小文字を問わない、と言う特徴がある。
一方、RacketのようなScheme系言語はシンボルでさえ大文字と小文字が取れる。
結果、マジで考えるとその辺どう扱うか、と言う問題が出てくるが、今回は無視しよう。

※2: 他にも、そもそもこの問題はLisp特有の型、と言って良い、シンボル前提の問題だから、だ。
Pythonにはシンボルがないので、結果文字列を使ってどう解釈するのか、と言うカンジで問題をスライドさせなきゃならない。
敢えてPythonで書くとこうなるだろう。

def question(arg):
 args = arg.split(' ')
 be = False
 for i in args:
  if i in ('am', 'is', 'are', 'was', 'were'):
   be = i
   break
 if be:
  return f'{be} {" ".join([i for i in args if i != be])}?'

argは文字列を受け取る前提としてsplitメソッドで文字列のリストを生成する。
最終的にはフォーマット文字列を使って文字列を整形しなおす。
ただし、途中のbe動詞を探し出すトコは、脱出を考えるとfunctools.reduceには頼れず、フツーにfor文を使って破壊的変更に頼らないとならない。
また、厳密な話をすると、このコードでも「非効率性」を完全に避けてる保証がない。Lispで書いたコードでは文章に当たるargへの走査もbe動詞のリストに対する走査も「完全に止めて脱出」してるが、便利構文inが「何かを見つけた時点で走査を止めてる」保証がない、んだ。
機能的にinmemberのように「発見次第終了」を保証してるかどうか分からない(少なくともリファレンスマニュアルには何も書いていない)。
また、inmemberに比べると記述量が少ない、と言うメリットはあるが、memberは関数だけどinは構文だ(厳密には__contains__メソッドの構文糖衣)。結果、構文はユーザーが触れる部位じゃないし、「ある記述の省略記法用構文がある」と言う事は、やはり言語としての「一貫性」を犠牲にしてる、って事だ。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事