ユニットテストでダミーの関数に差し替える

Common Lispユニットテストを行う場合に、ユニットテストから呼び出した関数の内部で呼ばれる関数を本来の動作とは違う別の関数に差し替えたい場合がある。

関数を一時的に置き換える方法についてのメモ。

関数

例えば次の例では関数 charlie をテストしようと思った場合に、その中で呼び出される関数 alfa を差し替えたいとする。

(defun alfa ()
  :bravo)

(defun charlie ()
  (alfa))

(pprint (eq (charlie) :delta))

上記のコードは、charlie:bravo を返すため nil となる。

危険なバージョン

最低限の機能を示すための危険なバージョンが次の通り。

(defun alfa ()
  :bravo)

(defun charlie ()
  (alfa))

(setf (symbol-function 'alfa) (lambda () :delta))
(pprint (eq (charlie) :delta))

上記のコードは、charlie:delta を返すため t となる。 最後から2行目で symbol-function への setf で、関数 alfacharlie 自体の変更をすることなく呼び出される関数 alfa を変更する事が出来ている。

多分安全なバージョン

ユニットテストで一時的に関数を変更したいということを考えると、最後は元々の関数に戻って欲しい。そのバージョンが次の通り。

(defun alfa ()
  :bravo)

(defun charlie ()
  (alfa))

(let ((orig-func (symbol-function 'alfa)))
  (unwind-protect (progn
                    (setf (symbol-function 'alfa) (lambda () :delta))
                    (pprint (eq (charlie) :delta)))
    (setf (symbol-function 'alfa) orig-func)))
(pprint (eq (charlie) :delta))

最後から3行目のコードは関数 alfa が差し変わっているので t となり、最後の行は関数 alfa が元に戻っているので nil となる。 unwind-protect は内部でなんらかコンディションが発生した場合でも確実に元に戻せるように。

メソッド

(defclass delta () ())

(defmethod echo :before ((delta delta))
  (pprint :foxtrot))

(defmethod echo ((delta delta))
  (pprint :golf))

(echo (make-instance 'delta))

実行結果

:FOXTROT
:GOLF

クラスのメソッドを差し替える場合には、総称関数およびオブジェクトシステムに関連して一手間かける必要がある。 まず総称関数オブジェクトを取得し、それを使ってオブジェクトシステムからqualifierやspecifierの一致するメソッドを取得する。

一度beforeメソッドを削除して、元に戻す。

(defclass delta () ())

(defmethod echo :before ((d delta))
  (pprint :foxtrot))

(defmethod echo ((d delta))
  (pprint :golf))

(let ((d (make-instance 'delta))
       (orig-method (find-method #'echo  '(:before) (mapcar #'find-class '(delta)))))
  (remove-method #'echo orig-method)
  (echo d)
  (add-method #'echo orig-method)
  (echo d))

実行結果

:GOLF
:FOXTROT
:GOLF

Carpをチョット触るその4

Carpをチョット触るその3 - gos-k’s blog

文法とか

https://github.com/carp-lang/Carp/blob/master/docs/LanguageGuide.md を見ながら文法の確認。

特殊形式は fn let do if while ref address set! the の9個だが(これで全部?)流石にそのままでは不便で、core/Macros.carpに色々ある。

例えば条件分岐だと cond when unless case があって、本体に一つの式しか取れないので when-do も定義されている。(unless-doはないのか?)

反復だと for foreach while-do forever-do

fib

たいして変わらないけど、一応フィボナッチ数もやってみる。

(defn fib [x]
  (cond
    (= x 0) 0
    (= x 1) 1
    (+ (fib (- x 1)) (fib (- x 2)))))

(defn main []
  (print* (fib 40) @"\n"))
time carp -x fib.carp
102334155

real    0m21.038s
user    0m17.416s
sys 0m0.480s

比較対象としてCommon Lisp版。処理系はSBCL 1.5.6

(defun fib (x)
  (cond
    ((= x 0) 0)
    ((= x 1) 1)
    (t (+ (fib (1- x)) (fib (- x 2))))))

(defun main ()
  (pprint (fib 40)))
time ros run -l fib.lisp -e "(main)" -q
102334155
real    0m8.985s
user    0m7.405s
sys 0m0.109s

2倍位Carpが遅いがまあこんなもんなのかな? Carpの出力コードのfib関数はこんな感じ。

int fib(int x) {
    int _39;
    bool _8 = Int__EQ_(x, 0);
    if (_8) {
        int _11 = 0;
        _39 = _11;
    } else {
        int _37;
        bool _17 = Int__EQ_(x, 1);
        if (_17) {
            int _20 = 1;
            _37 = _20;
        } else {
            Lambda _23 = { .callback = fib, .env = NULL, .delete = NULL, .copy = NULL }; //Sym fib LookupRecursive
            int _27 = Int__MINUS_(x, 1);
            int _28 = _23.env ? ((int(*)(LambdaEnv, int))_23.callback)(_23.env, _27) : ((int(*)(int))_23.callback)(_27);
            Lambda _29 = { .callback = fib, .env = NULL, .delete = NULL, .copy = NULL }; //Sym fib LookupRecursive
            int _33 = Int__MINUS_(x, 2);
            int _34 = _29.env ? ((int(*)(LambdaEnv, int))_29.callback)(_29.env, _33) : ((int(*)(int))_29.callback)(_33);
            int _35 = Int__PLUS_(_28, _34);
            int _36 = _35;
            _37 = _36;
        }
        int _38 = _37;
        _39 = _38;
    }
    return _39;
}

再帰呼び出しでちょっと遠回りしてるが、まだまだシンプルで素直に読める。

CFFI

出力がCなのでCFFIもサポート。 CarpからCのヘッダファイルを読み込む。

  • system-include
  • local-include

Carp上での型定義。

  • register
  • register-type

これやってて混乱するのはString型に感することで、Carpの String はCの char * に対応する。( const char * か?) それだけならいいが、Carpの文字列リテラル&String でありCの char * * に対応する。 なので例えば、

(system-include "stdio.h")
(register puts (Fn [String] Int))

(defn main []
  (puts "Hello, world!"))

と書きたくなるんだけど、Carpのputsに渡しているのが文字列リテラルで型が &String なのに対して、Cのputsは char * でCarpの String だから素直にいかない。 この辺について見てると、core/IO.carpcore/carp_io.h でprintln関数について、

void IO_println(String *s) { puts(*s); }

とやってCarpの &String で受け取って、Cの関数でデリファレンスするという回りくどいことをするもの、らしい。 Carp側だと str で型を合わせられる。

(system-include "stdio.h")
(register puts (Fn [String] Int))

(defn main []
  (puts (str "Hello, world!")))
carp -x cffi.carp
Hello, world!
[RUNTIME ERROR] '"./out/Untitled"' exited with return value 14.

putsの返り値がそのままmainに返ってエラーがでてるけど、一応動作はする。

Carpをチョット触るその3

Carpをチョット触るその2 - gos-k’s blog

前回に続いてその3。 よくみたら core/Macros.carpfor とか cond とか色々あった。

(defn main []
  (for [n 1 21]
    (print* (cond
              (and (= (mod n 3) 0)
                   (= (mod n 5) 0)) @"fizzbuzz"
              (= (mod n 5) 0) @"buzz"
              (= (mod n 3) 0) @"fizz"
              (Int.str n))
            (if (< n 20)
                @" "
                @"\n"))))
carp -x fizz-buzz5.carp
1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz 16 17 fizz 19 buzz

Carpをチョット触るその2

Carpをチョット触る - gos-k’s blog

前回に続いてCarpその2。 hello worldは書いたので、続いてfizz bazzを書く。

(defn fizz-buzz [n]
  (do
    (if (and (= (mod n 3) 0)
             (= (mod n 5) 0))
        (IO.print "fizzbuzz ")
        (if (= (mod n 5) 0)
            (IO.print "buzz ")
            (if (= (mod n 3) 0)
                (IO.print "fizz ")
                (do
                  (IO.print &(Int.str n))
                  (IO.print " ")))))
    (if (< n 20)
        (fizz-buzz (+ n 1))
        0)))

(defn main []
  (fizz-buzz 1))
carp -x fizz-buzz.carp
1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz 16 17 fizz 19 buzz 

if式の中にprintが沢山あるのでまとめたいのだが、うまく動かず。

(defn fizz-bazz [n]
  (do
    (IO.print (if (and (= (mod n 3) 0)
                       (= (mod n 5) 0))
                  "fizzbuzz"
                  (if (= (mod n 5) 0)
                      "buzz"
                      (if (= (mod n 3) 0)
                          "fizz"
                          &(Int.str n)))))
    (if (< n 20)
        (do
          (IO.print " ")
          (fizz-bazz (+ n 1)))
        (do
          (IO.println "")
          0))))

(defn main []
  (fizz-bazz 1))
carp -x fizz-buzz2.carp
  fizz  buzz fizz   fizz buzz  fizz   fizzbuzz   fizz  buzz

型エラーも出ずに実行できるが、数字が出力されない。 何かやり方間違ってるんだろうがよく分からん。。。

出力コードの対象部分を見ると

            } else {
                String _59 = Int_str(n);
                String* _60 = &_59; // ref
                String* _61 = _60;
                String_delete(_59);
                _62 = _61;
            }

参照を渡しているが、参照先の文字列がスコープから外れるから削除されているように見える。

(defn fizz-bazz [n]
  (do
    (IO.print &(if (and (= (mod n 3) 0)
                        (= (mod n 5) 0))
                   @"fizzbuzz"
                   (if (= (mod n 5) 0)
                       @"buzz"
                       (if (= (mod n 3) 0)
                           @"fizz"
                           (Int.str n)))))
    (if (< n 20)
        (do
          (IO.print " ")
          (fizz-bazz (+ n 1)))
        (do
          (IO.println "")
          0))))

(defn main []
  (fizz-bazz 1))
carp -x fizz-buzz3.carp
1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz 16 17 fizz 19 buzz

これで正しく動いてるように見えるが、文字列リテラルをわざわざコピーするもんなのか?

最後にループで書いてみる。

(defn main []
  (do
    (let [n 1]
      (while (<= n 20)
        (do
          (IO.print &(if (and (= (mod n 3) 0)
                              (= (mod n 5) 0))
                         @"fizzbuzz"
                         (if (= (mod n 5) 0)
                             @"buzz"
                             (if (= (mod n 3) 0)
                                 @"fizz"
                                 (Int.str n)))))
          (IO.print (if (< n 20)
                        " "
                        "\n"))
          (set! n (inc n)))))
    0))
carp -x fizz-buzz4.carp 
1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz 16 17 fizz 19 buzz

複数の式を列挙出来るのはdoだけで、defnのbodyですらダメなのか。 中々凄いな。。。

Carpをチョット触る

Lisp方言の一つであるCarpをちょっと触ってみた。 github.com

関係ないけど、ClispとかClaspとか最近Cで始まってpで終わる処理系を見ることが多くて、一瞬区別がつかない。

インストール

Ubuntu 18.04にインストールした。

Carpの処理系自体はHaskellで書かれているようなので、stackをインストールする。

apt install haskell-stack
stack upgrade

v0.3.0をローカルにインストールする。

mkdir $HOME/.local/share
cd $HOME/.local/share
git clone https://github.com/carp-lang/Carp
cd Carp
git checkout v0.3.0
stack build
stack install

環境変数を設定する。

export CARP_DIR=$HOME/.local/share/Carp

CARP_DIRが設定されないままcarpを実行するとエラーになる。

carp: EvalException I can't find a file named: './/core/Core.carp'

実行

実行する。

carp
Welcome to Carp 0.3.0
This is free software with ABSOLUTELY NO WARRANTY.
Evaluate (help) for more information.
> 

REPLが起動した。

(quit)

REPLが終了した。

(help)

でヘルプが表示される。

hello world

伝統と格式のhello world

(use IO)

(defn main []
  (println "Hello, World!"))

コマンドライン引数に -x をつけると、main関数が呼ばれる。

carp -x hello-world.carp
Hello, World!

生成されるコード本体を一部抜粋

int main(int argc, char** argv) {
    carp_init_globals(argc, argv);
    static String _4 = "Hello, World!";
    String *_4_ref = &_4;
    IO_println(_4_ref);
}

数値を表示してみる。

(use IO)
(use Int)

(defn main []
  (IO.println &(Int.str 1)))
carp -x print-1.carp
1

(Int.str 1) ではなく &(Int.str 1) なのがポイントで、何故ならprintlnの引数が &String だけどstrの返り値は String だから。 そうなのか。。。

筋肉量増えた時にやってた事

先日筋肉量増やすにはどうするのかとかいう話になった。

自分の場合パワーアップの為に筋トレをやっていたが、現実問題安全確実なパワーアップの為には筋肉量は必要だろうとも思っていた。 で、当初筋トレしても1kgたりとも筋肉らしきものが増えなかったが、何か色々変えているうちにどれが効果あったのかは実験してないけどある程度増えた。 今でも筋トレ自体はやってるものの、最近そういう方向でやってないなくて忘れかけてるのでメモ。

  • マシンのみから、フリーウェイトを入れた。
    • 当初はバーベルでbig3とかその辺から。
    • のちに上半身はダンベル、下半身はバーベルに変更。
  • 3セット10レップを辞めて、メインを2セット5レップとか徐々に高重量側に変えた。
    • 主にフリーウェイトの多関節種目に関して。
  • 食事は、朝は菓子パンと卵2個とりんごジュース、昼は居酒屋のランチセット、夜は牛丼並盛サラダセットとかそんなの。
    • 4食もやってみたけど、筋肉量的なプラスはなかったのでやらなくなった。
    • ささ身しか食べないのって色んな人に聞かれるけど、なんかTVでやってたとかだろうか?
      • それどう考えても減量食で筋肉減るよな。。。
  • 全身メニュー週3回を辞めて、1週間サイクル3分割メニューに変えた。
    • 5分割位まで増えたり3分割に戻したりとか色々変えた。
    • 1週間サイクルは基本変わらず。
  • サプリメントプロテインクレアチンと白砂糖。
    • 面倒なので筋トレ後に、WPC20gクレアチン5g白砂糖40gを混ぜて飲んでた。
      • 入れるにしてもせめて粉飴とかにするべきだとは思うが。。。
    • MRPその他も飲んでたことあるけど、効果が感じられなかったので結局辞めた。

まあこんなところか? 実際何がよかったのかよく分からんし、他人がこれやって効果あるのかもよく分からん。

5分割当時の種目がこんな感じ。

  • 脚後側
    • バーベルデッドリフト
    • マシンスタンディングカーフ
    • マシンライイングレッグカール
    • マシンシーテッドカーフ
    • マシンシーテッドレッグカール
  • 上半身下側
    • 45度オブリーク
    • ハンギングレイズ
    • マシンロータリートルソ
    • フロントネックカール
    • バックネックカール
    • マシンネックサイド
  • 脚前側
    • バーベルフルスクワット
    • マシンインナーサイ
    • マシンアウターサイ
    • マシンレッグエクス
    • ダンベルシュラッグ
    • マシンティビア
  • 上半身上側
    • ワイドプルアップ
    • ダンベルアーノルドプレス
    • ローケーブルロー
    • バーベルベンチプレス
    • マシンミッドロー
    • ケーブルビハインドネックラットプルダウン
    • バーベルアップライトロー
    • ダンベルサイドレイズ

Helixキーボードのキーマップ変更

HelixキーボードのhexファイルをUbuntuのavrdudeで書き込む - gos-k’s blog から大分時間が経過したが、やっとキーマップ変更に着手した。

以下参考にしたもの

QMKのファームウェアをもらってきて、自分用のキーマップを編集する。

git clone https://github.com/qmk/qmk_firmware
cp -r keyboards/helix/rev2/keymaps/default keyboards/helix/rev2/keymaps/gos-k
lem keyboards/helix/rev2/keymaps/gos-k/keymap.c

ファームウェアを書き込む。

$ make helix:gos-k:avrdude 
QMK Firmware 0.6.361
WARNING:
 Some git sub-modules are out of date or modified, please consider running:
 make git-submodule
 You can ignore this warning if you are not compiling any ChibiOS keyboards,
 or if you have modified the ChibiOS libraries yourself. 

Making helix/rev2 with keymap gos-k and target avrdude

avr-gcc (GCC) 5.4.0
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Size before:
   text    data     bss     dec     hex filename
      0   16918       0   16918    4216 .build/helix_rev2_gos-k.hex

Copying helix_rev2_gos-k.hex to qmk_firmware folder                                                 [OK]
Checking file size of helix_rev2_gos-k.hex                                                          [OK]
 * The firmware size is fine - 16918/28672 (11754 bytes free)
Detecting USB port, reset your controller now..................
Device /dev/ttyACM0 has appeared; assuming it is the controller.
Waiting for /dev/ttyACM0 to become writable.

Connecting to programmer: .
Found programmer: Id = "CATERIN"; type = S
    Software Version = 1.0; No Hardware Version given.
Programmer supports auto addr increment.
Programmer supports buffered memory access with buffersize=128 bytes.

Programmer supports the following devices:
    Device code: 0x44

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e9587 (probably m32u4)
avrdude: NOTE: "flash" memory has been specified, an erase cycle will be performed
         To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file ".build/helix_rev2_gos-k.hex"
avrdude: input file .build/helix_rev2_gos-k.hex auto detected as Intel Hex
avrdude: writing flash (16918 bytes):

Writing | ################################################## | 100% 1.34s

avrdude: 16918 bytes of flash written
avrdude: verifying flash memory against .build/helix_rev2_gos-k.hex:
avrdude: load data flash data from input file .build/helix_rev2_gos-k.hex:
avrdude: input file .build/helix_rev2_gos-k.hex auto detected as Intel Hex
avrdude: input file .build/helix_rev2_gos-k.hex contains 16918 bytes
avrdude: reading on-chip flash data:

Reading | ################################################## | 100% 0.16s

avrdude: verifying ...
avrdude: 16918 bytes of flash verified

avrdude: safemode: Fuses OK (E:FB, H:D8, L:FF)

avrdude done.  Thank you.

特に難しい事もなく、さっさとやればよかった。。。