レガシーコード生産ガイド

私に教えられることなら

オリジナル記法・PicoLisp風命名規則・簡単なオブジェクトシステムをScheme(Gauche)で

らくがきです。

なんとなくコードを書き散らしたくなったので、オリジナル記法とPicoLisp風命名規則を使い、オブジェクトシステムをScheme(Gauche)で書いてみた。

ネストが苦手

(昔この日記に書いた事を再度書く)

集中力が無いのか、脳のワーキングメモリが少ないのか、理由はわからないけどとにかくネストした式が苦手だ。letを多用して中間の値にどんどん名前をつけていかないと頭がおかしくなりそうになる。インデントも苦手で、どんどん右斜め下に流れていくコードを見ていると気が狂いそうになる。

さらに複雑な条件を把握するのも苦手なので、簡単な「除外する」条件で処理を減らしていく早期リターンも多用する。

letと早期リターンが絡むと、例えば次のようなコードの見た目になってやる気がゼロになる。

(defn foo [Bar]
  (let* ([X (baz Bar)]
         [Y (baz X)])
    (if (not (hoge? X))
        Y
        (let* ([Z (if (fuga? (baz Y)) Y (hoge Y))])
          (begin
            (if (null? Z) (print Z))
            (if (fuga? Z)
                Z
                (fuga Z)))))))

で、以前自作したブラウザ上のLisp環境「DenLisp」では、次のように書けるようにした。

(defn foo [Bar]
  let X (baz Bar)
  let Y (baz X)
  if not (hoge? X) Y
  let Y if not (fuga? (baz Y)) (hoge Y)
  do if (null? Y) (print Y)
  if (fuga? Y) Y
  (fuga Y))

S式大好きなのでS式から外れたりはしていない。専用の構文解析を行ってるわけではなく、単に関数の本体を1つのリストとして、前から順番にマクロで展開している。

ただ、この書き方が自分にとって本当に良いのかは疑問に思い始めている。コードを読み返す時にちょっと辛い。行あたりの情報密度が高すぎて疲れるのかもしれない。次のようにインデントするとちょっと楽に読めるからだ。

(defn foo [Bar]
  let X
    (baz Bar)
  let Y
    (baz X)
  if not (hoge? X)
    Y
  let Y if not (fuga? (baz Y))
    (hoge Y)
  do if (null? Y)
    (print Y)
  if (fuga? Y)
    Y
  (fuga Y))

この記事の最後のコード全体に、Gaucheで使えるマクロ定義も書いてある。

PicoLisp

いろいろあって、やっぱりWorse Is Betterかもなあと強く思うようになったので、それを感じるPicoLispを最近眺めている。(PicoLispがWorseと言ってることになるけど、Betterとも言ってるので許してください)

リストとシンボル(interned/transition)と数値しかない・関数もリスト・動的束縛・fexpr系とかなりオモシレッオモシレッな処理系なんだけど、やっぱりLexicalな束縛とmacroexpand可能なマクロの方が楽だし好きなので、Scheme(Gauche)で命名規則とオブジェクトシステムを真似して書いてみることにした。

真似したとこ

  • defineはde、define-methodはdmなどの短縮
  • メソッド内のSelf(他言語のthis)は勝手に束縛する
  • 関数名は普通にfoo、ローカル変数はFooBarグローバル変数*FooBar、クラス名は+Fooという命名規則。特にローカル変数は、慣れると余計に名前を考えなくて良くなって良い
  • (class +Foo)以降でdmするとそのクラスへのメソッド追加になる。インデントが1つ減って良い

真似しなかったとこ

  • オブジェクトに複数のクラスを指定できるのはあんまり好きじゃないのでなし
  • メタオブジェクトもなし

次のコードが動く。

(class +Pokemonsta/Type)

(dm init [Name]
  (set> Name))

(dm name []
  (get> Name))

(de *TypeNormal (new +Pokemonsta/Type "ノーマル"))
(de *TypeDenki (new +Pokemonsta/Type "でんき"))



(class +Pokemonsta)

(dm init [Name Type]
  (set> Name)
  (set> Type))

(dm name []
  (get> Name))

(dm type []
  (get> Type))

