Skip to content

Instantly share code, notes, and snippets.

@y2q-actionman
Last active January 17, 2021 07:08
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save y2q-actionman/ace81e2ebda09f0daedf to your computer and use it in GitHub Desktop.
Save y2q-actionman/ace81e2ebda09f0daedf to your computer and use it in GitHub Desktop.
Lisp Advent Calendar 2014-12-11

C系言語から Common Lisp に移行した時に戸惑ったこと

この文章は、 Lisp Advent Calendar 2014 の 12/11 担当分の記事として書かれました。

概要

C系言語から Common Lisp に移行した時、「どうして Lisp はこんなに書き辛いんだ?」と思っていたことを記憶しています。

こう思った一番の要因は、 Lisp の一般的なプログラミングスタイルを承知していなかったことなのですが・・ その時代のことをなんとなく思い出して書いてみようと思います。

なにが書き辛いと思っていたのか

僕は、 C をやり、 C++ をやり、その後 Lisp に流れてきた人間でした。

そんなわけで、 C系言語で身につけたスタイルが染み付いた状態でコードを書いて遊んでいたのですが・・ どうしてこんなにネストが深くなるんだ!? と思っていたのです。

C 系言語で身につけたスタイル

C++ を書いてたとき、こんなスタイルで書いてました:

int func (...) {
  if (/* エラーを確認して */) {
    return ERROR_1; // 無理ならさっさとやめる。
  }

  if (/* またエラーを確認して */) {
    return ERROR_2; // 無理ならさっさとやめる。
  }

  // そろそろメインの処理

  int xxx = ... ; // 確認した上で値を取る。
  int yyy = xxx + 1; // なんか上のに依存した値も取ったり

  hoge1(...); // 変数 zzz のために、いくつか前処理を呼んで・・
  hoge2(...);
  int zzz = hoge3(...); // 値を取る。
  
  fuga1(...); // 変数 www のために、いくつか前処理を呼んで・・
  fuga2(...);
  int www = fuga3(...); // 値を取る。

  return xxx + yyy + zzz + www; // そして何か返す。
}

そのまま Lisp に

このノリで Lisp を書くと・・

(defun func (...)
  (if (some-condition-1)                ; エラーを確認して
      ERROR_1                           ; 無理ならさっさとやめる
      (if (some-condition-2)            ; またエラーを確認して
          ERROR_2                       ; 無理ならさっさとやめる
          ;; そろそろメインの処理
          (let ((xxx ...))              ; 確認した上で値を取る
            (let ((yyy (+ 1 xxx)))      ; なんか上のに依存した値も取ったり
              (hoge1 ...)               ; 変数 zzz のために、いくつか前処理を呼んで・・
              (hoge2 ...)
              (let ((zzz (hoge3 ...)))  ; 値を取る。
                (fuga1 ...)             ; 変数 www のために、いくつか前処理を呼んで・・
                (fuga2 ...)
                (let ((www (fuga3 ...))) ; 時たま値を取る。
                  (+ xxx yyy zzz www)))))))) ; そして何か返す。

こんな、なんだか異常にネストが深いコードを書いておりました。当時は、

  • 早く制御を戻そうとするとネストが深くなるとか、おかしいだろ!?
  • 変数を使おうとするとネストが深くなるとか、おかしいだろ!?

とかなんとか思っていた気がします。

今ならどう書くか

素直に condition を投げよう

エラーをなぜか返り値で戻そうとしていて、それが妙な制御フローを招いています。 素直にエラーを報告しましよう:

(defun func (...)
  (when (some-condition-1)              ; エラーを確認して
    (error ERROR_1))                    ; 無理ならさっさとやめる
  (when (some-condition-2)              ; またエラーを確認して
    (error ERROR_2))                    ; 無理ならさっさとやめる
  ;; そろそろメインの処理
  (let ((xxx ...))                      ; 確認した上で値を取る
    (let ((yyy (+ 1 xxx)))              ; なんか上のに依存した値も取ったり
      (hoge1 ...)                       ; 変数 zzz のために、いくつか前処理を呼んで・・
      (hoge2 ...)
      (let ((zzz (hoge3 ...)))          ; 値を取る。
        (fuga1 ...)                     ; 変数 www のために、いくつか前処理を呼んで・・
        (fuga2 ...)
        (let ((www (fuga3 ...)))          ; 値を取る。
          (+ xxx yyy zzz www))))))       ; そして何か返す。