(dm info []
  `((Name ,(name Self))
    (Type ,(name (type Self)))))


(de (normal-pokemonsta Name)
  (new +Pokemonsta Name *TypeNormal))

(de (denki-pokemonsta Name)
  (new +Pokemonsta Name *TypeDenki))


(yaa
 let PaikaChu (denki-pokemonsta "パイカテフ")
 (print (info PaikaChu))
 (print (inspect PaikaChu)))

実行結果

$ gosh pico.scm
((Name パイカテフ) (Type でんき))
((Class +Pokemonsta) (Ancestors (+Object +Nil)) (InstVars (Type Name)))

実行を意味するdobeginシンタックスとして使われているので、九州(たぶん福岡佐賀長崎)の小学生で実行を意味する「ヤー!」を使った。

コード全体

(use srfi-1)

'(; emacs用
  (put 'de 'scheme-indent-hook 'defun)
  (put 'dm 'scheme-indent-hook 'defun)
  (put 'when 'scheme-indent-hook 1)
  (put 'unless 'scheme-indent-hook 1)
  (put 'loop 'scheme-indent-hook 'defun)
  )

(define-syntax yaa
  (syntax-rules (if not let do)
    ;; if
    [(_ if not Test Then Else ...)
     (if (not Test) Then (yaa Else ...))]
    [(_ if Test Then Else ...)
     (if Test Then (yaa Else ...))]
    ;; let
    [(_ let Var if not Test Expr Then ...)
     (let ([Var (if (not Test) Expr Var)]) (yaa Then ...))]
    [(_ let Var if Test Expr Then ...)
     (let ([Var (if Test Expr Var)]) (yaa Then ...))]
    [(_ let Var Val Then ...)
     (let ([Var Val]) (yaa Then ...))]
    ;; do
    [(_ do if Test Expr Then ...)
     (begin (if Test Expr) (yaa Then ...))]
    [(_ do Expr Then ...)
     (begin Expr (yaa Then ...))]
    ;; then...
    [(_ Then ...)
     (begin Then ...)]))

(define-syntax fn
  (syntax-rules ()
    [(_ Args Body ...)
     (lambda Args (yaa Body ...))]))

;; de: define
(define-syntax de
  (syntax-rules ()
    [(_ Var Val)
     (define Var Val)]
    [(_ (Var . Args) Body ...)
     (define Var (fn Args Body ...))]))

;; (let loop ...) だとyaaを挟むことになるので
(define-macro (loop Inits . Body)
  `(let recur ,Inits (yaa ,@Body)))

(de *ClassTable (make-hash-table))
(de *GenericTable (make-hash-table))

(de (make-cls-body Name Parent)
  let MethodTable (make-hash-table)
  (list MethodTable Name Parent))

(de (method-table Cls) (first Cls))
(de (cls-name Cls) (second Cls))
(de (cls-parent Cls) (third Cls))

(de (register-cls Name Cls)
  (hash-table-put! *ClassTable Name Cls))

(de (make-cls Name Parent)
  let Cls (make-cls-body Name Parent)
  (register-cls Name Cls)
  Cls)

(de +Nil (make-cls '+Nil ()))
(de +Object (make-cls '+Object +Nil))
(de *Class +Object)

(de (set-current-cls! Name)
  if not (hash-table-exists? *ClassTable Name) #f
  (set! *Class (hash-table-get *ClassTable Name))
  #t)

(de (new Cls . Args)
  let InstVars (make-hash-table)
  let Obj (list Cls InstVars)
  do if (pair? Args) (apply init Obj Args)
  Obj)

(de (obj-class Obj) (first Obj))
(de (obj-inst-vars Obj) (second Obj))

(de (lookup-method Obj Mes)
  (loop ([Cls (obj-class Obj)])
    if (null? Cls) (error (format #f "ない: ~A" Mes))
    let MT (method-table Cls)
    if (hash-table-exists? MT Mes) (hash-table-get MT Mes)
    (recur (cls-parent Cls))))

(de (invoke-method Obj Mes Args)
  let f (lookup-method Obj Mes)
  (apply f Obj Args))

(de (add-method Mes Meth)
  let Cls *Class
  let MT (method-table Cls)
  (hash-table-put! MT Mes Meth))

;; dm: define-method
(define-macro (dm Mes Args . Body)
  `(begin
     (when (not (hash-table-exists? *GenericTable ',Mes))
       (de (,Mes Obj . MethArgs)
         (invoke-method Obj ',Mes MethArgs))
       (hash-table-put! *GenericTable ,Mes #t))
     (let* ([Meth (lambda (Self ,@Args) ,@Body)])
       (add-method ',Mes Meth))))

(define-syntax class
  (syntax-rules ()
    [(_ Name) (class Name +Object)]
    [(_ Name Parent)
     (unless (set-current-cls! 'Name)
       (de Name (make-cls 'Name Parent))
       (set-current-cls! 'Name))]))


(de Nil (new +Nil))

(class +Object)

(dm init [])

(dm get-inst-var [Name]
  (hash-table-get (obj-inst-vars Self) Name))

(dm set-inst-var [Name Val]
  (hash-table-put! (obj-inst-vars Self) Name Val))

(define-macro (get> Name)
  `(get-inst-var Self ',Name))

(define-macro (set> Name :optional Val)
  `(set-inst-var Self ',Name ,(if (undefined? Val) Name Val)))

(dm ancestors []
  (loop ([Cls (cls-parent (obj-class Self))]
         [Acc ()])
    if (null? Cls) (reverse Acc)
    (recur (cls-parent Cls) (cons Cls Acc))))

(dm inspect []
  `((Class ,(cls-name (obj-class Self)))
    (Ancestors ,(map cls-name (ancestors Self)))
    (InstVars ,(hash-table-keys (obj-inst-vars Self)))))

;; Example
;; =============================================================================

(class +Pokemonsta/Type)

(dm init [Name]
  (set> Name))

(dm name []
  (get> Name))

(de *TypeNormal (new +Pokemonsta/Type "ノーマル"))
(de *TypeDenki (new +Pokemonsta/Type "でんき"))



(class +Pokemonsta)

(dm init [Name Type]
  (set> Name)
  (set> Type))

(dm name []
  (get> Name))

(dm type []
  (get> Type))

(dm info []
  `((Name ,(name Self))
    (Type ,(name (type Self)))))


(de (normal-pokemonsta Name)
  (new +Pokemonsta Name *TypeNormal))

(de (denki-pokemonsta Name)
  (new +Pokemonsta Name *TypeDenki))


(yaa
 let PaikaChu (denki-pokemonsta "パイカテフ")
 (print (info PaikaChu))
 (print (inspect PaikaChu)))

DenLispとDen供養

イメージベースっぽくブラウザ上で開発したいんじゃい、と今年6月ぐらいからLisp方言処理系の「DenLisp」を作り、更にSmalltalk風言語でイチから書きなおした「Den」を、ちょっとした仕事や日常のツールに便利に使っていた。

しかし段々と、この方針は上手く行かないのではという疑念が強まっていき、昨日パッと「アッもうだめだ」と悟ったので、理由などをここに簡単にメモして供養とする。

Denはgithubに置いている。多分この記事が消えるときに消すと思う。

github.com

(DenLispの方はもしかしたら最新版はこの世に存在しないかもしれない……)

たぶんWorse Is Better

編集→ビルド→実行・確認→編集 のループに時間がかかるとイライラするので、ブラウザで開発し、機能などの変更をその場で確認したいというのが最初の動機だった。

ある程度、色や遷移などを確認しながら同一画面でコードを編集していくことは達成できた。しかし、状態の管理場所を変更したり、URLなどページ毎に確認する必要があるものを変更すると、結局アプリケーションやIDE全体を更新することになった。マウスで操作する必要があるので、確認にかかるストレスは大して変わらなかった。

なんで最初の動機が生まれたかというと、もっと粘土をこねくり回すようにコードを書きたいと思ったから。目の前にあるものを、少しずつ形を整えていくのが自然なものづくりで、だから楽しいのだと思っていた。

でも冷静になって考えてみると、例えば紙飛行機を作って飛ばしてみるだとか、作曲して一回通して弾いてみるとか、編集→確認のループが必要な創作活動はいくらでもある。そして、それぞれの試行が独立だからこその便利さを痛感することが何度もあった。

それと、アンチフラジャイルに強く影響を受けてものごとの脆さ・反脆さを意識するようになったので、ファイルベースの反脆さに気づき、一周して好きになってしまった。Worse Is Betterに戻ってきた。好き嫌いでやってきたので、好き嫌いが変わったら行動も変えましょう。

などなど。言葉にできない部分も大きくて、それは言葉にする必要が無かったり、言葉にしようとしてしまうと不満が残りそうなので、ここでやめておく。

やってよかったこと・わかったこと

  • snabbdomを2つの言語で移植したのでかなりVirtualDOMの理解が深まった。もうソラで書けると思う。snabbdomベースのVueの動作もめちゃくちゃ良くわかった。
  • DenはSmalltalk風の方言だけど、St80のifTrue:とかは正直読みにくいしダサいと思ってたので、if文とloop文を導入した。結果的にものすごく読みやすくて大当たりだった。あとletも。
  • メソッド単位の編集はやっぱりかなり楽。インデントレベルが最低でも1減るから?
  • テンプレート文字列(ES6のやつ)と、それを直に書けるテンプレートメソッドの導入はものすごく便利だった。
  • とにかく異常な量のコードを書いたのでとても自信がついた

今後

なんかやる前とだいぶん価値観が変わってしまった(これはアンチフラジャイルのせい)。前はプログラミング言語を構成する概念や構文の、一貫性・シンプルさ・美しさをとても大事だと思ってたんだけど、180度変わって、そういったことが好きになれなくなった。人工的な、コンクリートの壁で囲まれた部屋みたいな印象を受ける。

手段にこだわらず、とにかくあったら便利だなと思うものをバリバリ書きたい。

ほな、また……。

広告を非表示にする

説得力タンクと事実タンク

人間(少なくとも自分)の心には、説得力タンクみたいなものがある。

何かに挑戦したり、信じたいものがあるとき、説得力タンクを満たそうと行動してしまうことがある。

以下は自分がやらかした行動例

  • 人の絵を上手く描きたい→ 人体の描き方を探してみる、ドリルのような練習をしてみる
  • 税務関係のある作業のやり方がわからない→ ネットや本で情報を探してみる
  • ある塗料の臭いが我慢できるか、防腐効果がどれぐらいなのか知りたい→ ネットや本で情報を探してみる
  • ◯◯言語で複雑なWebアプリケーションが作れるか→ 成功例の記事を探す、基礎となる(ようにみえる)簡単なものを作ってみる

説得力タンクは穴が空いていて、永遠に満タンになることはない。しかも、どれだけ説得力を集めても更に説得力を集めたくなる。悪循環に陥り、ずっと実際の行動に移れなくなる。

実際の行動とは、事実タンクに事実を溜める作業を指す。事実タンクは頑丈で、中の事実が減ることはない。

  • 人の絵を上手く描きたい→ 実際に描いて、気に入らないところを修正する
  • 税務関係のある作業のやり方がわからない→ 税務署などの機関にメールして聞く
  • ある塗料の臭いが我慢できるか、防腐効果がどれぐらいなのか知りたい→ 買って塗ってみる
  • ◯◯言語で複雑なWebアプリケーションが作れるか→ 業務で作ってるものと同じ機能のものを作る

事実タンクは、成功タンクでは無いことに注意したい。何かが失敗することもまた事実で、説得力とは違う、貴重で本物の情報となる。失敗を元に修正していけばやがて成功に辿り着けるかもしれないし、元々成功が無い道ならどこかで見切りをつけられるかもしれない。 例えば上の例だと、次のような失敗を得られる可能性がある。

  • 人の絵を上手く描きたい→書いてみる→ 気に入らない絵ができる
  • 税務関係のある作業のやり方がわからない→ メールしてみる→ メールの返信がこない
  • ある塗料の臭いが我慢できるか、防腐効果がどれぐらいなのか知りたい→ 買って塗ってみる→ 臭い、金を失う
  • ◯◯言語で複雑なWebアプリケーションが作れるか→ 作ってみる→ 美しく書けるという触れ込みだったのに、重複だらけの意味不明なコードになってしまう

でも成功タンクに成功だけを溜めようとすると、上記の行動は避けるべき、又は最後に成功があることを確証しないと動けなくなる。結果、説得力タンクを満たす行動に耽ってしまう。

まだまだ気を抜くとすぐ成功しようとしてしまい、説得力を探してしまう。そういったときに「事実を得よう」と切り替えられる何かがあればいいんだけど。