専用の構文を使おう

let の直下で let するという、まさにその目的のために、 let* があります。 素直に使いましょう:

(defun func (...)
  (when (some-condition-1)              ; エラーを確認して
    (error ERROR_1))                    ; 無理ならさっさとやめる
  (when (some-condition-2)              ; またエラーを確認して
    (error ERROR_2))                    ; 無理ならさっさとやめる
  ;; そろそろメインの処理
  (let* ((xxx ...)                      ; 確認した上で値を取る
         (yyy (+ 1 xxx)))           ; なんか上のに依存した値も取ったり
      (hoge1 ...)                       ; 変数 zzz のために、いくつか前処理を呼んで・・
      (hoge2 ...)
      (let ((zzz (hoge3 ...)))          ; 値を取る。
        (fuga1 ...)                     ; 変数 www のために、いくつか前処理を呼んで・・
        (fuga2 ...)
        (let ((www (fuga3 ...)))          ; 値を取る。
          (+ xxx yyy zzz www)))))       ; そして何か返す。

だらしない制御フローをなんとかしよう

上記のように書いていると、変数 www を得るためには、 hoge1, hoge2, hoge3 の呼び出しが必須であるように見えます。 しかし実際には、直接に必要なのは変数 zzz であり、それさえ手に入れば依存関係はない、なんてことがあります。 それなら、こう書けるでしょう。

(defun func (...)
  (when (some-condition-1)              ; エラーを確認して
    (error ERROR_1))                    ; 無理ならさっさとやめる
  (when (some-condition-2)              ; またエラーを確認して
    (error ERROR_2))                    ; 無理ならさっさとやめる
  ;; そろそろメインの処理
  (let* ((xxx ...)                      ; 確認した上で値を取る
         (yyy (+ 1 xxx))           ; なんか上のに依存した値も取ったり
         (zzz (progn (hoge1 ...) ; 変数 zzz のために、いくつか前処理を呼んで・・
                     (hoge2 ...)
                     (hoge3 ...))); 値を取る。
         (www (progn (fuga1 ...) ; 変数 www のために、いくつか前処理を呼んで・・
                     (fuga2 ...)
                     (fuga3 ...))))     ; 値を取る。
    (+ xxx yyy zzz www)))       ; そして何か返す。

あんまりまとまってないまとめ

C系言語の場合、変数の宣言だけではネストが深くならないということもあり、適当にだらだら変数を宣言しながら書いていっても、そんなに見苦しくならない気がします。 一方 Lisp の場合、そうしているとどんどんネストが深くなり、ゲロ以下のにおいがするコードになってしまいがちでした。

今から思うと、そういうのは適切な書き方を知らなかったからなんだなあ、と思います。 つまり・・

  • let* や、もしくは with- 系マクロなど、変数を導入するマクロに一通り目を通しておく。
  • Lisp 流のエラー処理 (condition) を知っておく。
  • loopformat なども、伏魔殿っぽいなあといって忌避しない。

くらいかなあ、と思います。

実例

全編を疑似コードで通すのもアレなので、ディスクの隅に眠っていた吐き気を催すコードをとりだして、あげつらってみようと思います。

FizzBuzz やりたかったの?

コード

(defun fizzbuzz1 (max)
  (labels ((printer (x)
             (cond ((= (mod x 15) 0) "FizzBuzz")
                   ((= (mod x 3) 0) "Fizz")
                   ((= (mod x 5) 0) "Buzz")
                   (t x))))
    (dotimes (x (1+ max))
      (print (printer x)))))

修正案

  • この規模で labels いるか? flet でいいし、そもそも分けるような規模じゃないだろ。
  • printer って名前なのに print してないっていう詐欺名称。頭わいてる。
(defun fizzbuzz1 (max)
  (dotimes (x (1+ max))
    (print
     (cond ((= (mod x 15) 0) "FizzBuzz")
           ((= (mod x 3) 0) "Fizz")
           ((= (mod x 5) 0) "Buzz")
           (t x)))))

ダサい素数判定

コード

(defun prime? (n)
  (labels ((worker (n i i-max)
             (cond ((> i i-max) t)
                   ((zerop (mod n i)) nil)
                   (t (worker n (+ i 2) i-max)))))
    (cond ((<= n 1) nil)
          ((= n 2) t)
          ((zerop (mod n 2)) nil)
          (t (worker n 3 (isqrt n))))))

修正案1

なんで再帰で書きたかったの? ってのは置いておくとして

  • labels のスコープは、もっと狭くていいでしょう? 先頭に置くと、 cond のどこからでも参照されるのかと思うが、実は一箇所だけ。
  • どうして変化させるつもりがない値を引数で渡してるのだろう
(defun prime? (n)
  (cond ((<= n 1) nil)
        ((= n 2) t)
        ((zerop (mod n 2)) nil)
        (t
         (let ((i-max (isqrt n)))
           (labels ((worker (i)
                      (cond ((> i i-max) t)
                            ((zerop (mod n i)) nil)
                            (t (worker (+ i 2))))))
             (worker 3))))))

修正案2

もう loop でいいんじゃないかな・・ letwith で内包できるし。

(defun prime? (n)
  (cond ((<= n 1) nil)
        ((= n 2) t)
        ((zerop (mod n 2)) nil)
        (t
         (loop with i-max = (isqrt n)
            for i from 3 to i-max by 2
            never (zerop (mod n i))
            finally (return t)))))

日付つき Hello World

C のコード

これはまあこれでいいんですが

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

int hello_tm(const char *msg) {
        time_t now = time(NULL);

        struct tm now_tm;
        if(!localtime_r(&now, &now_tm))
                return EXIT_FAILURE;
        
        char now_str[32] = {0};
        size_t now_str_len = strftime(now_str, sizeof(now_str),
                                      "%F", &now_tm);
        if(now_str_len == 0)
                return EXIT_FAILURE;

        printf("%*s: %s\n", (int)now_str_len, now_str, msg);
        return EXIT_SUCCESS;
}

int main() {
        return hello_tm("Hello, World!");
}

そのまんま Lisp コード版

Common Lisp には locale がなく、 strftime がないので、適当に手書きしてます。

(defun hello-tm (msg)
  (let ((now (get-universal-time)))
    (multiple-value-bind (_ __ ___ d m y)
        (decode-universal-time now)
      (declare (ignore _ __ ___))
      (let ((str (with-output-to-string (stream)
                   (format stream "~D-~D-~D" y m d))))
        (format t "~A: ~A~%" str msg)))))

修正案

  • get-universal-timedecode-universal-time の組み合わせは、 get-decoded-time でよい。
  • いちいち format を二回呼ばなくても、一回の呼び出しにまとめられる。
(defun hello-tm-2 (msg)
  (multiple-value-bind (_ __ ___ d m y)
      (get-decoded-time)
    (declare (ignore _ __ ___))
    (format t "~D-~D-~D: ~A~%" y m d msg)))

Allegro CL 専用

ご存知の通り、 Common Lisp の format には ~// ディレクティブというものがあり、 format から任意の関数を呼んで仕事をさせることが出来る。

Allegro CL には、 この形式で C の strftime を使うための locale-format-time という関数があるため、単一の format で達成することが出来るのだ!

(defun hello-tm-acl (msg)
  (format t "~,V:@/locale-format-time/: ~A~%"
          "%F"
          (get-universal-time)
          msg))
(in-package :cl-user)
(defun hello-tm (msg)
(let ((now (get-universal-time)))
(multiple-value-bind (_ __ ___ d m y)
(decode-universal-time now)
(declare (ignore _ __ ___))
(let ((str (with-output-to-string (stream)
(format stream "~D-~D-~D" y m d))))
(format t "~A: ~A~%" str msg)))))
(defun hello-tm-2 (msg)
(multiple-value-bind (_ __ ___ d m y)
(get-decoded-time)
(declare (ignore _ __ ___))
(format t "~D-~D-~D: ~A~%" y m d msg)))
(defun hello-tm-acl (msg)
(format t "~,V:@/locale-format-time/: ~A~%"
"%F"
(get-universal-time)
msg))
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
int hello_tm(const char *msg) {
time_t now = time(NULL);
struct tm now_tm;
if(!localtime_r(&now, &now_tm))
return EXIT_FAILURE;
char now_str[32] = {0};
size_t now_str_len = strftime(now_str, sizeof(now_str),
"%F", &now_tm);
if(now_str_len == 0)
return EXIT_FAILURE;
printf("%*s: %s\n", (int)now_str_len, now_str, msg);
return EXIT_SUCCESS;
}
int main() {
return hello_tm("Hello, World!");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment